Skip to content

Commit ee39e44

Browse files
authored
Internationalization (#459)
1 parent d6b7290 commit ee39e44

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2286
-3445
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
name: 📖 Translate Documentation
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
target_locale:
7+
description: "Target locale to translate"
8+
required: false
9+
default: "all"
10+
type: choice
11+
options:
12+
- "all"
13+
- "es"
14+
- "pt-BR"
15+
16+
force_translate:
17+
description: "Force translate all files (ignore cache)"
18+
required: false
19+
default: false
20+
type: boolean
21+
22+
cleanup_deleted:
23+
description: "Delete translated files when English originals are removed"
24+
required: false
25+
default: true
26+
type: boolean
27+
28+
dry_run:
29+
description: "Dry run (don't write files or update cache)"
30+
required: false
31+
default: false
32+
type: boolean
33+
34+
single_file:
35+
description: "Single file to translate (relative to app/en, e.g. 'home/page.mdx')"
36+
required: false
37+
default: ""
38+
type: string
39+
40+
concurrency:
41+
description: "Number of files to translate in parallel (1-10)"
42+
required: false
43+
default: 3
44+
type: number
45+
46+
jobs:
47+
translate:
48+
runs-on: ubuntu-latest
49+
50+
steps:
51+
- name: 🛒 Checkout repository
52+
uses: actions/checkout@v4
53+
with:
54+
fetch-depth: 0
55+
token: ${{ secrets.GITHUB_TOKEN }}
56+
57+
- name: 📦 Setup Node.js
58+
uses: actions/setup-node@v4
59+
with:
60+
node-version: "20"
61+
cache: "pnpm"
62+
63+
- name: 📥 Install pnpm
64+
uses: pnpm/action-setup@v4
65+
with:
66+
version: 9
67+
68+
- name: 📚 Install dependencies
69+
run: pnpm install --frozen-lockfile
70+
71+
- name: 🧹 Clean up deleted files
72+
if: inputs.cleanup_deleted
73+
run: |
74+
echo "🔍 Checking for deleted English files..."
75+
pnpm dlx tsx scripts/i18n-sync/index.ts --cleanup
76+
env:
77+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
78+
79+
- name: 🌍 Run translation
80+
run: |
81+
echo "🚀 Starting translation process..."
82+
83+
# Build the command with dynamic inputs
84+
CMD="pnpm dlx tsx scripts/i18n-sync/index.ts"
85+
86+
# Add target locale if not 'all'
87+
if [ "${{ inputs.target_locale }}" != "all" ]; then
88+
CMD="$CMD --locale ${{ inputs.target_locale }}"
89+
fi
90+
91+
# Add force flag if enabled
92+
if [ "${{ inputs.force_translate }}" = "true" ]; then
93+
CMD="$CMD --force"
94+
fi
95+
96+
# Add dry-run flag if enabled
97+
if [ "${{ inputs.dry_run }}" = "true" ]; then
98+
CMD="$CMD --dry-run"
99+
fi
100+
101+
# Add single file if specified
102+
if [ -n "${{ inputs.single_file }}" ]; then
103+
CMD="$CMD --file '${{ inputs.single_file }}'"
104+
fi
105+
106+
# Add concurrency
107+
CMD="$CMD --concurrency ${{ inputs.concurrency }}"
108+
109+
echo "💻 Executing: $CMD"
110+
eval $CMD
111+
env:
112+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
113+
OPENAI_MODEL: ${{ secrets.OPENAI_MODEL || 'gpt-4o-mini' }}
114+
115+
- name: 📊 Check translation status
116+
id: check_changes
117+
run: |
118+
if git diff --quiet && git diff --cached --quiet; then
119+
echo "No changes detected"
120+
echo "has_changes=false" >> $GITHUB_OUTPUT
121+
else
122+
echo "Changes detected"
123+
echo "has_changes=true" >> $GITHUB_OUTPUT
124+
125+
# Count changed files
126+
CHANGED_FILES=$(git diff --name-only | wc -l)
127+
STAGED_FILES=$(git diff --cached --name-only | wc -l)
128+
TOTAL_CHANGES=$((CHANGED_FILES + STAGED_FILES))
129+
130+
echo "changed_files=$TOTAL_CHANGES" >> $GITHUB_OUTPUT
131+
132+
# Get list of changed locales
133+
CHANGED_LOCALES=$(git diff --name-only | grep -E '^app/(es|pt-BR)/' | cut -d'/' -f2 | sort -u | tr '\n' ',' | sed 's/,$//')
134+
if [ -z "$CHANGED_LOCALES" ]; then
135+
CHANGED_LOCALES=$(git diff --cached --name-only | grep -E '^app/(es|pt-BR)/' | cut -d'/' -f2 | sort -u | tr '\n' ',' | sed 's/,$//')
136+
fi
137+
echo "changed_locales=$CHANGED_LOCALES" >> $GITHUB_OUTPUT
138+
fi
139+
140+
- name: 🏷️ Generate branch name
141+
if: steps.check_changes.outputs.has_changes == 'true'
142+
id: branch_name
143+
run: |
144+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
145+
if [ "${{ inputs.target_locale }}" != "all" ]; then
146+
BRANCH_NAME="translations/${{ inputs.target_locale }}-$TIMESTAMP"
147+
else
148+
BRANCH_NAME="translations/all-locales-$TIMESTAMP"
149+
fi
150+
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
151+
152+
- name: 🌿 Create and switch to new branch
153+
if: steps.check_changes.outputs.has_changes == 'true'
154+
run: |
155+
git config user.name "github-actions[bot]"
156+
git config user.email "github-actions[bot]@users.noreply.github.com"
157+
git checkout -b ${{ steps.branch_name.outputs.branch_name }}
158+
159+
- name: 📝 Commit changes
160+
if: steps.check_changes.outputs.has_changes == 'true'
161+
run: |
162+
git add .
163+
164+
# Create detailed commit message
165+
COMMIT_MSG="🌍 Update translations"
166+
167+
if [ "${{ inputs.target_locale }}" != "all" ]; then
168+
COMMIT_MSG="$COMMIT_MSG for ${{ inputs.target_locale }}"
169+
fi
170+
171+
COMMIT_MSG="$COMMIT_MSG (${{ steps.check_changes.outputs.changed_files }} files)"
172+
173+
if [ "${{ inputs.force_translate }}" = "true" ]; then
174+
COMMIT_MSG="$COMMIT_MSG [forced]"
175+
fi
176+
177+
if [ "${{ inputs.cleanup_deleted }}" = "true" ]; then
178+
COMMIT_MSG="$COMMIT_MSG [with cleanup]"
179+
fi
180+
181+
if [ "${{ inputs.dry_run }}" = "true" ]; then
182+
COMMIT_MSG="$COMMIT_MSG [dry-run]"
183+
fi
184+
185+
# Add configuration details to commit body
186+
COMMIT_BODY="Translation Configuration:
187+
- Target Locale: ${{ inputs.target_locale }}
188+
- Force Translate: ${{ inputs.force_translate }}
189+
- Cleanup Deleted: ${{ inputs.cleanup_deleted }}
190+
- Dry Run: ${{ inputs.dry_run }}
191+
- Single File: ${{ inputs.single_file || 'none' }}
192+
- Concurrency: ${{ inputs.concurrency }}
193+
- Changed Locales: ${{ steps.check_changes.outputs.changed_locales }}
194+
195+
Generated by GitHub Actions workflow"
196+
197+
git commit -m "$COMMIT_MSG" -m "$COMMIT_BODY"
198+
199+
- name: 📤 Push branch
200+
if: steps.check_changes.outputs.has_changes == 'true'
201+
run: |
202+
git push origin ${{ steps.branch_name.outputs.branch_name }}
203+
204+
- name: 🔧 Create Pull Request
205+
if: steps.check_changes.outputs.has_changes == 'true'
206+
uses: actions/github-script@v7
207+
with:
208+
script: |
209+
const { data: pr } = await github.rest.pulls.create({
210+
owner: context.repo.owner,
211+
repo: context.repo.repo,
212+
title: `🌍 Translation Update: ${{ inputs.target_locale }} (${{ steps.check_changes.outputs.changed_files }} files)`,
213+
head: '${{ steps.branch_name.outputs.branch_name }}',
214+
base: 'main',
215+
body: `## 📖 Automated Translation Update
216+
217+
This PR contains automated translations generated by the GitHub Actions workflow.
218+
219+
### 📊 Translation Summary
220+
- **Target Locale**: ${{ inputs.target_locale }}
221+
- **Files Changed**: ${{ steps.check_changes.outputs.changed_files }}
222+
- **Locales Updated**: ${{ steps.check_changes.outputs.changed_locales }}
223+
224+
### ⚙️ Configuration Used
225+
- **Force Translate**: ${{ inputs.force_translate }}
226+
- **Cleanup Deleted Files**: ${{ inputs.cleanup_deleted }}
227+
- **Dry Run**: ${{ inputs.dry_run }}
228+
- **Single File**: ${{ inputs.single_file || 'None (all files)' }}
229+
- **Concurrency**: ${{ inputs.concurrency }}
230+
- **Model**: gpt-4o-mini
231+
232+
### 🔍 Review Guidelines
233+
Please review the translations for:
234+
- [ ] Accuracy and context preservation
235+
- [ ] Proper handling of technical terms
236+
- [ ] UI/Dashboard elements remain in English
237+
- [ ] Code blocks and inline code unchanged
238+
- [ ] Markdown formatting preserved
239+
- [ ] Brand names (Arcade, Arcade Engine, Control Plane) kept in English
240+
241+
### 🚀 Auto-generated
242+
This PR was automatically created by the \`translate-docs.yml\` GitHub Action.
243+
244+
**Triggered by**: @${{ github.actor }}
245+
**Workflow Run**: [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`,
246+
draft: false
247+
});
248+
249+
// Add labels
250+
await github.rest.issues.addLabels({
251+
owner: context.repo.owner,
252+
repo: context.repo.repo,
253+
issue_number: pr.number,
254+
labels: ['🌍 translation', '🤖 automated', 'documentation']
255+
});
256+
257+
// Add specific locale label if not 'all'
258+
if ('${{ inputs.target_locale }}' !== 'all') {
259+
await github.rest.issues.addLabels({
260+
owner: context.repo.owner,
261+
repo: context.repo.repo,
262+
issue_number: pr.number,
263+
labels: [`locale:${{ inputs.target_locale }}`]
264+
});
265+
}
266+
267+
console.log(`Created PR #${pr.number}: ${pr.html_url}`);
268+
269+
- name: 📄 Summary
270+
if: always()
271+
run: |
272+
echo "## 🌍 Translation Workflow Summary" >> $GITHUB_STEP_SUMMARY
273+
echo "" >> $GITHUB_STEP_SUMMARY
274+
275+
if [ "${{ steps.check_changes.outputs.has_changes }}" = "true" ]; then
276+
echo "✅ **Translation completed successfully!**" >> $GITHUB_STEP_SUMMARY
277+
echo "" >> $GITHUB_STEP_SUMMARY
278+
echo "- **Files changed**: ${{ steps.check_changes.outputs.changed_files }}" >> $GITHUB_STEP_SUMMARY
279+
echo "- **Target locale**: ${{ inputs.target_locale }}" >> $GITHUB_STEP_SUMMARY
280+
echo "- **Branch created**: \`${{ steps.branch_name.outputs.branch_name }}\`" >> $GITHUB_STEP_SUMMARY
281+
echo "- **Changed locales**: ${{ steps.check_changes.outputs.changed_locales }}" >> $GITHUB_STEP_SUMMARY
282+
echo "" >> $GITHUB_STEP_SUMMARY
283+
echo "A Pull Request has been created for review." >> $GITHUB_STEP_SUMMARY
284+
else
285+
echo "ℹ️ **No changes detected**" >> $GITHUB_STEP_SUMMARY
286+
echo "" >> $GITHUB_STEP_SUMMARY
287+
echo "All translations are up to date. No PR was created." >> $GITHUB_STEP_SUMMARY
288+
fi
289+
290+
echo "" >> $GITHUB_STEP_SUMMARY
291+
echo "### Configuration Used" >> $GITHUB_STEP_SUMMARY
292+
echo "- Target Locale: ${{ inputs.target_locale }}" >> $GITHUB_STEP_SUMMARY
293+
echo "- Force Translate: ${{ inputs.force_translate }}" >> $GITHUB_STEP_SUMMARY
294+
echo "- Cleanup Deleted: ${{ inputs.cleanup_deleted }}" >> $GITHUB_STEP_SUMMARY
295+
echo "- Dry Run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY
296+
echo "- Single File: ${{ inputs.single_file || 'None' }}" >> $GITHUB_STEP_SUMMARY
297+
echo "- Concurrency: ${{ inputs.concurrency }}" >> $GITHUB_STEP_SUMMARY
298+
echo "- Model: gpt-4o-mini" >> $GITHUB_STEP_SUMMARY

.i18n-cache/hashes.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
2-
"es": {
3-
"_meta.ts": "bfff435958824a340b2f6f78af9edaa62bab68b2e0db4c7980c37fb29a170a1f",
4-
"page.mdx": "9489dd67ec525393d4cb3864edb26db843f20c57544b812b8b98637706cea666"
2+
"_metadata": {
3+
"version": "1.0.0",
4+
"created": "2025-09-30T08:04:53.155Z",
5+
"updated": "2025-09-30T14:53:21.378Z"
56
},
6-
"pt-BR": {
7-
"_meta.ts": "bfff435958824a340b2f6f78af9edaa62bab68b2e0db4c7980c37fb29a170a1f",
8-
"page.mdx": "9489dd67ec525393d4cb3864edb26db843f20c57544b812b8b98637706cea666"
7+
"data": {
8+
"es": {},
9+
"pt-BR": {}
910
}
1011
}

_dictionaries/en.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,24 @@ export default {
1111
poweredBy: "Powered by",
1212
editPage: "Edit this page on GitHub →",
1313
by: "by",
14+
banner: {
15+
aiTranslation:
16+
"🤖 This translation is in progress and was generated by AI. If you find something incorrect or want to help improve it, please",
17+
contributeLink: "contribute on GitHub",
18+
thanks: "Thank you for your help!",
19+
},
20+
notFoundPage: {
21+
title: "Page not found",
22+
description: "This page doesn't exist or may have moved.",
23+
notAvailablePrefix: "Not available in",
24+
tryEnglish: "Try English version",
25+
translationHint: "This page may not be translated yet.",
26+
viewOriginalEnglish: "View the original in English",
27+
goHome: "Go to homepage",
28+
goBack: "Go back",
29+
needHelp: "Need help? Try these popular pages:",
30+
quickstart: "Quickstart",
31+
toolkits: "Toolkits",
32+
createToolkit: "Create a toolkit",
33+
},
1434
};

_dictionaries/es.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,28 @@ export default {
99
backToTop: "Desplazarse hacia arriba",
1010
},
1111
lastUpdated: "Última actualización el",
12-
notFound: "Esta pagina no se pudo encontrar",
12+
notFound: "Esta página no se pudo encontrar",
1313
poweredBy: "Desarrollado por",
1414
editPage: "Edite esta página en GitHub →",
1515
by: "por",
16+
banner: {
17+
aiTranslation:
18+
"🤖 Esta traducción está en progreso y fue generada por IA. Si encuentras algo incorrecto o quieres ayudar a mejorarla, por favor",
19+
contributeLink: "contribuye en GitHub",
20+
thanks: "¡Gracias por tu ayuda!",
21+
},
22+
notFoundPage: {
23+
title: "Página no encontrada",
24+
description: "Esta página no existe o puede haber sido movida.",
25+
notAvailablePrefix: "No disponible en",
26+
tryEnglish: "Probar versión en inglés",
27+
translationHint: "Es posible que esta página aún no esté traducida.",
28+
viewOriginalEnglish: "Ver el original en inglés",
29+
goHome: "Ir a la página principal",
30+
goBack: "Volver",
31+
needHelp: "¿Necesitas ayuda? Prueba estas páginas populares:",
32+
quickstart: "Inicio rápido",
33+
toolkits: "Toolkits",
34+
createToolkit: "Crear un toolkit",
35+
},
1636
} satisfies Dictionary;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Dictionaries, Dictionary, Locale } from "./i18n-config";
2+
3+
// Client-safe dictionary loader (without server-only restriction)
4+
const dictionaries: Dictionaries = {
5+
en: () => import("./en"),
6+
es: () => import("./es"),
7+
"pt-BR": () => import("./pt-BR"),
8+
};
9+
10+
export async function getDictionaryClient(locale: string): Promise<Dictionary> {
11+
const localeKey: Locale = (Object.keys(dictionaries) as Locale[]).includes(
12+
locale as Locale
13+
)
14+
? (locale as Locale)
15+
: "en";
16+
const { default: dictionary } = await dictionaries[localeKey]();
17+
18+
return dictionary;
19+
}

0 commit comments

Comments
 (0)