Skip to content

docs: update translations for changed English sources (#371) #84

docs: update translations for changed English sources (#371)

docs: update translations for changed English sources (#371) #84

name: Translate Docs
on:
push:
branches: [main]
paths:
- 'docs/*.mdx'
- 'docs/**/*.mdx'
- 'README.md'
workflow_dispatch:
inputs:
force:
description: 'Ignore cache and re-translate everything'
type: boolean
default: false
languages:
description: 'Comma-separated language codes (leave empty for all)'
type: string
default: ''
concurrency:
group: translate-docs
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
languages: ${{ steps.langs.outputs.languages }}
steps:
- id: langs
run: |
if [ -n "${{ inputs.languages }}" ]; then
echo "languages=$(echo '${{ inputs.languages }}' | jq -Rc 'split(",") | map(gsub("\\s"; ""))')" >> "$GITHUB_OUTPUT"
else
echo 'languages=["zh","ja","ko","es","pt-br","de","fr","ru","hi","tr","vi","it","ar","he"]' >> "$GITHUB_OUTPUT"
fi
translate:
needs: prepare
runs-on: ubuntu-latest
strategy:
fail-fast: false
# Cap matrix fan-out so the LiteLLM proxy at ANTHROPIC_BASE_URL
# never sees the full 14-way burst. With 4 jobs x MAX_CONCURRENT=2
# = 8 simultaneous requests, well under the proxy's empirical
# connection-drop knee point (see scripts/translate-docs/cli.ts).
max-parallel: 4
matrix:
lang: ${{ fromJson(needs.prepare.outputs.languages) }}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_AUTH_TOKEN }}
ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }}
FAILPROOFAI_TELEMETRY_DISABLED: "1"
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/cache@v5
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
restore-keys: bun-${{ runner.os }}-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Restore translation cache
uses: actions/cache/restore@v5
with:
path: scripts/translate-docs/.translation-cache.json
key: translation-cache-${{ hashFiles('scripts/translate-docs/.translation-cache.json') }}
restore-keys: translation-cache-
- name: Translate ${{ matrix.lang }}
run: bun run translate --languages ${{ matrix.lang }} ${{ inputs.force == true && '--force' || '' }}
- name: Upload translated files
uses: actions/upload-artifact@v7
with:
name: translations-${{ matrix.lang }}
path: |
docs/${{ matrix.lang }}/
docs/i18n/README.${{ matrix.lang }}.md
retention-days: 1
if-no-files-found: warn
- name: Upload cache fragment
uses: actions/upload-artifact@v7
with:
name: cache-${{ matrix.lang }}
path: scripts/translate-docs/.translation-cache.json
retention-days: 1
if-no-files-found: warn
include-hidden-files: true
consolidate:
needs: [prepare, translate]
if: always() && needs.translate.result != 'cancelled'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
- name: Download all translations
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: translations-*
merge-multiple: true
path: docs
- name: Download cache fragments
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: cache-*
path: cache-fragments/
- name: Merge translation caches
run: |
node -e "
const fs = require('fs');
const path = require('path');
const dir = 'cache-fragments';
if (!fs.existsSync(dir)) { console.log('No cache fragments found, skipping merge'); process.exit(0); }
const merged = { sourceHash: '', lastUpdated: '', translations: {} };
let baseLoaded = false;
for (const sub of fs.readdirSync(dir).sort()) {
const f = path.join(dir, sub, '.translation-cache.json');
if (!fs.existsSync(f)) continue;
const data = JSON.parse(fs.readFileSync(f, 'utf-8'));
// Load base entries from the first fragment (all fragments share the same base)
if (!baseLoaded) {
Object.assign(merged.translations, data.translations);
baseLoaded = true;
}
// Each fragment is authoritative only for its own language.
// Extract lang from directory name: 'cache-zh' -> 'zh'
const lang = sub.replace('cache-', '');
const suffix = '::' + lang;
for (const [key, value] of Object.entries(data.translations)) {
if (key.endsWith(suffix)) {
merged.translations[key] = value;
}
}
if (data.lastUpdated > merged.lastUpdated) {
merged.lastUpdated = data.lastUpdated;
}
}
fs.mkdirSync('scripts/translate-docs', { recursive: true });
fs.writeFileSync(
'scripts/translate-docs/.translation-cache.json',
JSON.stringify(merged, null, 2)
);
console.log('Merged ' + Object.keys(merged.translations).length + ' cache entries');
"
- name: Save translation cache
uses: actions/cache/save@v5
with:
path: scripts/translate-docs/.translation-cache.json
key: translation-cache-${{ hashFiles('scripts/translate-docs/.translation-cache.json') }}
- name: Check for changes
id: changes
run: |
git add -A
if git diff --cached --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Create or update PR with translations
if: steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# When an auto-translation PR is already open, push to its branch
# instead of dropping these translations. Previously this step
# exited early if any open PR matched, which combined with the
# cache-save above meant lost translations were never re-attempted
# (cache marked them done, file changes were never committed).
EXISTING_PR=$(gh pr list --base main --search "[auto] update translations" --state open --json number,headRefName --limit 1)
EXISTING_COUNT=$(echo "$EXISTING_PR" | jq length)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if [ "$EXISTING_COUNT" -gt 0 ]; then
EXISTING_BRANCH=$(echo "$EXISTING_PR" | jq -r '.[0].headRefName')
EXISTING_NUMBER=$(echo "$EXISTING_PR" | jq -r '.[0].number')
echo "Updating existing PR #$EXISTING_NUMBER on branch $EXISTING_BRANCH"
# Snapshot freshly-generated translations and the merged cache,
# then switch branches and replay them on top so newer English
# source wins over whatever the existing PR branch had.
git reset HEAD
tar -czf /tmp/translations.tar.gz docs/ scripts/translate-docs/.translation-cache.json
git fetch origin "$EXISTING_BRANCH"
git checkout -f -B "$EXISTING_BRANCH" "origin/$EXISTING_BRANCH"
tar -xzf /tmp/translations.tar.gz
rm /tmp/translations.tar.gz
BRANCH="$EXISTING_BRANCH"
else
BRANCH="auto/translate-docs-$(date +%Y%m%d-%H%M)"
git checkout -b "$BRANCH"
fi
git add -A
if git diff --cached --quiet; then
echo "No new translation changes to push. Done."
exit 0
fi
git commit -m "docs: update translations for changed English sources"
git push origin "$BRANCH"
if [ "$EXISTING_COUNT" -eq 0 ]; then
PR_BODY="## Summary
Automated translation update triggered by changes to English documentation sources.
- Only changed pages were re-translated (content-hash cache)
- All 14 languages across 3 tiers (parallel matrix)"
gh pr create \
--title "[auto] update translations" \
--body "$PR_BODY" \
--base main \
--head "$BRANCH"
fi