Skip to content

Commit 6d3a840

Browse files
committed
Add tooling and build support for translated docs sites
A manually-run tool under scripts/docs/i18n translates prose pages and navigation labels into the languages registered in i18n/languages.yml, guided by per-language instruction files and glossaries, with structural and meaning checks before anything is written. The docs build stages each language into its own site under /<code>/ (English pages overlaid with the available translations plus status banners) with translated navigation and a language switcher. Human-authored inputs and generated output both live under i18n/; docs/translations.md explains the model to readers.
1 parent a4f4ccd commit 6d3a840

87 files changed

Lines changed: 8802 additions & 126 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
# Generated
22
uv.lock linguist-generated=true
3+
i18n/languages/*/pages/** linguist-generated=true
4+
i18n/languages/*/state.json linguist-generated=true
5+
i18n/languages/*/nav.yml linguist-generated=true
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: 🌐 Translation problem
2+
description: Report a wrong, awkward, or misleading passage in a translated docs page
3+
labels: ["translation"]
4+
5+
body:
6+
- type: markdown
7+
attributes:
8+
value: |
9+
The translated docs are machine-generated from the English pages; https://py.sdk.modelcontextprotocol.io/translations/ explains how.
10+
Fixes never go into the translated text directly. They go into that language's glossary or style guide under `i18n/languages/`, so you can also open a PR there instead of an issue.
11+
12+
- type: dropdown
13+
id: language
14+
attributes:
15+
label: Language
16+
options:
17+
- Simplified Chinese (zh-CN)
18+
- Japanese (ja)
19+
- Korean (ko)
20+
- Brazilian Portuguese (pt-BR)
21+
validations:
22+
required: true
23+
24+
- type: input
25+
id: page
26+
attributes:
27+
label: Page URL
28+
description: The translated page where you found the problem.
29+
placeholder: https://py.sdk.modelcontextprotocol.io/ja/servers/tools/
30+
validations:
31+
required: true
32+
33+
- type: textarea
34+
id: passage
35+
attributes:
36+
label: The passage
37+
description: Quote the translated text that's wrong, and the English it corresponds to if you have it.
38+
validations:
39+
required: true
40+
41+
- type: textarea
42+
id: problem
43+
attributes:
44+
label: What's wrong, or how it should read
45+
description: A wrong term, awkward phrasing, meaning that drifted from the English, tone that's off. If you know the better rendering, give it.
46+
validations:
47+
required: true
48+
49+
- type: dropdown
50+
id: native-speaker
51+
attributes:
52+
label: Are you a native or fluent speaker of this language?
53+
options:
54+
- "Yes"
55+
- "No"
56+
validations:
57+
required: true

.github/workflows/deploy-docs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ on:
1212
# docs pages include their code blocks from these files via `--8<--`, so a
1313
# change here changes the rendered site even when no .md file moves.
1414
- docs_src/**
15+
# translated pages, banners and the language registry ship in the site
16+
- i18n/**
1517
- mkdocs.yml
1618
- src/mcp/**
1719
- src/mcp-types/**

.github/workflows/docs-preview.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ on:
2121
paths:
2222
- docs/**
2323
- docs_src/**
24+
- i18n/**
2425
- mkdocs.yml
2526
- scripts/docs/**
2627
- pyproject.toml
@@ -137,7 +138,16 @@ jobs:
137138
# /preview-docs, still build with MkDocs. Both arms must write the site
138139
# to site/. Keep the detection in sync with build_site() in
139140
# scripts/build-docs.sh.
140-
- run: |
141+
#
142+
# DOCS_SITE_URL is the preview's Cloudflare branch-alias host (deploy
143+
# publishes to `--branch=pr-<N>`, served at pr-<N>.<project>.pages.dev),
144+
# so the absolute links the build bakes point at the preview instead of
145+
# production; empty when no Pages project is configured, which makes
146+
# build.sh fall back to the production site_url.
147+
- env:
148+
DOCS_SITE_URL: >-
149+
${{ vars.CLOUDFLARE_PAGES_PROJECT && format('https://pr-{0}.{1}.pages.dev', needs.authorize.outputs.pr_number, vars.CLOUDFLARE_PAGES_PROJECT) || '' }}
150+
run: |
141151
if [ -f scripts/docs/build.sh ]; then
142152
bash scripts/docs/build.sh
143153
else

.github/workflows/shared.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ jobs:
139139
- name: Check README snippets are up to date
140140
run: uv run --frozen scripts/update_readme_snippets.py --check
141141

142-
# `scripts/docs/build.sh` is the whole gauntlet: build_config.py fails on
142+
# `scripts/docs/build.sh` is the whole gauntlet: it opens with the fast,
143+
# no-network gates (a prose heading without a pinned anchor, an invalid
144+
# translation config or committed translation), then build_config.py fails on
143145
# nav entries without a page and pages without a nav entry, `zensical build
144146
# --strict` fails on broken .md links, `pymdownx.snippets: check_paths:
145147
# true` fails on a deleted `docs_src/` include, and the post-build steps

.gitignore

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,15 @@ venv.bak/
144144
# documentation
145145
/site
146146
/.worktrees/
147-
# Generated at build time by scripts/docs/ (the API reference tree and the
148-
# concrete Zensical config spliced from mkdocs.yml).
147+
# Local scratch notes are never part of the repo.
148+
/notes/
149+
# Generated at build time by scripts/docs/ (the API reference tree, the
150+
# concrete Zensical configs spliced from mkdocs.yml, and the staged per-language
151+
# docs trees).
149152
/docs/api/
150153
/mkdocs.gen.yml
154+
/mkdocs.*.gen.yml
155+
/.build/
151156

152157
# mypy
153158
.mypy_cache/

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ repos:
1111
hooks:
1212
- id: prettier
1313
types_or: [yaml, json5]
14+
# The translation tool writes each language's state.json and nav.yml
15+
# in its own stable format; a formatter rewrite would fight the generator.
16+
exclude: ^i18n/languages/[^/]+/(state\.json|nav\.yml)$
1417

1518
- repo: https://github.com/igorshubovych/markdownlint-cli
1619
rev: v0.45.0
@@ -25,6 +28,10 @@ repos:
2528
"/tool/markdown/lint",
2629
]
2730
types: [markdown]
31+
# Machine-translated pages are generated artefacts: corrections flow
32+
# through i18n/languages/<code>/{instructions.md,glossary.json}, never
33+
# through hand or linter edits to the pages themselves.
34+
exclude: ^i18n/languages/[^/]+/pages/
2835

2936
- repo: local
3037
hooks:

CONTRIBUTING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ pre-commit run --all-files
126126
- Add type hints to all functions
127127
- Include docstrings for public APIs
128128

129+
## Documentation and Translations
130+
131+
Documentation contributions are English only: the pages under `docs/` are the source of truth, and the translated documentation sites are generated from them, guided by the per-language style guides and glossaries under `i18n/languages/<lang>/`. Never edit the generated pages under `i18n/languages/<lang>/pages/`—the next translation run overwrites them. To fix a translation, change that language's `instructions.md` or `glossary.json` (or the English page, if that's where the problem is), and the fix carries into every future run. See [`i18n/README.md`](i18n/README.md) for the details.
132+
129133
## Pull Requests
130134

131135
By the time you open a PR, the "what" and "why" should already be settled in an issue. This keeps reviews focused on implementation.

docs/translations.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Translations {#translations}
2+
3+
This documentation is written in English. To make it useful to more people, we also publish it in a few other languages. Those editions are machine-translated, and this page explains what that means for you and how to help improve them.
4+
5+
## What's available {#whats-available}
6+
7+
Translated documentation is currently a **preview**, published in four languages: Simplified Chinese, Japanese, Korean and Brazilian Portuguese. Pick one from the language switcher at the top of any page.
8+
9+
Every translated page opens with a note saying it was machine-translated and linking to its English original. The API reference is not translated: every language site links to the single English one.
10+
11+
## English is the source of truth {#english-is-the-source-of-truth}
12+
13+
If a translated page and its English original disagree, the English page is correct. Two situations are called out on the page itself:
14+
15+
- A page that hasn't been translated yet shows the English text, with a note saying so.
16+
- A page whose English original changed after it was translated carries a warning that it may be behind, until the translation catches up.
17+
18+
## How the translations are made {#how-the-translations-are-made}
19+
20+
Translated pages are generated by a tool in this repository from the English pages under `docs/`, guided by two human-written inputs per language: a style guide (register, tone, typography, how to handle jokes and idioms) and a glossary (which terms stay in English, and the required and forbidden renderings for the rest). The generated text is never edited by hand. Every improvement goes into those inputs instead, so it survives the next time the pages are regenerated.
21+
22+
## Reporting a translation problem {#reporting-a-translation-problem}
23+
24+
Found a wrong term, an awkward sentence, or a translation that says something the English doesn't? [Open a translation issue](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=translation.yaml) with the language, the page and the passage; reports from native speakers are especially valuable, and maintainers track these with the `translation` label. If you know the fix, propose it directly as a pull request against that language's style guide or glossary under [`i18n/languages/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/i18n/languages) — the correction then shows up on every affected page the next time the translations are regenerated. Problems with the English text itself are fixed in the pages under `docs/`, like any other documentation change.

i18n/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Documentation translations
2+
3+
The English documentation under `docs/` is the source of truth. Everything in this directory either steers how those pages are machine-translated (human-authored inputs) or is the generated output of that process. The public-facing explanation lives at [`docs/translations.md`](../docs/translations.md); this file is for maintainers.
4+
5+
## Layout
6+
7+
- `languages.yml` — the language registry: one entry per language site (directory/URL code, native name, theme language, hreflang, enabled flag), the pages excluded from translation, and the model IDs the tool uses. The build and the tool both read it; nothing else hardcodes the language list.
8+
- `general-prompt.md` — the translation rules shared by every language. Sent with every translation request.
9+
- `banners/<code>/` — the notes staged onto pages in each language site: `disclosure.md` (machine-translated notice), `outdated.md` (may be behind the English page), `untranslated.md` (not translated yet), `stale.md` (translation withdrawn while it is refreshed — see below) and `english-only.md` (page deliberately kept in English). The build fills the `{english_url}` and `{translations_page_url}` placeholders; `banners/en/` is the fallback used when a language has no translated banner text.
10+
- `languages/<code>/instructions.md` — register, voice, humour policy and typography for one language. Human-authored.
11+
- `languages/<code>/glossary.json` — the termbase for one language: terms that stay in English, required renderings, and banned renderings. Human-authored, validated by the tool.
12+
- `languages/<code>/pages/` — the generated translations, mirroring the paths under `docs/`. Never edited by hand (see below).
13+
- `languages/<code>/state.json` — the generated record of what each translated page was built from.
14+
- `languages/<code>/nav.yml` — the generated map from each English sidebar label in `mkdocs.yml` (section titles and explicit `Label: page.md` entries) to its translation, plus the record of what it was built from. The language build swaps these into the sidebar; a label without a rendering stays English. Never edited by hand.
15+
16+
## The tool
17+
18+
Translation, validation and housekeeping are handled by the package under `scripts/docs/i18n/`, which provides these commands:
19+
20+
```bash
21+
uv run --frozen --group docs python scripts/docs/i18n <command> [options]
22+
```
23+
24+
- `status [--lang <code>]` — for each enabled language, list pages that are missing, outdated (English changed since translation, or prompt/glossary fingerprint changed), current, and removable (English page deleted), plus the state of its `nav.yml` label map. No network.
25+
- `translate --lang <code> [--pages a.md b.md] [--nav] [--all-missing] [--all-outdated] [--limit N] [--dry-run] [--no-verify]` — translate the selected pages (network), and with `--nav` (re)generate the language's `nav.yml` label map; `--all-missing`/`--all-outdated` also pick the map up when it is missing or outdated. Nothing is selected unless you pass `--pages`, `--nav` or an `--all-*` flag. `--limit` caps pages only. `--dry-run` prints the assembled prompts (the nav prompt included) and exits without calling the API.
26+
- `check [--lang <code>] [--pages ...]` — validate the committed translations and each language's `nav.yml`, no network; this is the CI gate for pull requests touching `i18n/`, and it only ever fails for a real integrity problem. Each page is checked as what it is: a *current* page (English source, glossary and instructions unchanged) gets the full structural validation against that English page; an *outdated* page is not measured against the English or the glossary that have since moved on — it only has to be well-formed by itself (front matter closes, every code fence closes, headings pin distinct well-formed anchors) and is reported so the next translation run refreshes it. Glossary rules are deliberately not enforced on an outdated page: a glossary edit is one of the things that makes a page outdated, and enforcing it offline would fail the gate until a fresh translation run landed. A page whose English source is gone is reported for `prune`, and a translation file with no `state.json` record is reported as untracked. For a `nav.yml` map, labels it lacks are merely behind and entries for labels the nav has dropped are reported as stale, neither failing the check (`--pages` narrows the run to pages only). English and glossary edits therefore never fail `check`; staleness is bannered by the build, not blocked here.
27+
- `verify --lang <code> --pages ...` — run the semantic review gate over the given pages (network). `translate` runs it on freshly generated text unless `--no-verify` is passed.
28+
- `prune --lang <code>` — delete translated pages whose English source no longer exists and drop them from the state file.
29+
- `stage --lang <code> [--site-url URL]` and `languages` — build machinery used by `scripts/docs/build.sh`: `stage` assembles a language's docs tree under `.build/i18n/<code>/docs/` (English pages, translations laid over them, status banners stamped in, and links into the API reference pointed at the English one — a language site links to the single English API reference rather than building its own), and `languages` prints the enabled codes. `--site-url` names the URL the site is served from (the build script derives it from `DOCS_SITE_URL`, defaulting to `mkdocs.yml`'s `site_url`), which the banners' English-page links and the API-reference links are built on. A translation whose links no longer resolve against the English tree (its English page has since renamed or dropped a target the translation still links) is withdrawn rather than staged — the English page is served with the `stale` banner and `stage` prints a warning line for it — so an outdated translation can delay a language site but never break its build.
30+
31+
Exit codes: `0` success, `1` validation or verification failures, `2` usage or configuration errors. The network commands read `ANTHROPIC_API_KEY` (or `ANTHROPIC_AUTH_TOKEN`) from the environment and fail fast if neither is set; the model IDs come from `languages.yml` and can be overridden with `MCP_DOCS_I18N_MODEL` and `MCP_DOCS_I18N_VERIFY_MODEL`. Run any command with `--help` for the full option list.
32+
33+
## Correcting a translation
34+
35+
Never edit the files under `languages/<code>/pages/` or a language's `nav.yml` — the next translation run overwrites them. Put the correction where the tool will pick it up:
36+
37+
- A wrong or inconsistent term → add or fix an entry in that language's `glossary.json`.
38+
- A recurring style or register problem (too formal, a joke that lands badly, an awkward loanword) → add a rule to that language's `instructions.md`, ideally with a short good/bad example.
39+
- English text that is ambiguous or hard to translate → fix the English page under `docs/`.
40+
41+
Editing the instructions or glossary changes their fingerprint, which marks that language's pages and nav map as outdated (see below), so the next `translate --all-outdated` run regenerates them with the fix in place. A wrong sidebar label is corrected the same way — through the glossary or instructions, then a `--nav` (or `--all-outdated`) run.
42+
43+
Readers report problems through the "Translation problem" issue form (`.github/ISSUE_TEMPLATE/translation.yaml`), which applies the `translation` label; triage each report into one of the three fixes above.
44+
45+
## How staleness works
46+
47+
`languages/<code>/state.json` records, for every translated page, the git commit and content hash of the English page it was translated from, a hash per content block, the fingerprints of `general-prompt.md`, that language's `instructions.md` and `glossary.json`, and the model and timestamp of the run. A page is a set of blocks: its front matter, the text before the first `##` heading, then one block per `##` section, so an edit to one section only re-translates that section and the untouched blocks are carried over verbatim from the previous translation.
48+
49+
`status` compares those records against the current English tree: a page is *missing* if it has no translation, *outdated* if any recorded hash or fingerprint no longer matches, *current* otherwise, and *removable* if its English source is gone. The docs build reads the same state to add a "may be behind the English page" note to outdated pages; pages with no translation are served from the English source with a "not translated yet" note. An outdated page whose translation still links a page the English tree has since renamed or removed is a special case: it would break the language site's strict build, so `status` reports it as "links broken by an English change — English shown until refreshed" and the build serves the English page with the `stale` banner until the next translation run rewrites it. Its `state.json` record already marks it outdated, so nothing else is needed — the next `translate --all-outdated` run refreshes it like any other outdated page.
50+
51+
The nav map follows the same contract at a smaller grain. `languages/<code>/nav.yml` records a hash of the ordered English label list plus the same three prompt-input fingerprints, so it is *missing* until first generated, *outdated* when a label is added, removed or reworded in `mkdocs.yml` or when the general prompt, instructions or glossary change, and *current* otherwise. A retranslation carries every label whose prompt inputs are unchanged over from the previous map byte-for-byte, so only new labels come from the model; a glossary or instructions edit re-asks the whole list. A label with no rendering is shown in English rather than failing the build.
52+
53+
## Adding a language
54+
55+
1. Add an entry to `languages.yml` (code, native name, theme language, hreflang, `enabled: true`).
56+
2. Create `languages/<code>/instructions.md` with these sections in this order: register, voice, humour and idioms, typography, terminology, and a provisional note. Keep it tight; it is sent verbatim with every request.
57+
3. Create `languages/<code>/glossary.json`: the `keep_in_source_language` list plus the seed `terms` entries.
58+
4. Optionally add `banners/<code>/` with translated banner text; without it the English banners are used.
59+
5. Run `status --lang <code>` to see what is missing, then `translate --lang <code> --all-missing`. The `pages/` tree, `state.json` and `nav.yml` are generated by that run; commit them.

0 commit comments

Comments
 (0)