Skip to content

[translation] Update zh-TW language. #195

[translation] Update zh-TW language.

[translation] Update zh-TW language. #195

#############################################################################################
# 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