Translate #56
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 | |
| on: | |
| # Translations are triggered manually by maintainers, typically ahead of a new release. | |
| # Automatic: when English content or prompts change | |
| # [EDIT] Commented out to save API costs - run manually as needed | |
| # push: | |
| # branches: | |
| # - master | |
| # paths: | |
| # - "docs/en/docs/**/*.md" | |
| # - "docs/*/llm-prompt.md" | |
| # - "_scripts/general-llm-prompt.md" | |
| # Manual trigger | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: >- | |
| normal: incrementally update translations based on English changes since the last translation PR. | |
| fix: scan all existing translations for structural issues and re-translate broken files from scratch. | |
| type: choice | |
| options: | |
| - normal | |
| - fix | |
| default: normal | |
| language: | |
| description: "Language code (empty = all)" | |
| type: string | |
| since: | |
| description: "Compare since this commit (default: auto-detect from last translation PR)" | |
| type: string | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| detect: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| languages: ${{ steps.detect.outputs.languages }} | |
| has_work: ${{ steps.detect.outputs.has_work }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | |
| - name: Detect work | |
| id: detect | |
| working-directory: _scripts | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| INPUT_LANGUAGE: ${{ inputs.language }} | |
| INPUT_SINCE: ${{ inputs.since }} | |
| INPUT_MODE: ${{ inputs.mode }} | |
| run: | | |
| args=() | |
| [ -n "$INPUT_LANGUAGE" ] && args+=(--language "$INPUT_LANGUAGE") | |
| [ -n "$INPUT_SINCE" ] && args+=(--since "$INPUT_SINCE") | |
| [ "$INPUT_MODE" = "fix" ] && args+=(--fix) | |
| uv run -q python -m translate ci-detect "${args[@]}" >> "$GITHUB_OUTPUT" | |
| translate: | |
| needs: detect | |
| if: needs.detect.outputs.has_work == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false # Continue other languages if one fails | |
| max-parallel: 5 # Limit concurrent languages to avoid API rate limits | |
| matrix: | |
| language: ${{ fromJson(needs.detect.outputs.languages) }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | |
| - name: Translate ${{ matrix.language }} | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| LANGUAGE: ${{ matrix.language }} | |
| INPUT_MODE: ${{ inputs.mode }} | |
| working-directory: _scripts | |
| run: | | |
| args=(sync "$LANGUAGE" --log "translate-$LANGUAGE.json") | |
| [ "$INPUT_MODE" = "fix" ] && args+=(--fix) | |
| uv run -q python -m translate "${args[@]}" | |
| - name: Upload translation artifacts | |
| if: success() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: translation-${{ matrix.language }} | |
| path: docs/${{ matrix.language }}/docs/ | |
| retention-days: 1 | |
| if-no-files-found: ignore | |
| - name: Upload translation log | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: log-${{ matrix.language }} | |
| path: _scripts/translate-${{ matrix.language }}.json | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| merge: | |
| needs: [detect, translate] | |
| if: always() && needs.detect.outputs.has_work == 'true' && !cancelled() | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| # Intentionally persists credentials: ci-pr (in _scripts/translate/cli.py) | |
| # runs `git push origin <branch>` to create the translation PR. | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # zizmor: ignore[artipacked] | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.sha }} # Use trigger SHA, not latest master (avoids workflow file conflicts) | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download all translation artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: translation-* | |
| path: artifacts/ | |
| merge-multiple: false | |
| - name: Check for artifacts | |
| id: check | |
| run: | | |
| if [ -z "$(ls -A artifacts/ 2>/dev/null)" ]; then | |
| echo "has_artifacts=false" >> $GITHUB_OUTPUT | |
| echo "No translation artifacts found" | |
| else | |
| echo "has_artifacts=true" >> $GITHUB_OUTPUT | |
| echo "Found translations for:" | |
| ls artifacts/ | sed 's/translation-/ - /' | |
| fi | |
| - name: Merge translations | |
| if: steps.check.outputs.has_artifacts == 'true' | |
| run: | | |
| # Without nullglob, an unmatched glob gives dirs=("artifacts/translation-*/") with length 1, | |
| # so the empty check below would silently pass with a literal string instead of a real path. | |
| shopt -s nullglob | |
| dirs=(artifacts/translation-*/) | |
| if [ ${#dirs[@]} -eq 0 ]; then | |
| echo "ERROR: No translation-* subdirectories found in artifacts/" | |
| echo "Contents of artifacts/:" | |
| ls -la artifacts/ | |
| exit 1 | |
| fi | |
| for dir in "${dirs[@]}"; do | |
| lang=$(basename "$dir" | sed 's/translation-//') | |
| echo "Merging $lang..." | |
| mkdir -p "docs/$lang/docs/" | |
| cp -r "$dir"* "docs/$lang/docs/" | |
| done | |
| - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | |
| if: steps.check.outputs.has_artifacts == 'true' | |
| - name: Run pre-commit on translations | |
| if: steps.check.outputs.has_artifacts == 'true' | |
| run: | | |
| pip install pre-commit | |
| pre-commit run --all-files || true | |
| - name: Delete orphaned translation files | |
| if: steps.check.outputs.has_artifacts == 'true' | |
| working-directory: _scripts | |
| run: uv run -q python -m translate ci-delete-orphans | |
| - name: Create PR | |
| if: steps.check.outputs.has_artifacts == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| GITHUB_SHA: ${{ github.sha }} | |
| GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| working-directory: _scripts | |
| run: | | |
| # Determine trigger description | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| TRIGGER="manual" | |
| else | |
| # Get first line of commit message for title | |
| TRIGGER=$(git log -1 --format=%s ${{ github.sha }}) | |
| # Get full commit message for body (passed via env var to handle multi-line) | |
| export TRIGGER_COMMIT_MESSAGE=$(git log -1 --format=%B ${{ github.sha }}) | |
| fi | |
| uv run -q python -m translate ci-pr --trigger "$TRIGGER" |