Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Tutorial: Fresh Setup

Create a new translation project from scratch

This tutorial walks through creating a new Chinese translation of an English lecture series from scratch. By the end, you’ll have:

Time: ~30 minutes (plus translation time, which depends on project size) Cost: ~$0.16/lecture for translation using claude-sonnet-5

Prerequisites

Before starting, make sure you have:

RequirementCheck
Source repo cloned locallyls ~/repos/lecture-python-intro/lectures/
gh CLI installed & authenticatedgh auth status
Node.js 18+node --version
Anthropic API keyconsole.anthropic.com
action-translation installedSee Installation

Overview

The fresh setup process has 5 steps:

Step 0: Install action-translation
Step 1: Scaffold the target repo          (translate setup)
Step 2: Translate all lectures             (translate init)
Step 3: Push and verify                    (git push + translate status)
Step 4: Configure workflows and secrets    (manual GitHub setup)
Step 5: Test the pipeline                  (merge a test PR)

Steps 2 and 3 translate and publish every lecture at once, for an edition translated in one go. If each lecture is reviewed before it is published, follow One reviewed lecture at a time in place of Steps 2 and 3.


Step 0: Install action-translation

git clone https://github.com/QuantEcon/action-translation.git
cd action-translation
npm install
npm run build:cli

Verify it works:

npx translate --version

Set your API key for translation steps:

export ANTHROPIC_API_KEY=sk-ant-...

Step 1: Scaffold the target repository

The setup command creates a new GitHub repository and plants the initial configuration files.

Preview first (dry run):

npx translate setup \
  --source QuantEcon/lecture-python-intro \
  --target-language zh-cn \
  --dry-run

This shows what would be created without touching GitHub.

Create the repository:

npx translate setup \
  --source QuantEcon/lecture-python-intro \
  --target-language zh-cn

This:

  1. Creates QuantEcon/lecture-python-intro.zh-cn on GitHub

  2. Clones it locally to ./lecture-python-intro.zh-cn/

  3. Writes .translate/config.yml, .github/workflows/translation-sync.yml, .gitignore, README.md

  4. Commits and pushes

What you get:

lecture-python-intro.zh-cn/
├── .translate/
│   └── config.yml          # source-language: en, target-language: zh-cn
├── .github/
│   └── workflows/
│       └── translation-sync.yml
├── .gitignore
└── README.md

Step 2: Translate all lectures

The init command bulk-translates every lecture in the source project.

Preview first (dry run):

npx translate init \
  -s ~/repos/lecture-python-intro \
  -t ./lecture-python-intro.zh-cn \
  --target-language zh-cn \
  --dry-run

This lists all lectures that would be translated and all non-markdown files that would be copied, without making any API calls.

Run the translation:

npx translate init \
  -s ~/repos/lecture-python-intro \
  -t ./lecture-python-intro.zh-cn \
  --target-language zh-cn

The init command runs a 7-phase pipeline:

PhaseWhat happens
1. Load glossaryLoads glossary/zh-cn.json (357 technical terms)
2. Parse _toc.ymlDiscovers all lectures in the source project
3. Setup targetCreates the docs folder structure
4. Copy non-markdown filesImages, _config.yml, _toc.yml, CSS, data files
5. Translate lecturesSequential translation with progress bar
6. Generate heading-mapsPosition-based section matching, injected into frontmatter
7. Write reportTRANSLATION-REPORT.md with stats and any failures

A progress bar shows the current lecture being translated.

If translation is interrupted, resume from where you left off:

npx translate init \
  -s ~/repos/lecture-python-intro \
  -t ./lecture-python-intro.zh-cn \
  --target-language zh-cn \
  --resume-from cobweb.md

Localization options

By default, init localizes code cells: translates Python comments, matplotlib labels, and injects CJK font configuration. Control this with --localize:

# Translate everything except font configuration
npx translate init ... --localize code-comments,figure-labels

# No code-cell localization at all
npx translate init ... --localize none

Font setup (zh-cn only)

After translation, the CLI prints font download instructions. For Chinese, you need to place a font file at:

lectures/_fonts/SourceHanSerifSC-SemiBold.otf

Download from Adobe Source Han Serif releases.

What init produces

After translation, each lecture file contains:

---
title: 蛛网模型
translation:
  title: 蛛网模型
  headings:
    Overview: "概述"
    Equilibrium: "均衡"
    Exercises: "练习"
---

And the .translate/state/ folder tracks sync metadata:

.translate/
├── config.yml
└── state/
    ├── intro.md.yml
    ├── cobweb.md.yml
    └── solow.md.yml

Step 3: Push and verify

Commit the translated content and push:

cd lecture-python-intro.zh-cn

# Review what was created
ls lectures/

# Commit all translated content
git add .
git commit -m "Initial zh-cn translation via translate init"
git push origin main

Verify the sync state:

cd /path/to/action-translation

npx translate status \
  -s ~/repos/lecture-python-intro \
  -t ~/repos/lecture-python-intro.zh-cn

All files should show ✅ ALIGNED.

Run a health check to confirm everything is properly configured:

npx translate doctor \
  -t ~/repos/lecture-python-intro.zh-cn

All checks should pass (✅) before proceeding to workflow configuration.


One reviewed lecture at a time

Follow this route in place of Steps 2 and 3 when each lecture is reviewed before it is published. The edition grows by one pull request per lecture. Each PR carries the lecture and its state file, .translate/state/<file>.yml, and merging it puts the lecture in sync: the source repo’s sync workflow runs with sync-scope: existing (Step 4a), which updates only the lectures whose state file is on the edition’s default branch. Lectures the edition has not taken yet are skipped and listed in the sync PR or the job summary, never translated from scratch. See Partial editions.

The commands below run translate from the action-translation folder, and expect the edition’s clone at ~/repos/lecture-python-intro.zh-cn, beside the source clone, as Step 3’s checks do.

Prune the table of contents at build time

Do this once, before the first lecture’s PR. Sync keeps the committed _toc.yml a mirror of the source’s, with the edition’s translated part captions, so it lists lectures the edition does not have yet. The first lecture’s PR brings such a copy too, since init copies the source’s. Make the edition’s build prune it to the lectures the edition has, rather than committing a pruned copy that the next sync of _toc.yml would overwrite. QuantEcon/lecture-python-programming.ml does this with scripts/prune_toc.py.

Then repeat the next four steps for each lecture.

Draft the lecture

Update the source clone, and start a branch from an up-to-date main in the edition’s clone:

cd ~/repos/lecture-python-intro
git pull

cd ~/repos/lecture-python-intro.zh-cn
git switch main
git pull
git switch -c translate-cobweb

-f names a lecture listed in the source _toc.yml, and takes the first entry in ToC order that equals or contains the name: -f solow.md would match ak_solow.md if that came first. Preview the match with --dry-run, which prints the Translating single file: line and stops before translating or writing anything:

cd /path/to/action-translation
npx translate init \
  -s ~/repos/lecture-python-intro \
  -t ~/repos/lecture-python-intro.zh-cn \
  --target-language zh-cn \
  -f cobweb.md \
  --dry-run

If the line shows the lecture you meant, run the same command without --dry-run to translate it.

Keep only what the PR needs

-f narrows the translation, not the rest of the run: every init run except a dry run rewrites .translate/config.yml and TRANSLATION-REPORT.md, and copies every non-Markdown file in the source docs folder into the target, _toc.yml and _config.yml included. See what changed:

cd ~/repos/lecture-python-intro.zh-cn
git status

The first lecture’s PR keeps the copied build files (_config.yml, _toc.yml, images and other static files): the edition has none yet, and cannot build without them. A later PR needs only the lecture, its state file and any new file the lecture uses, such as an image: restore the edition’s copies of everything else, and delete the new files the PR does not need, TRANSLATION-REPORT.md among them. For example:

git restore lectures/_toc.yml lectures/_config.yml

Confirm the state file

init does not fail when it cannot write the state file, and without it the lecture never syncs:

ls .translate/state/cobweb.md.yml

translate doctor also flags lectures at the top level of the docs folder that have no state file.

Open the PR, and check for English changes before merging

Commit the lecture, its state file and anything else you kept, then open a PR for review:

git add lectures/cobweb.md .translate/state/cobweb.md.yml   # plus any files you kept
git commit -m "Translate cobweb.md"
git push -u origin translate-cobweb
gh pr create --title "Translate cobweb.md" --body "First translation of cobweb.md, for review."

An English change to this lecture that merges after the draft and before this PR merges is not synced to it: the lecture is out of scope until its PR merges. init recorded the English commit the draft was made from, so before merging, update the source clone, put the edition’s clone on the PR’s branch and pull it (reviewers’ suggestions land on that branch as commits), and check:

cd ~/repos/lecture-python-intro
git pull

cd ~/repos/lecture-python-intro.zh-cn
git switch translate-cobweb
git pull

cd /path/to/action-translation
npx translate status \
  -s ~/repos/lecture-python-intro \
  -t ~/repos/lecture-python-intro.zh-cn \
  -l zh-cn \
  -f cobweb.md

ALIGNED means the English has not changed since the draft, and merging the PR puts the lecture in sync. OUTDATED means it has. The English commit the draft was made from is the source-sha in .translate/state/cobweb.md.yml; in the source clone, git log -p <source-sha>..HEAD -- lectures/cobweb.md shows every change since. Carry the changes into the PR, and commit them in the edition’s clone. Then record the new English commit in the lecture’s state file, with the same command, --write-state and --carried:

cd /path/to/action-translation
npx translate status \
  -s ~/repos/lecture-python-intro \
  -t ~/repos/lecture-python-intro.zh-cn \
  -l zh-cn \
  -f cobweb.md \
  --write-state \
  --carried

--carried confirms that the PR carries every English change since the commit the state file records. Without it, --write-state refuses, and lists the English commits since that one (or says that commit is not in your source clone), so you can check each against the PR. Commit the carried change before this run, because --write-state also refuses a lecture whose English commit is dated after the lecture’s last commit on the branch. The run rewrites the state file with the new English commit and keeps the model init recorded. It also rewrites .translate/config.yml from its flags, so pass the edition’s own -l, and -d if the docs folder is not lectures. Of this run’s changes, the PR takes only the state file: restore the config, then commit and push the state file:

cd ~/repos/lecture-python-intro.zh-cn
git restore .translate/config.yml
git add .translate/state/cobweb.md.yml
git commit -m "Record the English commit carried into cobweb.md"
git push

Then run the check once more: the lecture should now read ALIGNED, and merging the PR puts it in sync.


Step 4: Configure workflows and secrets

You need two workflows: one in the source repo (to trigger translations) and one in the target repo (to review them).

4a: Source repo — Sync workflow

Create .github/workflows/sync-translations.yml in the source (English) repository:

name: Sync Translations

on:
  pull_request:
    types: [closed]
    branches: [main]
    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 &&
       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'
          target-language: 'zh-cn'
          docs-folder: 'lectures'
          anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
          github-token: ${{ secrets.TRANSLATION_PAT }}

If the edition is built one reviewed lecture at a time, add sync-scope: existing (a temporary input; see Partial editions) to the action’s inputs, so that a sync updates only the lectures the edition has taken:

- uses: QuantEcon/action-translation@v0
  with:
    mode: sync
    # ... the inputs above
    sync-scope: existing

4b: Source repo — Secrets

Add these secrets to the source repo (Settings → Secrets and variables → Actions):

SecretValuePurpose
ANTHROPIC_API_KEYYour Anthropic API keyPays for Claude translations
TRANSLATION_PATGitHub PAT with repo scopeCreates PRs in the target repo

The PAT needs repo scope because the action creates PRs in the target repository from the source repo’s workflow.

4c: Target repo — Review workflow (optional)

Create .github/workflows/review-translations.yml in the target repository:

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'
          anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
          github-token: ${{ secrets.GITHUB_TOKEN }}

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). Add the ANTHROPIC_API_KEY secret to the target repo as well (for the review workflow).

4d: Target repo — Secret

SecretValuePurpose
ANTHROPIC_API_KEYYour Anthropic API keyPays for AI-powered translation reviews

The target repo uses ${{ secrets.GITHUB_TOKEN }} (automatic) for PR access — no PAT needed.


Step 5: Test the pipeline

Make a small change in the source repo and merge a PR to verify the full pipeline:

  1. Create a branch in the source repo and edit a lecture (e.g., fix a typo in cobweb.md)

  2. Open and merge a PR — the sync workflow triggers

  3. Check the target repo — a translation PR should appear within a few minutes

  4. Review the PR — if the review workflow is configured, it posts a quality assessment

  5. Merge the translation PR — the target repo is now in sync

On an edition built one lecture at a time, edit a lecture the edition already has. A change to any other lecture is skipped, and on its own opens no translation PR.


What happens going forward

Once configured, the automated pipeline handles ongoing changes:

You merge a PR in source repo
        │
        ▼
Sync workflow detects changed sections
        │
        ▼
Claude translates ONLY the changed sections
        │
        ▼
Translation PR created in target repo
        │
        ▼
Review workflow posts quality scores (optional)
        │
        ▼
Human reviewer approves & merges

For maintenance tasks (checking drift, finding backport suggestions, recovering from failed syncs), use the CLI tools:

# Quick status check
npx translate status -s ~/source -t ~/target

# Find improvements worth backporting to English
npx translate backward -s ~/source -t ~/target

# Resync drifted files
npx translate forward -s ~/source -t ~/target

Troubleshooting

gh repo create fails

init fails mid-translation

Use --resume-from to pick up where you left off. Check TRANSLATION-REPORT.md for details on which lectures failed and why.

Sync workflow doesn’t trigger

Translation PR has no changes

Next steps