Complete reference for the QuantEcon/action-translation GitHub Action.
Modes¶
The action operates in three modes, specified by the mode input:
Sync mode¶
Runs in the source (English) repository. When a PR is merged that changes Markdown files, the action:
Detects which files and sections changed
Translates only the changed sections using Claude
Reconstructs the target document, preserving unchanged sections
Creates a PR in the target repository with the updated translations
Posts a confirmation comment on the source PR
If sync fails, it automatically opens a GitHub Issue with error details and recovery instructions.
Review mode¶
Runs in the target (translated) repository. When a translation PR is opened (typically by sync mode), the action:
Compares the translation against the source content
Evaluates translation quality and diff accuracy
Posts a review comment with scores, structured findings, and a machine-readable verdict block (see Metadata contract)
Rebase mode¶
Runs in the target (translated) repository, triggered when a translation PR is merged. Open translation PRs go stale as soon as the base moves, so the action:
Finds the other open translation PRs — both sync (
translation-sync-*) and CLI resync (resync/*) branchesFor each one whose files overlap the merged PR, re-runs the sync pipeline and force-pushes, resolving the conflict while preserving translated content
For each one that does not overlap, skips by default — or, with
rebase-stale-siblings, refreshes it against the new base without re-translating
Inputs¶
Required inputs¶
| Input | Description |
|---|---|
mode | Operation mode: sync, review, or rebase |
anthropic-api-key | Anthropic API key for Claude |
github-token | GitHub token for API access (cross-repo requires a PAT with repo scope) |
Sync mode inputs¶
| Input | Default | Description |
|---|---|---|
target-repo | (required) | Target repository for translations (owner/repo) |
target-language | (required) | Target language code (e.g., zh-cn, fa) |
docs-folder | lectures/ | Documentation folder containing Markdown files |
source-language | en | Source language code |
glossary-path | (empty) | Path to custom glossary JSON file. Overrides the built-in glossary for the target language; if empty, the built-in one is used. Honoured by all three modes — sync, rebase and review. A path that cannot be read or parsed fails the run rather than falling back to different terminology |
claude-model | claude-sonnet-5 | Claude model for translation |
bibliography | backfill | How to handle citations a run introduces that the target bibliography lacks (#117). backfill copies the entry from the source bibliography; lint reports and fails the run without copying; off disables the check. Entries are only ever appended — a key that already resolves in the target is never a candidate, so an edition that has localised an entry cannot have it clobbered. An unrecognised value fails the run |
pr-labels | action-translation,automated | Comma-separated labels for created PRs |
pr-reviewers | (empty) | GitHub usernames to request as reviewers |
pr-team-reviewers | (empty) | GitHub team slugs to request as reviewers |
test-mode | false | Use PR head commit instead of merge commit (for testing) |
Review mode inputs¶
| Input | Default | Description |
|---|---|---|
source-repo | (required) | Source repository for English content (owner/repo) |
source-language | en | Source language code |
docs-folder | lectures/ | Documentation folder |
max-suggestions | 5 | Maximum findings in review comment |
claude-model | claude-sonnet-5 | Claude model for review |
auto-merge-mode | off | off or shadow. Shadow records the would-auto-merge decision in the verdict block and outputs without acting on it. active is not implemented and fails loudly (#103) |
There is no target-language input in review mode: the language is detected from the repository-name suffix (lecture-python-intro.zh-cn → zh-cn), which selects the glossary for terminology review. A repo whose name carries no language suffix logs a warning and reviews without a glossary.
Rebase mode inputs¶
| Input | Default | Description |
|---|---|---|
rebase-stale-siblings | false | Also refresh open translation PRs that do not overlap the merged PR, so their checks re-run against the new base. No re-translation and no model calls — the branch is merged forward, nothing else. |
rebase-stale-siblings exists for drift-recovery waves. translate forward --github opens one PR per lecture, and each PR touches only that lecture and its own state file — so no two siblings ever overlap, none are conflict-rebased, and every merge leaves the rest of the wave stale. Enabling this keeps them current.
It is off by default because the cost scales with the wave: with 60 open PRs, every merge refreshes up to 59 branches and re-runs their checks. Turn it on while a wave is in flight, and off again afterwards.
Token caveat — this applies to all of rebase mode, not just the new input. Commits pushed with the default GITHUB_TOKEN do not trigger workflows (GitHub’s recursion guard), so a branch rebased or refreshed with it gets its new commit but no CI runs on it — verified live on the test harness, where 13 force-push rebased PRs ended with zero check runs on their new heads. If the goal is re-run checks (it usually is), pass a PAT or GitHub App token as github-token in the rebase workflow, exactly as the sync workflows already do with the machine-user PAT. With required status checks, a GITHUB_TOKEN refresh is worse than nothing: the PR goes from stale-but-green to a head with no runs at all, which blocks merging until someone triggers checks by hand.
Outputs¶
Sync mode outputs¶
| Output | Description |
|---|---|
pr-url | URL of the created translation PR |
files-synced | Number of files synchronized |
Review mode outputs¶
| Output | Description |
|---|---|
review-verdict | Review verdict: PASS, WARN, or FAIL |
translation-score | Overall translation quality score (1–10) |
diff-score | Diff accuracy score (1–10) |
review-recommendation | Categorical routing recommendation: auto-merge or editor (verdict v2) |
reviewed-head-sha | Head SHA the verdict was computed against |
would-auto-merge | Shadow-gate decision (true/false); only set when auto-merge-mode: shadow |
Workflow examples¶
Basic sync (single language)¶
name: Sync Translations
on:
pull_request:
types: [closed]
paths:
- 'lectures/**/*.md'
- '_toc.yml'
issue_comment:
types: [created]
jobs:
sync-to-chinese:
# The issue_comment path requires all three: a comment on a PR (not a bare
# issue), the command, and a trusted author — otherwise any account could
# fire a secrets-bearing run (Anthropic spend plus the PAT) from any comment.
if: >
(github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '\translate-resync') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
runs-on: ubuntu-latest
# The action authenticates with TRANSLATION_PAT; the ambient GITHUB_TOKEN
# is unused beyond checkout, so keep it read-only.
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 2
- uses: QuantEcon/action-translation@v0
with:
mode: sync
target-repo: 'QuantEcon/lecture-intro.zh-cn'
target-language: 'zh-cn'
docs-folder: 'lectures/'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.TRANSLATION_PAT }}The issue_comment trigger enables the \translate-resync command — comment it on any merged PR to retry a failed sync. To retrigger only one language, add the language code: \translate-resync fa or \translate-resync zh-cn. Bare \translate-resync retriggers all languages.
The four conditions on that clause are load-bearing. issue_comment workflows run in default-branch context with full access to secrets, and GitHub cannot filter the event by comment body at the trigger level — so the if: is the only gate. It checks that the comment is on a pull request (github.event.issue.pull_request; plain issues raise the same event), that it carries the command, and that its author is an OWNER, MEMBER or COLLABORATOR. Without the last one, any GitHub account can spend your Anthropic credits by commenting on a merged PR. CONTRIBUTOR — anyone with one merged PR — is deliberately excluded, and the action enforces the same set internally, so widening the workflow alone would only buy a run that no-ops. permissions: contents: read completes the picture: the action authenticates to the target repo with TRANSLATION_PAT, so the ambient GITHUB_TOKEN never needs write.
Multi-language sync¶
Use separate jobs for each target language. They run in parallel and create independent PRs:
name: Sync Translations
on:
pull_request:
types: [closed]
paths:
- 'lectures/**/*.md'
issue_comment:
types: [created]
jobs:
sync-to-chinese:
# Every job carries the same guard — the trust gate belongs on each one,
# not on the first.
if: >
(github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '\translate-resync') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 2
- uses: QuantEcon/action-translation@v0
with:
mode: sync
target-repo: 'QuantEcon/lecture-intro.zh-cn'
target-language: 'zh-cn'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.TRANSLATION_PAT }}
sync-to-farsi:
if: >
(github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '\translate-resync') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 2
- uses: QuantEcon/action-translation@v0
with:
mode: sync
target-repo: 'QuantEcon/lecture-intro.fa'
target-language: 'fa'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.TRANSLATION_PAT }}Review on translation PRs¶
name: Review Translations
on:
pull_request:
types: [opened, synchronize, labeled, reopened]
jobs:
review:
# `labeled` matters: the sync applies its labels after opening the PR.
# The second clause ignores `labeled` events for every other label.
if: >
contains(github.event.pull_request.labels.*.name, 'action-translation') &&
(github.event.action != 'labeled' || github.event.label.name == 'action-translation')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
# One review per PR — supersede an in-flight review instead of running both
concurrency:
group: review-translations-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 2
- uses: QuantEcon/action-translation@v0
with:
mode: review
source-repo: 'QuantEcon/lecture-python-intro'
source-language: 'en'
docs-folder: 'lectures/'
max-suggestions: 5
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}Using outputs¶
- uses: QuantEcon/action-translation@v0
id: translate
with:
mode: sync
# ... other inputs
- name: Comment on PR
if: steps.translate.outputs.pr-url
run: |
echo "Translation PR created: ${{ steps.translate.outputs.pr-url }}"
echo "Files synced: ${{ steps.translate.outputs.files-synced }}"How sync mode works¶
When a PR is merged, sync mode:
Identifies changed files — Compares the PR’s diff against the docs folder pattern
Classifies each file — New file (full translation) or existing file (section-level update)
Parses sections — Splits documents at
##headings using a stack-based parserDetects section changes — Compares old and new source to find which sections differ
Translates changed sections — Sends only modified sections to Claude (UPDATE mode)
Reconstructs the document — Merges translated sections back with unchanged content
Updates heading-map — Refreshes the section ID mapping in the target frontmatter
Creates a PR — Commits all updated files to a branch in the target repo
Posts a success comment — Confirms sync completion on the source PR with a link to the translation PR
If any files fail to process, the action opens a GitHub Issue with error details and a link to the source PR. Comment \translate-resync on the merged PR to retry all languages, or \translate-resync fa to retry a specific language.
For new files, the entire document is translated in a single call (NEW mode).
Claude model selection¶
The default model is claude-sonnet-5, which provides excellent translation quality at reasonable cost. Options:
| Model | Cost | Best for |
|---|---|---|
claude-sonnet-5 | ~$0.06/file | Daily sync operations (recommended) |
claude-opus-4-8 | ~$0.10/file | High-stakes translations needing maximum quality |
claude-haiku-4-5 | ~$0.02/file | Budget-conscious bulk operations |
Costs are approximate and based on Sonnet 5 standard pricing (15 per M input/output tokens); the introductory rate (10 per M through 2026-08-31) runs ~13% lower. Sonnet 5’s tokenizer produces ~30% more tokens than Sonnet 4.6 for the same text, which is already reflected above.
File naming conventions¶
The action follows QuantEcon’s repository naming convention:
Source:
lecture-python-intro(English)Target:
lecture-intro.zh-cn(Chinese),lecture-intro.fa(Farsi)
The target-repo input must be the full owner/repo path (e.g., QuantEcon/lecture-intro.zh-cn).
Root-level docs folder¶
If your Markdown files are in the repository root (not a subfolder), set docs-folder to .:
docs-folder: '.'The action handles this correctly, filtering only top-level .md files.