Repository navigation
fix(docs): mark English fallbacks, localize the docs chrome, copy the page shown - #306
Merged
Merged
Conversation
… page shown A locale URL whose page has no source file in that locale serves the English page. It now says so: the title, description, body and TOC entries carry lang="en" (the document keeps the route locale, which the chrome is in), and a localized notice above the title reads "This page is not yet translated; showing English." Detection reuses translatedLocales(), which compares the page's source file against the English one — exact, because fumadocs copies the English file objects into every locale's storage. Fumadocs' interface strings (search, "On this page", "Choose a language", previous/next) and this app's own controls (Copy Markdown, Open, the notice) come from lib/ui-text.ts: English in code, one JSON table per locale. ui-text/zh-Hant.json is generated from ui-text/zh-Hans.json by gen-zh-hant (OpenCC s2twp) and covered by its --check, like every other zh-Hant file. Copy Markdown on a real translation copies that locale's Markdown, built at render time by getLLMText; English pages and fallbacks keep fetching the English .mdx URL, which stays the only Markdown surface. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
The legal links #304 added to the docs sidebar footer rendered "Legal", "Privacy" and "Terms" in English under every locale. They are interface copy, so they now come from lib/ui-text.ts like the rest of the chrome; the zh-Hant strings are regenerated from zh-Hans by gen-zh-hant. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
This was referenced Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #298
Head
0e5d348onmain@601bb37. It was staged on53ba2f3and rebased over #304 with no conflict. A second commit moves #304's sidebar Privacy/Terms links onto the same interface-copy table.What changed
A locale URL whose page has no source file in that locale serves the English page. Before this PR it did that with no notice, under the route locale's
langattribute, inside English chrome. Now:lang="en". The document's ownlangstays the route locale, because the chrome around the page (sidebar, search, buttons) is in that locale. A localized notice sits above the English title, in the route locale: "This page is not yet translated; showing English." Translated pages and English pages carry neither the notice nor any element-levellang.translatedLocales()/canonicalLocale(), which the page already called forcanonical,og:localeandinLanguage. fumadocs-core 16.8.12 builds each locale's storage by copying the English file objects in (new FileSystem(inherit)inloader) and letting real translations overwrite them. So a fallback page keeps the English file's ownpath(operate/backup.mdx, notoperate/backup.ja.mdx).page.localecan't tell the two kinds apart, because it is just the locale the page was looked up under.RootProvidernow getsi18n.translations, which covers search, "No results found", "On this page", "No Headings", "Choose a language", previous/next, theme and "Edit on GitHub". This app's own controls are localized too: Copy Markdown, Open, Open in GitHub/ChatGPT/Claude, and the notice. The English values are Fumadocs' defaults verbatim, so the English site renders the same strings.apps/docs/lib/ui-text.ts, which holds the English source and the types, plus one JSON table per locale underapps/docs/lib/ui-text/. The tables are UI copy in app code, the same shape asapp/not-found.tsxand the privacy and terms pages. They are notcontent/docs/translations.ui-text/zh-Hant.jsonis generated, not written.gen-zh-hant.mjsderives it fromui-text/zh-Hans.jsonwith the same OpenCCs2twpconverter and preset it uses for every*.zh-Hant.mdxandmeta.zh-Hant.json, and its--check(already a CI step) covers it.tscenforces the key set in both directions. The tables are keyed by the literal locale union fromlib/i18n.ts, so a locale with no table failstsc. A locale table that is missing a key failstsc, and so does one with an extra key./docs/PAGE.mdx, and that is byte-for-byte what is on screen.getLLMText()(the same shape/llms.mdx/docs/...serves for English) from the modulepage.data.load()had already loaded, and rendered into the page..mdxsurface stays English-only, as themarkdownUrlcomment records (fix(docs): emit the locale-independent .mdx URL from the docs page #211).app/[lang]/docs/layout.tsx. They are now threeui-textkeys, so English renders as before and every other locale gets its own string. For example, de shows "Datenschutz" and "Nutzungsbedingungen", and zh-Hant shows 隱私 and 條款 (generated).meta.zh-Hans.json(and a meta for every other locale) with a localized title. The rootmeta.jsontitle is the brand. The English labels left in the zh-Hans sidebar are the frontmatter titles of the 29 pages that have no zh-Hans source. A meta file cannot rename those; only the translation pass can. They are listed under Acceptance notes.Measured on the built site
Everything below was re-measured on the final head
0e5d348, after the rebase onto601bb37. The turbo build hash isffce3c965f8ceceaand the type-check hash isf943bb873ca4e2d0. The figures match the staged run on53ba2f3.Built HTML. All 632 prerendered docs pages were checked against an oracle derived from
content/docs/(a page is translated in a locale if and only if itsPAGE.LOCALE.mdxexists), with script bodies stripped:lang="en"on title/description/body/TOC, notice before the H1)lang)langThat is 323 fallback URLs and 230 translated locale URLs, with 0 findings. (The card counted 290 at
b754364; docs: remove locale pages that call ObjectOS Apache-2.0, fix search in CJK locales, fix the Quickstart anchor #302 and docs: make the upgrade, AI and help pages follow their public sources #303 have deleted more siblings since.)Negative control: running the same scan with the oracle inverted gives 553 of 553 findings, so it can fail on both branches.
An earlier version of the scan, which counted the RSC payload as markup, also went red. That was a measuring error, not a page defect.
Built-HTML check of the legal links: the sidebar foot shows "Legal"/"Privacy"/"Terms" on English and its own strings in each of the 7 locales.
Browser. Playwright and Chromium against
opennextjs-cloudflare preview(real workerd), on the Worker packaged from that build:html-has-lang,html-lang-valid,valid-lang) on one fallback page (operate/backup) and one translated page (build/agents) in each of the 7 locales, plus the English control: 0 violations on all 15 pages.html-lang-validpasses on all 15.valid-langpasses with 10 nodes on every fallback page. On the translated and English pages it is inapplicable, because they have no element-levellang.lang="english"on the H1 firesvalid-lang, and setting the html element'slangtodeutschfireshtml-lang-valid.enand every TOC link markeden.lang.operate/backup, de fallback and zh-Hans fallback each copy 8996 B, identical to/docs/operate/backup.mdx.build/agentscopy that locale's Markdown: 9510 B / 8138 B / 8206 B / 10535 B, starting with "Agents sind die KI-Assistenten", "Agents 是供你的终端用户对话", "Agents 是供你的終端使用者對話" and "エージェントは、エンドユーザーが" respectively.Cost of the inlined Markdown. 1,696,702 B across the 230 translated pages (mean 7.4 KB per page), which is 4.51% of those pages' HTML. The same bytes appear again in their
.rscfiles. English and fallback pages carry none. These are static prerender assets, not Worker code.wrangler deploy --dry-runon this build reportsTotal Upload: 49082.42 KiB / gzip: 7586.69 KiB, against the CI budget of 61440 KiB. No delta is claimed, because the CI comment documents a few KiB of same-tree noise.Gates
0e5d348.node apps/docs/scripts/gen-zh-hant.mjs --check✓ zh-Hant: 62 generated file(s) match the zh-Hans sources byte for byte.pnpm turbo run type-check --continue --force✓ Types generated successfully(fumadocs-mdx && next typegen && tsc --noEmit)pnpm turbo run build --force(NEXT_PRIVATE_STANDALONE=true)✓ Generating static pages using 3 workers (1052/1052)check-locale-surface.mjs✓ every advertised URL has a source file and every source file is advertised; …check-positioning.mjs✓ positioning: 4 copies equal their constants; the brand is right in 659 pages and 2 llms bodies; …check-search-locales.mjs✓ search locales: all 8 locales answer 200, find "permissions", find every own page by its title, and find nothing for a noncepnpm turbo run test --force✓ 10 self-test(s) passedsmoke-docs.mjs --base(local preview)✓ smoke: 4 page(s) rendered against http://127.0.0.1:8798, negative control demonstrated redwrangler deploy --dry-run)Total Upload: 49082.42 KiB(budget 61440)check-translations.mjs✓ translations gate passedcheck-translation-output.mjs --self-test/--files✓ translation output gate passed (98 pre-existing finding(s) reported)check-translation-ownership.mjs --actor … --files(workflow argv:git diff --name-status --no-renames origin/main...HEAD, withTRANSLATION_BOT_LOGINset, human actor)✓ 14 file(s) changed, no translation artifacts touched.Reverse checks (one-off, nothing left in the tree)
These ran on the staged commit, before the rebase. All three legs were run from the committed tree. Each was restored with
git checkout HEAD --, and the restore was proven bygit diff HEADbeing empty and by matching blob hashes.staleKeyinui-text/de.jsonmakestscexit 2 (TS2345 atlib/ui-text.ts:95).notTranslatedfromui-text/de.jsonmakestscexit 2.ui-text/zh-Hant.json(搜尋 → 搜索) makesgen-zh-hant --checkexit 1 withdiffers from generator output: apps/docs/lib/ui-text/zh-Hant.json.tscand--checkboth exit 0.Acceptance notes
English sidebar labels left in zh-Hans. All 29 are frontmatter titles of untranslated pages, which only the translation pass can close:
zh-Hant has the same 29. ja, de, es, fr and ko have 53 each.
Still English, and outside Fumadocs'
TranslationsAPI. Fumadocs-ui 16.8.12 hardcodes these accessible names:Open Search,Open Sidebar,Collapse Sidebar,Toggle Theme,Copy Anchor LinkandCopy Text. They render under every locale. Fixing them needs a fumadocs-ui patch or slot overrides; this is reported for a follow-up.Page-tree names are not marked. The sidebar, breadcrumb and prev/next names of untranslated pages are English with no
lang="en". axe has no rule for language of parts, so it stays green on them. Reported for a follow-up.Not changed by this PR:
.mdxURL, because the Markdown surface is English-only.app/not-found.tsxhas no zh-Hant entry, so zh-Hant shows the English 404 text.AGENTS.md is not updated. I left the map and the zh-Hant "never hand-edit" line unchanged so this PR stays off the governed surface.
gen-zh-hant --checkenforces the rule in CI regardless. Its "62 of 79" Simplified coverage count is stale (50 of 79 at53ba2f3).Local
next startis not usable for verification. With this build,next start307-loops unprefixed English URLs, because the middleware rewrite is answered as an external redirect. The OpenNext preview, which is what CI's smoke step boots, serves them 200, so verification used the preview.Generated by Claude Code