[translation] Update zh-TW language. #195
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
| ############################################################################################# | |
| # Translation issue bot | |
| # | |
| # Purpose: Community contributions come in as issues, not PRs. This workflow: | |
| # 1. On opened/edited: validate the issue body, post a preview comment. | |
| # 2. On labeled `approved` (by a maintainer): apply the translation to the | |
| # target locale file, commit it, close the issue. | |
| # | |
| # The `approved` label is only acted on if the labeler has write/maintain/admin | |
| # permission on this repo, verified via the GitHub API at the top of the apply | |
| # job. (`github.event.sender.author_association` is NOT populated on the issues | |
| # event: `author_association` lives on Issue/Comment/Review entities, not on | |
| # the simple-user `sender`, so an `if:`-based check would silently always be | |
| # false and no apply would ever fire.) | |
| # | |
| # Comments and the apply commit both authenticate through the Saki-Yuzishima | |
| # App installation token, so the bot shows up as `saki-yuzishima[bot]` rather | |
| # than the generic `github-actions[bot]`. The apply commit is AUTHORED by the | |
| # issue contributor (committer stays the bot), so translators appear in the | |
| # contributors graph and get profile credit for their work. Requires org secrets | |
| # CIDER_I18N_BOT_APP_ID and CIDER_I18N_BOT_PRIVATE_KEY, and the App must have | |
| # `Contents: write` and `Issues: write` granted on this repo. | |
| ############################################################################################# | |
| name: Translation issue | |
| on: | |
| issues: | |
| types: [opened, edited, labeled] | |
| # Least-privilege at the workflow level. Individual jobs widen as needed. | |
| # Nothing here ever runs on pull_request, so even if a malicious PR landed, | |
| # this workflow's secrets would not be reachable from that PR's code. | |
| permissions: {} | |
| jobs: | |
| validate: | |
| if: | | |
| contains(github.event.issue.labels.*.name, 'translation') && | |
| (github.event.action == 'opened' || github.event.action == 'edited') | |
| name: Validate and preview | |
| runs-on: ubuntu-latest | |
| # No GITHUB_TOKEN usage in this job: comments post under the App token. | |
| permissions: {} | |
| steps: | |
| - name: 🪪 Mint App installation token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.CIDER_I18N_BOT_APP_ID }} | |
| private-key: ${{ secrets.CIDER_I18N_BOT_PRIVATE_KEY }} | |
| permission-issues: write | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| - run: npm install --no-package-lock | |
| - name: Run linter | |
| env: | |
| # Pass the App token as GITHUB_TOKEN so the preview comment posts | |
| # as saki-yuzishima[bot] instead of the generic github-actions[bot]. | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| ISSUE_AUTHOR: ${{ github.event.issue.user.login }} | |
| ISSUE_BODY: ${{ github.event.issue.body }} | |
| # 'opened' or 'edited': the linter uses this to phrase the comment | |
| # differently and to upsert (edit) its own previous comment rather | |
| # than posting a new one for every edit. | |
| ISSUE_ACTION: ${{ github.event.action }} | |
| MODE: validate | |
| run: node scripts/lint-translation-issue.mjs | |
| apply: | |
| if: | | |
| github.event.action == 'labeled' && | |
| github.event.label.name == 'approved' && | |
| contains(github.event.issue.labels.*.name, 'translation') | |
| name: Apply approved translation | |
| runs-on: ubuntu-latest | |
| # GITHUB_TOKEN is used only by the Verify step (collaborator-permission | |
| # lookup needs metadata:read, which is granted implicitly). The actual | |
| # apply, push, and comment all authenticate via the App token. | |
| permissions: {} | |
| # Serialize approvals so two simultaneous `approved` labels can't race | |
| # against each other when writing the same locale file. Don't cancel: | |
| # losing an approved contribution mid-write would be worse than waiting. | |
| concurrency: | |
| group: translation-issue-apply | |
| cancel-in-progress: false | |
| steps: | |
| - name: 🛡️ Verify approver has write access | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| perm=$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${GITHUB_ACTOR}/permission" --jq .permission) | |
| case "$perm" in | |
| admin|maintain|write) | |
| echo "✓ @${GITHUB_ACTOR} has $perm; proceeding." | |
| ;; | |
| *) | |
| echo "::error::@${GITHUB_ACTOR} cannot apply translations (permission: $perm). Only repo members with write/maintain/admin may approve." | |
| exit 1 | |
| ;; | |
| esac | |
| - name: 🪪 Mint App installation token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.CIDER_I18N_BOT_APP_ID }} | |
| private-key: ${{ secrets.CIDER_I18N_BOT_PRIVATE_KEY }} | |
| permission-contents: write | |
| permission-issues: write | |
| - uses: actions/checkout@v4 | |
| with: | |
| # The push authenticates explicitly with the App token below. | |
| persist-credentials: false | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| - run: npm install --no-package-lock | |
| - name: Apply to locale file | |
| env: | |
| # App token so the success comment + close-issue both come from | |
| # saki-yuzishima[bot]. | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| ISSUE_AUTHOR: ${{ github.event.issue.user.login }} | |
| ISSUE_BODY: ${{ github.event.issue.body }} | |
| ISSUE_ACTION: ${{ github.event.action }} | |
| MODE: apply | |
| run: node scripts/lint-translation-issue.mjs | |
| - name: Update credits and badges | |
| run: node scripts/update-credits.mjs | |
| - name: Commit and push | |
| env: | |
| APP_TOKEN: ${{ steps.app-token.outputs.token }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| ISSUE_AUTHOR: ${{ github.event.issue.user.login }} | |
| ISSUE_AUTHOR_ID: ${{ github.event.issue.user.id }} | |
| run: | | |
| set -euo pipefail | |
| # Stage first, then diff the index. A first-ever contribution for a | |
| # language creates a new untracked file, which `git diff` alone | |
| # would not see. Badges and the README table are regenerated by the | |
| # previous step and ride along in the same commit. | |
| git add locales/ .github/badges/ README.md | |
| if git diff --cached --quiet; then | |
| echo "No file changes after apply. Was the issue already applied?" | |
| exit 0 | |
| fi | |
| git config user.name 'saki-yuzishima[bot]' | |
| git config user.email '286499753+saki-yuzishima[bot]@users.noreply.github.com' | |
| # Author = the contributor, so GitHub's contributors graph and the | |
| # commit byline credit the human; committer stays the bot. The | |
| # id+login noreply address keeps the attribution attached to the | |
| # account even if the user later renames. (Values come in via env, | |
| # not inline workflow expressions, so nothing user-controlled | |
| # touches the script source; logins are [A-Za-z0-9-] and the apply | |
| # step has already hard-failed on anything weirder via AUTHOR_RE.) | |
| git commit \ | |
| --author="${ISSUE_AUTHOR} <${ISSUE_AUTHOR_ID}+${ISSUE_AUTHOR}@users.noreply.github.com>" \ | |
| -m "feat(i18n): apply translation from #${ISSUE_NUMBER}" | |
| # Push with rebase-retry. The job-level concurrency group serializes | |
| # apply runs against each other, but cross-workflow pushes (ai-fill | |
| # commits, en-US mirror from Citadel) can still land between this | |
| # checkout and our push, causing a non-fast-forward rejection. On | |
| # rejection, fetch the new tip, rebase our commit on top, retry. | |
| # Token explicit in the URL: does not depend on stored credentials. | |
| REMOTE="https://x-access-token:${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" | |
| for attempt in 1 2 3 4 5; do | |
| if git push "$REMOTE" HEAD:main; then | |
| echo "push succeeded (attempt $attempt)" | |
| exit 0 | |
| fi | |
| echo "::warning::push attempt $attempt rejected (likely concurrent push). Rebasing onto origin/main..." | |
| git fetch "$REMOTE" main | |
| if ! git rebase FETCH_HEAD; then | |
| echo "::error::rebase conflicts the bot can't resolve. A maintainer can re-apply the 'approved' label to retry from a fresh checkout." | |
| git rebase --abort || true | |
| exit 1 | |
| fi | |
| # Jittered backoff so concurrent retries don't synchronize. | |
| sleep $((attempt + RANDOM % 3)) | |
| done | |
| echo "::error::push failed after 5 attempts; the remote ref keeps moving." | |
| exit 1 |