Repository navigation
Translate Docs #296
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Translate Docs | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'content/docs/**' | |
| # Exclude generated translations of any locale. A `*.<locale>.mdx` or | |
| # `meta.<locale>.json` file is bot output, never a translation source, so it | |
| # must not retrigger this workflow. Keep both two-letter and regional locale | |
| # patterns here, matching TARGET_LOCALES in lib/i18n/config.ts. | |
| - '!content/docs/**/*.[a-z][a-z].mdx' | |
| - '!content/docs/**/*.[a-z][a-z]-[A-Z][A-Z].mdx' | |
| - '!content/docs/**/meta.[a-z][a-z].json' | |
| - '!content/docs/**/meta.[a-z][a-z]-[A-Z][A-Z].json' | |
| # Convergence sweep. A push only translates after a content change, and adding a | |
| # new language doesn't touch content/docs at all — so a big batch (several new | |
| # locales and/or long files) can't fully land in the single push-triggered run | |
| # under the flex-tier rate limit. This half-hourly run writes generated docs to | |
| # TRANSLATE_BRANCH and keeps translation memory in the Actions cache, so main stays | |
| # free of partial progress commits and cache files. | |
| schedule: | |
| - cron: '*/30 * * * *' | |
| workflow_dispatch: | |
| inputs: | |
| force: | |
| description: 'Retranslate everything (ignore the cache)' | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| env: | |
| TRANSLATE_BRANCH: automation/docs-translations | |
| concurrency: | |
| group: translate-docs | |
| cancel-in-progress: false | |
| jobs: | |
| translate: | |
| runs-on: ubuntu-latest | |
| environment: Production | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # Full history so the long-lived translation branch can merge current | |
| # main before each convergence pass. | |
| fetch-depth: 0 | |
| - name: Prepare translation branch | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git fetch origin "+refs/heads/main:refs/remotes/origin/main" | |
| if git fetch origin "+refs/heads/${TRANSLATE_BRANCH}:refs/remotes/origin/${TRANSLATE_BRANCH}" 2>/dev/null; then | |
| git switch -c "$TRANSLATE_BRANCH" "origin/$TRANSLATE_BRANCH" | |
| git merge --no-edit origin/main | |
| else | |
| git switch -c "$TRANSLATE_BRANCH" origin/main | |
| fi | |
| - name: Restore translation memory | |
| uses: actions/cache@v4 | |
| with: | |
| path: content/.i18n-cache | |
| key: translate-docs-${{ github.run_id }} | |
| restore-keys: | | |
| translate-docs- | |
| - name: Seed translation memory from git history | |
| run: | | |
| if [ -d content/.i18n-cache ]; then | |
| exit 0 | |
| fi | |
| # First run after removing cache files from main: seed from the parent of | |
| # the cache-removal commit, then unstage it so the files stay out of git. | |
| seed_commit=$(git rev-list -n 1 HEAD -- content/.i18n-cache || true) | |
| if [ -n "$seed_commit" ]; then | |
| git checkout "${seed_commit}^" -- content/.i18n-cache || true | |
| git reset -- content/.i18n-cache || true | |
| fi | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - name: Translate docs | |
| env: | |
| OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} | |
| OPENROUTER_TRANSLATE_MODEL: ${{ secrets.OPENROUTER_TRANSLATE_MODEL }} | |
| run: | | |
| FLAGS="" | |
| if [ "${{ inputs.force }}" = "true" ]; then | |
| FLAGS="--force" | |
| fi | |
| npx tsx scripts/translate.ts $FLAGS | |
| - name: Commit translations to branch | |
| run: | | |
| git add content/docs | |
| if git diff --cached --quiet; then | |
| echo "No translation changes to commit" | |
| else | |
| # [skip ci] keeps the long-lived translation branch from running CI on | |
| # every half-hourly convergence commit. Main only changes when this branch | |
| # is intentionally merged. | |
| git commit --no-verify -m "chore: update docs translations [skip ci]" | |
| fi | |
| if git show-ref --verify --quiet "refs/remotes/origin/${TRANSLATE_BRANCH}"; then | |
| base="origin/${TRANSLATE_BRANCH}" | |
| else | |
| base="origin/main" | |
| fi | |
| ahead=$(git rev-list --count "${base}..HEAD") | |
| if [ "$ahead" -eq 0 ]; then | |
| echo "Translation branch already up to date" | |
| exit 0 | |
| fi | |
| pushed=false | |
| for attempt in 1 2 3 4 5; do | |
| if git push origin HEAD:"$TRANSLATE_BRANCH"; then | |
| pushed=true | |
| break | |
| fi | |
| echo "push attempt $attempt rejected (remote advanced); merging latest branch/main" | |
| git fetch origin "+refs/heads/main:refs/remotes/origin/main" | |
| git fetch origin "+refs/heads/${TRANSLATE_BRANCH}:refs/remotes/origin/${TRANSLATE_BRANCH}" || true | |
| if git show-ref --verify --quiet "refs/remotes/origin/${TRANSLATE_BRANCH}"; then | |
| git merge --no-edit "origin/${TRANSLATE_BRANCH}" | |
| fi | |
| git merge --no-edit origin/main | |
| sleep 5 | |
| done | |
| if [ "$pushed" != "true" ]; then | |
| echo "::error::failed to push translations to $TRANSLATE_BRANCH after 5 attempts" | |
| exit 1 | |
| fi |