From 44ca093122d27b00e05b94533ac458c825606f98 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:31:05 +0000 Subject: [PATCH 1/4] ci: add workflow to auto-remove stale zh translations when EN changes When English source MDX files are updated, any corresponding zh translation files become stale and will reference content that may no longer exist (broken images, removed components, etc.). This workflow triggers on push to main when fern/products/**/*.mdx files change, detects zh translation files for the changed EN pages, and removes them. Fern's localization system then falls back to auto-translating from the current EN source for those pages. This prevents the broken links issue that PR #5515 fixed, where stale zh translations referenced images/components that EN had since replaced. --- .github/workflows/sync-translations.yml | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/sync-translations.yml diff --git a/.github/workflows/sync-translations.yml b/.github/workflows/sync-translations.yml new file mode 100644 index 0000000000..83aa8d9455 --- /dev/null +++ b/.github/workflows/sync-translations.yml @@ -0,0 +1,67 @@ +name: Sync Translations + +on: + push: + branches: + - main + paths: + - 'fern/products/**/*.mdx' + workflow_dispatch: + +permissions: + contents: write + +jobs: + sync: + name: Remove stale translations + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + fetch-depth: 2 + + - name: Find and remove stale translation files + run: | + set -euo pipefail + + # Get EN MDX files changed in this push + CHANGED_EN_FILES=$(git diff --name-only HEAD~1 HEAD -- 'fern/products/**/*.mdx' || true) + + if [ -z "$CHANGED_EN_FILES" ]; then + echo "No EN MDX files changed, nothing to do" + exit 0 + fi + + STALE_FILES="" + while IFS= read -r en_file; do + [ -z "$en_file" ] && continue + # Map EN path to zh translation path: + # fern/products/docs/pages/foo.mdx -> fern/translations/zh/products/docs/pages/foo.mdx + zh_file="fern/translations/zh/${en_file#fern/}" + if [ -f "$zh_file" ]; then + echo "Stale: $zh_file (EN source changed: $en_file)" + STALE_FILES="$STALE_FILES $zh_file" + fi + done <<< "$CHANGED_EN_FILES" + + if [ -z "$STALE_FILES" ]; then + echo "No stale translation files found" + exit 0 + fi + + # shellcheck disable=SC2086 + git rm $STALE_FILES + echo "stale_found=true" >> "$GITHUB_ENV" + + - name: Commit and push + if: env.stale_found == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "chore: remove stale zh translations for updated EN pages + + Automatically removes zh translation files whose EN source was + updated. These pages will fall back to auto-translation from the + current EN source until new translations are provided." + git push From a27f78b3f2679a3610db09f76ea14910add0d493 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 12:29:38 +0000 Subject: [PATCH 2/4] ci: update workflow to re-translate stale zh files instead of deleting Uses Anthropic Claude API to re-translate stale zh translation files when EN source changes, rather than deleting them. This preserves pre-computed translations and avoids the cost of on-the-fly translation at build/serve time. Requires ANTHROPIC_API_KEY to be set as a GitHub Actions secret. --- .github/workflows/sync-translations.yml | 98 +++++++++++++++++++++---- 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/.github/workflows/sync-translations.yml b/.github/workflows/sync-translations.yml index 83aa8d9455..7f0940a35b 100644 --- a/.github/workflows/sync-translations.yml +++ b/.github/workflows/sync-translations.yml @@ -13,7 +13,7 @@ permissions: jobs: sync: - name: Remove stale translations + name: Update stale translations runs-on: ubuntu-latest steps: - name: Checkout repository @@ -21,7 +21,17 @@ jobs: with: fetch-depth: 2 - - name: Find and remove stale translation files + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: pip install anthropic + + - name: Find and re-translate stale translation files + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} run: | set -euo pipefail @@ -33,35 +43,95 @@ jobs: exit 0 fi - STALE_FILES="" + export STALE_EN_FILES="" while IFS= read -r en_file; do [ -z "$en_file" ] && continue - # Map EN path to zh translation path: - # fern/products/docs/pages/foo.mdx -> fern/translations/zh/products/docs/pages/foo.mdx zh_file="fern/translations/zh/${en_file#fern/}" if [ -f "$zh_file" ]; then echo "Stale: $zh_file (EN source changed: $en_file)" - STALE_FILES="$STALE_FILES $zh_file" + STALE_EN_FILES="$STALE_EN_FILES $en_file" fi done <<< "$CHANGED_EN_FILES" + export STALE_EN_FILES - if [ -z "$STALE_FILES" ]; then + if [ -z "$STALE_EN_FILES" ]; then echo "No stale translation files found" exit 0 fi - # shellcheck disable=SC2086 - git rm $STALE_FILES - echo "stale_found=true" >> "$GITHUB_ENV" + python3 << 'PYEOF' + import os, sys, time + import anthropic + + SYSTEM_PROMPT = """You are translating Fern developer documentation from English to Simplified Chinese (zh). + + Rules: + 1. Translate ALL prose, headings, frontmatter (title, description, sidebar-title, headline), callout text, and step titles to Chinese. + 2. DO NOT translate: + - Code blocks (content inside ``` fences) + - Component tag names (, , , , , , , etc.) + - Component prop names and values (e.g. title="...", href="...", src="...", language="...") + - URLs, file paths, import paths + - Variable names, API endpoints, CLI commands, package names + - YAML/JSON keys in code blocks + - includes + - and tags (keep them as-is) + - Content inside blocks (keep in English as it's for AI agents) + 3. Translate component prop values ONLY when they contain human-readable display text: + - Translate: title="Getting started" → title="开始使用" + - Do NOT translate: href="/learn/docs/...", src="./image.png", language="python" + 4. Keep the same MDX structure, whitespace, and formatting as the original. + 5. Keep frontmatter YAML structure exactly the same (same keys, just translate values). + 6. For technical terms commonly kept in English in Chinese tech docs, keep them in English or use the standard Chinese translation with the English term in parentheses on first use. Examples: + - SDK, API, CLI, MDX, YAML, JSON, OpenAPI, gRPC → keep in English + - endpoint → 端点 or keep as endpoint + - middleware → 中间件 + 7. Output ONLY the translated MDX content. No explanations, no markdown fences around the output.""" + + client = anthropic.Anthropic() + stale = os.environ.get("STALE_EN_FILES", "").split() + updated = 0 + + for en_file in stale: + zh_file = "fern/translations/zh/" + en_file.removeprefix("fern/") + with open(en_file) as f: + en_content = f.read() + + print(f"Translating: {en_file} -> {zh_file}") + try: + resp = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=16000, + system=SYSTEM_PROMPT, + messages=[{"role": "user", "content": f"Translate this MDX documentation page to Simplified Chinese:\n\n{en_content}"}], + ) + zh_content = resp.content[0].text + + os.makedirs(os.path.dirname(zh_file), exist_ok=True) + with open(zh_file, "w") as f: + f.write(zh_content) + if not zh_content.endswith("\n"): + f.write("\n") + updated += 1 + time.sleep(1) + except Exception as e: + print(f" ERROR translating {en_file}: {e}", file=sys.stderr) + + print(f"Updated {updated}/{len(stale)} translation files") + if updated > 0: + with open(os.environ["GITHUB_ENV"], "a") as f: + f.write("stale_found=true\n") + PYEOF - name: Commit and push if: env.stale_found == 'true' run: | + git add fern/translations/zh/ git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "chore: remove stale zh translations for updated EN pages + git commit -m "chore: re-translate stale zh translations for updated EN pages - Automatically removes zh translation files whose EN source was - updated. These pages will fall back to auto-translation from the - current EN source until new translations are provided." + Automatically re-translates zh translation files whose EN source was + updated, keeping pre-computed translations in sync with the latest + English content." git push From beaee195bf6e57dfa31d07ee9052e7f5a17fd607 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 12:31:57 +0000 Subject: [PATCH 3/4] ci: fix zizmor findings - pin setup-python hash, add persist-credentials: false Addresses two zizmor security findings: 1. Pin actions/setup-python to commit hash (unpinned action reference) 2. Add persist-credentials: false to checkout (credential persistence) 3. Use explicit GITHUB_TOKEN for push step instead of persisted credentials --- .github/workflows/sync-translations.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sync-translations.yml b/.github/workflows/sync-translations.yml index 7f0940a35b..7690767450 100644 --- a/.github/workflows/sync-translations.yml +++ b/.github/workflows/sync-translations.yml @@ -20,9 +20,10 @@ jobs: uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: fetch-depth: 2 + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.12' @@ -125,6 +126,8 @@ jobs: - name: Commit and push if: env.stale_found == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git add fern/translations/zh/ git config user.name "github-actions[bot]" @@ -134,4 +137,5 @@ jobs: Automatically re-translates zh translation files whose EN source was updated, keeping pre-computed translations in sync with the latest English content." + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" git push From a5171c7c26bb5a044e75b15d620d45143c79e9bd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 12:35:02 +0000 Subject: [PATCH 4/4] ci: switch translation sync to nightly schedule Runs at 6 AM UTC daily instead of on every push to main. Uses git commit timestamps to detect stale zh translations (EN source has a newer commit than the zh file) rather than comparing HEAD~1. Full repo history (fetch-depth: 0) is needed for timestamp comparison. --- .github/workflows/sync-translations.yml | 34 +++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/.github/workflows/sync-translations.yml b/.github/workflows/sync-translations.yml index 7690767450..fab1094f23 100644 --- a/.github/workflows/sync-translations.yml +++ b/.github/workflows/sync-translations.yml @@ -1,11 +1,9 @@ name: Sync Translations on: - push: - branches: - - main - paths: - - 'fern/products/**/*.mdx' + # Run nightly at 6 AM UTC + schedule: + - cron: '0 6 * * *' workflow_dispatch: permissions: @@ -19,7 +17,7 @@ jobs: - name: Checkout repository uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: - fetch-depth: 2 + fetch-depth: 0 persist-credentials: false - name: Set up Python @@ -36,23 +34,21 @@ jobs: run: | set -euo pipefail - # Get EN MDX files changed in this push - CHANGED_EN_FILES=$(git diff --name-only HEAD~1 HEAD -- 'fern/products/**/*.mdx' || true) + # For each zh translation file, check if its EN source has a newer + # commit than the zh file. If so, the translation is stale. + export STALE_EN_FILES="" + for zh_file in $(find fern/translations/zh/products -name '*.mdx' 2>/dev/null); do + en_file="fern/${zh_file#fern/translations/zh/}" + [ -f "$en_file" ] || continue - if [ -z "$CHANGED_EN_FILES" ]; then - echo "No EN MDX files changed, nothing to do" - exit 0 - fi + en_date=$(git log -1 --format=%ct -- "$en_file" 2>/dev/null || echo 0) + zh_date=$(git log -1 --format=%ct -- "$zh_file" 2>/dev/null || echo 0) - export STALE_EN_FILES="" - while IFS= read -r en_file; do - [ -z "$en_file" ] && continue - zh_file="fern/translations/zh/${en_file#fern/}" - if [ -f "$zh_file" ]; then - echo "Stale: $zh_file (EN source changed: $en_file)" + if [ "$en_date" -gt "$zh_date" ]; then + echo "Stale: $zh_file (EN updated $(date -d @"$en_date" -u +%Y-%m-%d), zh last updated $(date -d @"$zh_date" -u +%Y-%m-%d))" STALE_EN_FILES="$STALE_EN_FILES $en_file" fi - done <<< "$CHANGED_EN_FILES" + done export STALE_EN_FILES if [ -z "$STALE_EN_FILES" ]; then