Skip to content

Commit 002b0ee

Browse files
committed
fix(workflows): new translation flow
1 parent ce451e7 commit 002b0ee

3 files changed

Lines changed: 388 additions & 0 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Extracts changed keys from git diff of lang/main.json.
5+
* Outputs a JSON object with the changed keys and their English values.
6+
*/
7+
8+
const { execSync } = require('child_process');
9+
const fs = require('fs');
10+
11+
/**
12+
* Gets the git diff for main.json between HEAD~1 and HEAD.
13+
*
14+
* @returns {string} The git diff output.
15+
*/
16+
function getMainJsonDiff() {
17+
try {
18+
return execSync('git diff HEAD~1 HEAD -- lang/main.json', {
19+
encoding: 'utf-8'
20+
});
21+
} catch (error) {
22+
console.error('Error getting git diff:', error.message);
23+
process.exit(1);
24+
}
25+
}
26+
27+
/**
28+
* Extracts changed and removed keys from the git diff.
29+
*
30+
* @param {string} diff - Git diff output.
31+
* @returns {Object} Object with 'added' (keys to translate) and 'removed' (keys to delete).
32+
*/
33+
function extractChangedKeys(diff) {
34+
const added = {};
35+
const removed = [];
36+
const lines = diff.split('\n');
37+
38+
for (const line of lines) {
39+
// Look for added or modified lines (starting with +).
40+
if (line.startsWith('+') && !line.startsWith('+++')) {
41+
const match = line.match(/^\+\s*"([^"]+)":\s*"(.*)"/);
42+
if (match) {
43+
const [, key, value] = match;
44+
45+
// Remove trailing comma if present.
46+
const cleanValue = value.replace(/",?\s*$/, '');
47+
added[key] = cleanValue;
48+
}
49+
}
50+
// Look for removed lines (starting with -).
51+
else if (line.startsWith('-') && !line.startsWith('---')) {
52+
const match = line.match(/^\-\s*"([^"]+)":\s*"(.*)"/);
53+
if (match) {
54+
const [, key] = match;
55+
removed.push(key);
56+
}
57+
}
58+
}
59+
60+
return { added, removed };
61+
}
62+
63+
/**
64+
* Main function.
65+
*/
66+
function main() {
67+
const diff = getMainJsonDiff();
68+
69+
if (!diff || diff.trim() === '') {
70+
console.error('No changes detected in lang/main.json');
71+
process.exit(0);
72+
}
73+
74+
const { added, removed } = extractChangedKeys(diff);
75+
76+
if (Object.keys(added).length === 0 && removed.length === 0) {
77+
console.error('No translatable changes detected');
78+
process.exit(0);
79+
}
80+
81+
// Output JSON to stdout.
82+
console.log(JSON.stringify({ added, removed }, null, 2));
83+
}
84+
85+
main();
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Merges translated keys into the Bulgarian translation file.
5+
* Reads translations from stdin (JSON format) and updates lang/main-bg.json.
6+
*/
7+
8+
const fs = require('fs');
9+
const path = require('path');
10+
11+
/**
12+
* Reads JSON from stdin.
13+
*
14+
* @returns {Promise<Object>} Parsed JSON object.
15+
*/
16+
function readStdin() {
17+
return new Promise((resolve, reject) => {
18+
let data = '';
19+
20+
process.stdin.on('data', chunk => {
21+
data += chunk;
22+
});
23+
24+
process.stdin.on('end', () => {
25+
try {
26+
resolve(JSON.parse(data));
27+
} catch (error) {
28+
reject(new Error('Failed to parse JSON from stdin: ' + error.message));
29+
}
30+
});
31+
32+
process.stdin.on('error', reject);
33+
});
34+
}
35+
36+
/**
37+
* Reads and parses a JSON file.
38+
*
39+
* @param {string} filePath - Path to the JSON file.
40+
* @returns {Object} Parsed JSON object.
41+
*/
42+
function readJsonFile(filePath) {
43+
const fullPath = path.join(process.cwd(), filePath);
44+
const content = fs.readFileSync(fullPath, 'utf-8');
45+
return JSON.parse(content);
46+
}
47+
48+
/**
49+
* Writes a JSON object to a file with proper formatting.
50+
*
51+
* @param {string} filePath - Path to the JSON file.
52+
* @param {Object} data - Data to write.
53+
*/
54+
function writeJsonFile(filePath, data) {
55+
const fullPath = path.join(process.cwd(), filePath);
56+
57+
// Sort keys alphabetically.
58+
const sortedData = Object.keys(data)
59+
.sort()
60+
.reduce((obj, key) => {
61+
obj[key] = data[key];
62+
return obj;
63+
}, {});
64+
65+
const content = JSON.stringify(sortedData, null, 4) + '\n';
66+
fs.writeFileSync(fullPath, content, 'utf-8');
67+
}
68+
69+
/**
70+
* Main function.
71+
*/
72+
async function main() {
73+
try {
74+
// Read data from stdin (contains both translations and removed keys).
75+
const data = await readStdin();
76+
77+
if (!data) {
78+
console.error('No data provided');
79+
process.exit(1);
80+
}
81+
82+
const { translations = {}, removed = [] } = data;
83+
84+
console.error(`Received ${Object.keys(translations).length} translation(s) and ${removed.length} key(s) to remove`);
85+
86+
// Read existing Bulgarian translations.
87+
const bulgarianTranslations = readJsonFile('lang/main-bg.json');
88+
89+
// Remove deleted keys.
90+
let removedCount = 0;
91+
for (const key of removed) {
92+
if (key in bulgarianTranslations) {
93+
delete bulgarianTranslations[key];
94+
removedCount++;
95+
console.error(`✓ Removed "${key}"`);
96+
}
97+
}
98+
99+
// Merge new translations.
100+
let updatedCount = 0;
101+
for (const [key, value] of Object.entries(translations)) {
102+
bulgarianTranslations[key] = value;
103+
updatedCount++;
104+
console.error(`✓ Updated "${key}"`);
105+
}
106+
107+
// Write back to file (with alphabetical sorting).
108+
writeJsonFile('lang/main-bg.json', bulgarianTranslations);
109+
110+
console.error(`✅ Successfully merged ${updatedCount} translation(s) and removed ${removedCount} key(s) from lang/main-bg.json`);
111+
} catch (error) {
112+
console.error('Error:', error.message);
113+
process.exit(1);
114+
}
115+
}
116+
117+
main();
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
name: Auto-translate Bulgarian (Optimized)
2+
3+
on:
4+
push:
5+
branches: [master, auto-translate-bulgarian-automation]
6+
paths:
7+
- 'lang/main.json'
8+
9+
permissions:
10+
contents: write
11+
pull-requests: write
12+
13+
jobs:
14+
translate:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 2
22+
token: ${{ secrets.GITHUB_TOKEN }}
23+
24+
- name: Setup Node.js
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: '20'
28+
29+
- name: Install Claude Code CLI
30+
run: npm install -g @anthropic-ai/claude-code
31+
32+
- name: Extract changed keys
33+
id: extract
34+
run: |
35+
node .github/scripts/extract-changed-keys.js > changed-keys.json
36+
if [ ! -s changed-keys.json ]; then
37+
echo "No changes to translate"
38+
echo "has_changes=false" >> $GITHUB_OUTPUT
39+
exit 0
40+
fi
41+
42+
# Check if there are keys to translate
43+
ADDED_COUNT=$(node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync('changed-keys.json', 'utf-8')).added).length)")
44+
REMOVED_COUNT=$(node -e "console.log(JSON.parse(require('fs').readFileSync('changed-keys.json', 'utf-8')).removed.length)")
45+
46+
echo "has_changes=true" >> $GITHUB_OUTPUT
47+
echo "needs_translation=$( [ "$ADDED_COUNT" -gt 0 ] && echo 'true' || echo 'false' )" >> $GITHUB_OUTPUT
48+
49+
echo "Changes detected:"
50+
echo " - Keys to translate: $ADDED_COUNT"
51+
echo " - Keys to remove: $REMOVED_COUNT"
52+
cat changed-keys.json
53+
54+
- name: Translate with Claude
55+
if: steps.extract.outputs.needs_translation == 'true'
56+
run: |
57+
# Extract only the "added" keys that need translation
58+
KEYS_TO_TRANSLATE=$(node -e "
59+
const data = JSON.parse(require('fs').readFileSync('changed-keys.json', 'utf-8'));
60+
console.log(JSON.stringify(data.added, null, 2));
61+
")
62+
63+
echo "Keys to translate:"
64+
echo "$KEYS_TO_TRANSLATE"
65+
66+
claude -p "You are a professional translator specializing in Bulgarian translations for software interfaces.
67+
68+
I have some English strings from a video conferencing application (Jitsi Meet) that need to be translated to Bulgarian.
69+
70+
IMPORTANT: First, read the file lang/main-bg.json to understand the existing Bulgarian translation style, terminology, and consistency patterns.
71+
72+
Here are the English strings to translate (JSON format):
73+
$KEYS_TO_TRANSLATE
74+
75+
TRANSLATION GUIDELINES:
76+
- Use professional, natural Bulgarian language
77+
- Be consistent with the existing Bulgarian translations in lang/main-bg.json (terminology, formal/informal style, etc.)
78+
- Mixed Bulgarian and English is unacceptable. Everything must be translated to proper Bulgarian.
79+
- Exception: Established technical terms that are widely used in English (like 'WebAssembly', 'URL', 'API') can remain in English if they don't have good Bulgarian translations and are commonly used by Bulgarian speakers.
80+
- Example: 'phone number' MUST be translated to Bulgarian, but 'WebAssembly' can stay in English.
81+
- Preserve any placeholders like {{variable}} or {name} exactly as they appear.
82+
- Maintain formal/informal consistency with existing translations.
83+
84+
Please respond with ONLY a JSON object mapping each key to its Bulgarian translation. No explanations, no markdown, just the JSON object.
85+
" --output-format stream-json > claude-output.txt 2>&1
86+
87+
# Extract JSON from Claude's response
88+
cat claude-output.txt
89+
90+
# Try to extract the JSON object from the output
91+
node -e "
92+
const fs = require('fs');
93+
const output = fs.readFileSync('claude-output.txt', 'utf-8');
94+
95+
// Try to find JSON object in the output
96+
const jsonMatch = output.match(/\{[\s\S]*\}/);
97+
if (!jsonMatch) {
98+
console.error('Could not find JSON in Claude output');
99+
process.exit(1);
100+
}
101+
102+
const translations = JSON.parse(jsonMatch[0]);
103+
fs.writeFileSync('translations.json', JSON.stringify(translations, null, 2));
104+
" || (echo "Failed to extract translations from Claude output" && exit 1)
105+
106+
echo "Extracted translations:"
107+
cat translations.json
108+
env:
109+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
110+
111+
- name: Merge translations and remove deleted keys
112+
if: steps.extract.outputs.has_changes == 'true'
113+
run: |
114+
# Prepare data for merge script (translations + removed keys)
115+
node -e "
116+
const fs = require('fs');
117+
const changedKeys = JSON.parse(fs.readFileSync('changed-keys.json', 'utf-8'));
118+
119+
let translations = {};
120+
if (fs.existsSync('translations.json')) {
121+
translations = JSON.parse(fs.readFileSync('translations.json', 'utf-8'));
122+
}
123+
124+
const data = {
125+
translations: translations,
126+
removed: changedKeys.removed
127+
};
128+
129+
console.log(JSON.stringify(data, null, 2));
130+
" | node .github/scripts/merge-translations.js
131+
132+
- name: Check for translation changes
133+
id: check_changes
134+
run: |
135+
if git diff --quiet lang/main-bg.json; then
136+
echo "has_changes=false" >> $GITHUB_OUTPUT
137+
else
138+
echo "has_changes=true" >> $GITHUB_OUTPUT
139+
fi
140+
141+
- name: Create Pull Request
142+
if: steps.check_changes.outputs.has_changes == 'true'
143+
run: |
144+
BRANCH_NAME="auto-translate-bulgarian-$(date +%Y%m%d-%H%M%S)"
145+
146+
git config user.name "GitHub Actions Bot"
147+
git config user.email "actions@github.com"
148+
149+
git checkout -b "$BRANCH_NAME"
150+
git add lang/main-bg.json
151+
152+
# Count changes for commit message
153+
ADDED=$(git diff --cached lang/main-bg.json | grep -c '^+[^+]' || echo "0")
154+
REMOVED=$(git diff --cached lang/main-bg.json | grep -c '^-[^-]' || echo "0")
155+
156+
git commit -m "$(cat <<EOF
157+
feat(i18n): Auto-update Bulgarian translations
158+
159+
Automated Bulgarian translation update based on changes to main.json.
160+
- Added/Updated: $ADDED key(s)
161+
- Removed: $REMOVED key(s)
162+
163+
This PR was generated automatically by the auto-translate workflow.
164+
EOF
165+
)"
166+
167+
git push origin "$BRANCH_NAME"
168+
169+
gh pr create \
170+
--title "feat(i18n): Auto-update Bulgarian translations" \
171+
--body "$(cat <<'EOF'
172+
## Summary
173+
- Automated Bulgarian translation update based on changes to main.json
174+
- Translations generated using Claude AI for consistency and quality
175+
176+
## Test plan
177+
- [ ] Review translation quality and accuracy
178+
- [ ] Verify proper JSON formatting and indentation
179+
- [ ] Check that all new/modified keys from main.json are translated
180+
- [ ] Ensure consistency with existing Bulgarian translations
181+
EOF
182+
)" \
183+
--base master \
184+
--head "$BRANCH_NAME"
185+
env:
186+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)