This tutorial covers connecting action-translation to a project where the target repository already exists and contains translations — but was translated manually or with a different tool, before action-translation was in place.
By the end, you’ll have:
.translate/metadata bootstrapped from the current stateHeading-maps generated for all translated files
Automated sync and review workflows configured
Time: ~30 minutes (includes sync verification) Cost: Free for status/heading-maps. ~0.16/file if resync needed.
When to use this tutorial¶
Use this when you have:
An English source repo (e.g.,
QuantEcon/lecture-python-intro)A Chinese target repo (e.g.,
QuantEcon/lecture-python-intro.zh-cn)The target already has translated
.mdfiles in a docs folderThe repos are not yet connected via action-translation (no
.translate/folder, no sync workflow)
It also fits a partial edition: one that has translated some lectures, may hold English copies of others, and takes the rest one reviewed PR at a time, as in One reviewed lecture at a time. Notes in Steps 2, 4, 5, 6 and 7, and under Common issues, cover what differs for it.
If you’re starting from scratch with no translations, see Tutorial: Fresh Setup instead.
Overview¶
Step 1: Assess the current state (translate status)
Step 2: Check sync before bootstrapping (translate status --check-sync)
Step 3: Catch up stale translations (translate forward, if needed)
Step 4: Bootstrap .translate/ metadata (translate status --write-state)
Step 5: Fix missing heading-maps (translate headingmap)
Step 6: Configure workflows and secrets (manual GitHub setup)
Step 7: Verify the connection (translate doctor)Step 1: Assess the current state¶
Before making changes, understand what you’re working with. The status command gives a free, instant diagnostic:
npx translate status \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cnExample output:
Sync Status: lecture-python-intro ↔ lecture-python-intro.zh-cn (zh-cn)
File Status
──────────────────────────────── ────────────────────
intro.md ✅ ALIGNED
cobweb.md 📋 MISSING_HEADINGMAP
solow.md 📋 MISSING_HEADINGMAP
cagan_adaptive.md ⏳ OUTDATED
pv.md ⚠️ SOURCE_AHEAD
new_lecture.md ➕ SOURCE_ONLY
old_removed.md 🔸 TARGET_ONLYWhat to look for:
| Status | What it means | Action needed |
|---|---|---|
ALIGNED | Sections match and file is up to date | None |
MISSING_HEADINGMAP | No heading-map in target frontmatter | Add heading-map (Step 3) |
OUTDATED | Source has newer commits | Resync later (see Resync Tutorial) |
SOURCE_AHEAD | Source has more sections than target | Resync later |
SOURCE_ONLY | File exists only in source | Translate new file later |
TARGET_ONLY | File exists only in target | Investigate (orphaned?) |
At this stage, you’re gathering information. Pay attention to OUTDATED and SOURCE_AHEAD statuses — these indicate the translations may have fallen behind the source. We’ll address those in Step 2 before bootstrapping.
For a more comprehensive assessment including workflow and state file verification, run:
npx translate doctor \
-t ~/repos/lecture-python-intro.zh-cnThis reports configuration health across all dimensions. All items should be ✅ by the end of Step 5.
Step 2: Check sync before bootstrapping¶
Run the sync check to compare source and target content:
npx translate status \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cn \
--check-syncThis uses a lightweight LLM comparison to classify each file:
| Result | Meaning | Action |
|---|---|---|
IDENTICAL | Translation matches current source | Safe to bootstrap |
I18N_ONLY | Differences are localization-only (fonts, RTL, etc.) | Safe to bootstrap |
CONTENT_CHANGES | Source has content changes not in translation | Resync first (Step 3) |
TARGET_HAS_ADDITIONS | Target has extra content not in source | Run backward to capture, then resync |
If all files show IDENTICAL or I18N_ONLY, skip to Step 4. If any show CONTENT_CHANGES, proceed to Step 3. If any show TARGET_HAS_ADDITIONS, consider running translate backward first to capture improvements before resyncing.
Step 3: Catch up stale translations¶
For files flagged as CONTENT_STALE, use the forward command to bring them up to date. The forward command preserves existing localization (RTL adaptations, font configuration, # i18n code, locale-specific links) while updating the translation to match the current source content.
# Dry run first — see what would change
npx translate forward \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cn \
--dry-run
# Resync all stale files
npx translate forward \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cnFor a single file:
npx translate forward \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cn \
-f solow.mdReview and commit the changes:
cd ~/repos/lecture-python-intro.zh-cn
git diff lectures/ # Review all changes
git add lectures/
git commit -m "Resync translations to current source via forward"
git pushStep 4: Bootstrap .translate/ metadata¶
Now that translations are current, bootstrap the state tracking. The .translate/ folder records which source commit each translation is synced to:
npx translate status \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cn \
-l zh-cn \
--write-stateThis creates:
.translate/
├── config.yml # Project settings
└── state/
├── intro.md.yml # Per-file metadata
├── cobweb.md.yml
└── solow.md.ymlEach state file records:
source-sha: abc1234f # Current HEAD SHA for the source file
synced-at: "2026-03-16" # Target file's last commit date
model: unknown # Not recoverable from history
mode: RESYNC # Marked as RESYNC (not a fresh translation)
section-count: 5 # Current source section countCommit the metadata:
cd ~/repos/lecture-python-intro.zh-cn
git add .translate/
git commit -m "Bootstrap .translate/ metadata via action-translation"
git push origin mainStep 5: Fix missing heading-maps¶
Heading-maps are essential for reliable section matching. If Step 1 showed MISSING_HEADINGMAP for some files, you need to add them.
Option A: Use headingmap to generate maps (recommended)¶
The headingmap command generates heading-maps by matching source and target headings by position — no LLM calls needed, and no changes to translations:
# Generate heading-map for a single file
npx translate headingmap \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cn \
-f cobweb.mdThis:
Parses both files to extract section headings
Matches them by position (1st source → 1st target, etc.)
Injects the
heading-mapinto the target file’s YAML frontmatterMakes no changes to the translation content
For all files missing heading-maps:
npx translate headingmap \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cnPreview first with --dry-run:
npx translate headingmap \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cn \
--dry-runOption B: Use forward to regenerate (also adds heading-maps)¶
If you already ran forward in Step 3, heading-maps were generated as part of that process. Check if any files still need them:
npx translate headingmap \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cn \
--dry-runThe diff should show the heading-map added to the frontmatter. The translation content should be largely preserved (the RESYNC mode is designed to maintain existing style).
For multiple files:
# Resync all files that need attention
npx translate forward \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cnOption C: Add heading-maps manually¶
If you don’t want to re-translate files (e.g., they’ve been carefully reviewed by human translators), you can add heading-maps by hand.
For each translated file:
Parse the source file to find the English section headings and their IDs
Find the corresponding translated headings in the target file
Add the heading-map to the target file’s YAML frontmatter
Example:
English source (cobweb.md):
## Overview
## The Cobweb Model
### Equilibrium
## ExercisesChinese target (cobweb.md):
## 概述
## 蛛网模型
### 均衡
## 练习Add this to the target’s frontmatter:
---
title: 蛛网模型
translation:
title: 蛛网模型
headings:
Overview: "概述"
The Cobweb Model: "蛛网模型"
The Cobweb Model::Equilibrium: "均衡"
Exercises: "练习"
---Heading keys use the full heading text with :: nesting for subsections. See Heading Maps for complete format rules.
Commit the heading-maps¶
cd ~/repos/lecture-python-intro.zh-cn
git add lectures/
git commit -m "Add heading-maps to translated files"
git push origin mainStep 6: Configure workflows and secrets¶
Now set up the automated pipeline. You need a workflow in the source repo and one in the target repo.
6a: Source repo — Sync workflow¶
Create .github/workflows/sync-translations.yml in the source repository:
name: Sync Translations
on:
pull_request:
types: [closed]
branches: [main]
paths:
- 'lectures/**/*.md'
- 'lectures/_toc.yml'
issue_comment:
types: [created]
jobs:
sync:
# 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 &&
startsWith(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-python-intro.zh-cn'
source-language: 'en'
target-language: 'zh-cn'
docs-folder: 'lectures'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.TRANSLATION_PAT }}On a partial edition, add sync-scope: existing (a temporary input; see Partial editions) to the action’s inputs. A sync then updates only the lectures that have a state file on the edition’s default branch, and never translates a lecture the edition has not taken:
- uses: QuantEcon/action-translation@v0
with:
mode: sync
# ... the inputs above
sync-scope: existingSwitch only once every lecture the edition has translated has its state file on the default branch (Step 4). Under existing, a lecture without one is not synced, so an edition with no state files would sync no lecture at all.
Sync keeps _toc.yml a mirror of the source’s under existing too, so the first sync of a source PR that changes it replaces the edition’s table of contents with one that lists lectures the edition does not have yet. Before this workflow merges, make the edition’s build prune it (see Prune the table of contents at build time).
6b: Source repo — Secrets¶
| Secret | Value | Purpose |
|---|---|---|
ANTHROPIC_API_KEY | Your Anthropic API key | Pays for Claude translations |
TRANSLATION_PAT | GitHub PAT with repo scope | Creates PRs in the target repo |
6c: Target repo — Review workflow¶
Create .github/workflows/review-translations.yml in the target repository:
name: Review Translations
on:
pull_request:
types: [opened, synchronize, labeled, reopened]
jobs:
review:
# Ignore `labeled` events for every other label: a sync applies its labels one call at a
# time, and each one would otherwise start a full (billed) review of the same diff.
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'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}There is no target-language input here: review mode detects the language from the repository-name suffix (lecture-python-intro.zh-cn → zh-cn). A repo without a language suffix logs a warning and reviews without a glossary.
6d: Target repo — Secret¶
| Secret | Value |
|---|---|
ANTHROPIC_API_KEY | Your Anthropic API key |
6e: Merge order¶
Merge the target repo changes first (.translate/, heading-maps, review workflow), then the source repo sync workflow. This ensures the target is ready to receive translation PRs when the sync workflow activates.
Step 7: Verify the connection¶
Run status again to confirm everything is properly connected:
npx translate status \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cnWhat “ready” looks like:
All files show
✅ ALIGNEDor⏳ OUTDATED(OUTDATED is OK — it just means the source has newer changes)No
MISSING_HEADINGMAPwarnings.translate/config.ymlexists in the target repo.translate/state/*.ymlfiles exist for each translated file
Run a final health check:
npx translate doctor \
-t ~/repos/lecture-python-intro.zh-cn \
-s ~/repos/lecture-python-introAll checks should pass ✅. If some files show OUTDATED or SOURCE_AHEAD, address them with forward before considering the connection complete — otherwise those files will be silently out of date.
On a partial edition, some of this reads differently:
statusshows the lectures the edition has not taken asSOURCE_ONLY. Undersync-scope: existing, sync never translates them: each joins the edition in its own reviewed PR, as in One reviewed lecture at a time.The English copies have no state file and no heading-map, on purpose.
doctorwarns about the missing state files, and about the missing heading-maps of the copies that have sections, whichstatusshows withMISSING_HEADINGMAP.A copy the source has moved on from shows
OUTDATED, orSOURCE_AHEADwhen the source added a section. Leave it:forwardwould rewrite it and give it a state file, which puts it in sync’s scope.doctornames at most five files without a state file, and checks only the top level of the docs folder. Compare.translate/state/with the lectures the edition has translated, subfolders included: each translated lecture needs a state file, and no English copy may have one.
Common issues when connecting¶
Many files show MISSING_HEADINGMAP¶
This is expected for repos translated before action-translation.
On a partial edition, the English copies that have sections show MISSING_HEADINGMAP too, and should keep it. Run either command below with -f for each translated lecture, or with --exclude for the copies: without either, forward rewrites each copy the source has moved on from and gives it a state file, which puts it in sync’s scope (see the note in Step 5).
Use headingmap to fix without changing translations:
npx translate headingmap \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cnOr use forward if you also want to update the translations:
npx translate forward \
-s ~/repos/lecture-python-intro \
-t ~/repos/lecture-python-intro.zh-cnReview changes, commit, and push.
Section counts don’t match (SOURCE_AHEAD / TARGET_AHEAD)¶
This means the source and target have different numbers of ## sections. Common causes:
A section was added to the source but not translated yet → address with
forwardThe target was manually restructured → may need manual alignment
Subsections (
###) inside a section don’t affect the count — only##headings matter
On a partial edition, an English copy can show SOURCE_AHEAD too; leave it (see Step 7).
Files exist in only one repo¶
SOURCE_ONLY— new file that hasn’t been translated yet. Translate it withinit -f <file>, sinceforwardonly resyncs a file the target already has; Tutorial: Resync a Drifted Target lists what elseinit -fchanges. Under the defaultsync-scope: all, you can instead wait for the sync action to translate it when a PR changes it. Undersync-scope: existing, sync never translates it: add it in its own reviewed PR, as in One reviewed lecture at a time.TARGET_ONLY— a file that was deleted from source. Consider deleting it from target too.
Next steps¶
Tutorial: Resync a Drifted Target — catch up on outdated files
Tutorial: Backward Analysis & Review — find improvements in translations worth backporting
Tutorial: Automated Maintenance — set up scheduled checks
CLI Reference — full command documentation
Heading Maps — format rules and ID generation