Automated translation has been off since GitHub retired GitHub Models on 30 July 2026 (every request now returns 410). The pipeline it was built on is intact — what is missing is a provider.
This is configuration, not code. .github/workflows/translate.yml is gated on the TRANSLATE_ENDPOINT repository variable: unset, every run skips cleanly. Setting it, plus TRANSLATE_MODEL and the TRANSLATE_API_KEY secret, turns the workflow on with no change to scripts/translate.mjs.
What to do
translations/README.md is the step-by-step — creating the Cloudflare account, minting a scoped API token, the curl smoke test, and the three repository settings. In short:
- Secret
TRANSLATE_API_KEY — the Cloudflare API token
- Variable
TRANSLATE_ENDPOINT — https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1/chat/completions
- Variable
TRANSLATE_MODEL — @cf/zai-org/glm-4.7-flash
Cloudflare Workers AI is OpenAI-compatible and takes the same bearer header the script already sends, which is why no code changes.
Why Cloudflare and not something else
Two options look right and are not, both already investigated:
- Azure Translator has the biggest free tier (F0, 2M characters/month) and cannot do this job. It takes no prompt, so the terminology glossary has nowhere to go, and its
textType is plain/html only — wikilinks and fenced code do not survive, and checkStructure rejects essentially every page.
- Azure Foundry Models would drop straight in but is pay-per-token with no free allowance.
An open-source project cannot carry a per-token bill, which is the constraint that picked Cloudflare: the Workers Free plan includes 10,000 Neurons/day, resetting 00:00 UTC.
The current backlog, and what the free tier can and cannot do
7 languages are configured in translations/languages.json. Only French is seeded:
| Language |
Pages present |
--dry-run says would translate |
fr French |
99 |
28 |
es Spanish |
0 |
104 |
de German |
0 |
104 |
zh-cn Chinese (Simplified) |
0 |
104 |
pt-br Portuguese (Brazil) |
0 |
104 |
it Italian |
0 |
104 |
ja Japanese |
0 |
104 |
That is ~650 page-translations. 10,000 Neurons/day is enough to track changes to docs/, not to seed a language from nothing — which is what the script's request budget and the workflow's nightly drain are already shaped around. So the expected split is:
- Turn the workflow on so
fr's 28 drifted pages are picked up and docs/ stops drifting further.
- Seed the six empty languages in bulk by hand.
translations/SEEDING.md has the full recipe and its gotchas — it is how the French tree was built.
Note the six empty languages are not currently broken for readers: quartz/scripts/rtd-build.mjs lays each language over a copy of docs/, so an untranslated page falls back to English rather than 404ing. Each also needs its own Read the Docs project (same repo, same branch, differing only in the Language setting) before it is reachable.
Acceptance
Note on --verify baseline
fr currently fails --verify on 20 pages (15 before #140, which changed English pages the French tree has not caught up with). That is the expected stale state, not a regression — those pages are queued in --dry-run and regenerate once a provider exists. Worth capturing the number before flipping the switch so the improvement is visible.
Everything that does not call a model still works today: --dry-run reports drift, and --verify/--relink are unaffected.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CVRiHn5P4C15Lu3LxGtN8g
Automated translation has been off since GitHub retired GitHub Models on 30 July 2026 (every request now returns
410). The pipeline it was built on is intact — what is missing is a provider.This is configuration, not code.
.github/workflows/translate.ymlis gated on theTRANSLATE_ENDPOINTrepository variable: unset, every run skips cleanly. Setting it, plusTRANSLATE_MODELand theTRANSLATE_API_KEYsecret, turns the workflow on with no change toscripts/translate.mjs.What to do
translations/README.mdis the step-by-step — creating the Cloudflare account, minting a scoped API token, thecurlsmoke test, and the three repository settings. In short:TRANSLATE_API_KEY— the Cloudflare API tokenTRANSLATE_ENDPOINT—https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1/chat/completionsTRANSLATE_MODEL—@cf/zai-org/glm-4.7-flashCloudflare Workers AI is OpenAI-compatible and takes the same bearer header the script already sends, which is why no code changes.
Why Cloudflare and not something else
Two options look right and are not, both already investigated:
textTypeis plain/html only — wikilinks and fenced code do not survive, andcheckStructurerejects essentially every page.An open-source project cannot carry a per-token bill, which is the constraint that picked Cloudflare: the Workers Free plan includes 10,000 Neurons/day, resetting 00:00 UTC.
The current backlog, and what the free tier can and cannot do
7 languages are configured in
translations/languages.json. Only French is seeded:--dry-runsays would translatefrFrenchesSpanishdeGermanzh-cnChinese (Simplified)pt-brPortuguese (Brazil)itItalianjaJapaneseThat is ~650 page-translations. 10,000 Neurons/day is enough to track changes to
docs/, not to seed a language from nothing — which is what the script's request budget and the workflow's nightly drain are already shaped around. So the expected split is:fr's 28 drifted pages are picked up anddocs/stops drifting further.translations/SEEDING.mdhas the full recipe and its gotchas — it is how the French tree was built.Note the six empty languages are not currently broken for readers:
quartz/scripts/rtd-build.mjslays each language over a copy ofdocs/, so an untranslated page falls back to English rather than 404ing. Each also needs its own Read the Docs project (same repo, same branch, differing only in the Language setting) before it is reachable.Acceptance
curlsmoke test intranslations/README.mdreturns a completiontranslate.ymlruns green on a push touchingdocs/**and opens/updates its commitnode scripts/translate.mjs fr --verifyno worse than before (see below)Note on
--verifybaselinefrcurrently fails--verifyon 20 pages (15 before #140, which changed English pages the French tree has not caught up with). That is the expected stale state, not a regression — those pages are queued in--dry-runand regenerate once a provider exists. Worth capturing the number before flipping the switch so the improvement is visible.Everything that does not call a model still works today:
--dry-runreports drift, and--verify/--relinkare unaffected.🤖 Generated with Claude Code
https://claude.ai/code/session_01CVRiHn5P4C15Lu3LxGtN8g