Skip to content

fix(docs): mark English fallbacks, localize the docs chrome, copy the page shown - #306

Merged
hotlong merged 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1
Oct 6, 2026
Merged

hotlong merged 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #298

Head 0e5d348 on main @ 601bb37. It was staged on 53ba2f3 and 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 lang attribute, inside English chrome. Now:

  1. Fallback pages say what they are. The title, description, body and every TOC entry carry lang="en". The document's own lang stays 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-level lang.
  2. Fallback detection is exact. It reuses translatedLocales() / canonicalLocale(), which the page already called for canonical, og:locale and inLanguage. fumadocs-core 16.8.12 builds each locale's storage by copying the English file objects in (new FileSystem(inherit) in loader) and letting real translations overwrite them. So a fallback page keeps the English file's own path (operate/backup.mdx, not operate/backup.ja.mdx). page.locale can't tell the two kinds apart, because it is just the locale the page was looked up under.
  3. The chrome is localized in all 8 locales through Fumadocs' own channel. RootProvider now gets i18n.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.
    • All the strings live in apps/docs/lib/ui-text.ts, which holds the English source and the types, plus one JSON table per locale under apps/docs/lib/ui-text/. The tables are UI copy in app code, the same shape as app/not-found.tsx and the privacy and terms pages. They are not content/docs/ translations.
    • ui-text/zh-Hant.json is generated, not written. gen-zh-hant.mjs derives it from ui-text/zh-Hans.json with the same OpenCC s2twp converter and preset it uses for every *.zh-Hant.mdx and meta.zh-Hant.json, and its --check (already a CI step) covers it.
    • tsc enforces the key set in both directions. The tables are keyed by the literal locale union from lib/i18n.ts, so a locale with no table fails tsc. A locale table that is missing a key fails tsc, and so does one with an extra key.
  4. Copy Markdown copies the page being shown.
    • English pages and fallbacks still fetch the English /docs/PAGE.mdx, and that is byte-for-byte what is on screen.
    • On a real translation, the button now copies that locale's Markdown. It is built by getLLMText() (the same shape /llms.mdx/docs/... serves for English) from the module page.data.load() had already loaded, and rendered into the page.
    • No new URL is published. The .mdx surface stays English-only, as the markdownUrl comment records (fix(docs): emit the locale-independent .mdx URL from the docs page #211).
  5. The sidebar's legal links (docs: a short /docs description, legal-page metadata, share cards that fit, no MDX comments in llms bodies #304) are localized too. The labels "Privacy" and "Terms" and the nav name "Legal" were English literals in app/[lang]/docs/layout.tsx. They are now three ui-text keys, 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).
  6. Sidebar labels: no meta file changed. Every folder already has a meta.zh-Hans.json (and a meta for every other locale) with a localized title. The root meta.json title 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 onto 601bb37. The turbo build hash is ffce3c965f8cecea and the type-check hash is f943bb873ca4e2d0. The figures match the staged run on 53ba2f3.

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 its PAGE.LOCALE.mdx exists), with script bodies stripped:

locale fallback URLs (notice + lang="en" on title/description/body/TOC, notice before the H1) translated URLs (no notice, no element lang) localized chrome
zh-Hans 29 / 29 50 / 50 79 / 79
zh-Hant 29 / 29 50 / 50 79 / 79
ja 53 / 53 26 / 26 79 / 79
de 53 / 53 26 / 26 79 / 79
es 53 / 53 26 / 26 79 / 79
fr 53 / 53 26 / 26 79 / 79
ko 53 / 53 26 / 26 79 / 79
en — 79 English pages: no notice, no element lang —

Browser. Playwright and Chromium against opennextjs-cloudflare preview (real workerd), on the Worker packaged from that build:

  • axe-core 4.14.0 (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-valid passes on all 15.
    • valid-lang passes with 10 nodes on every fallback page. On the translated and English pages it is inapplicable, because they have no element-level lang.
    • Negative control: setting lang="english" on the H1 fires valid-lang, and setting the html element's lang to deutsch fires html-lang-valid.
  • DOM after hydration, on 29 pages (1 fallback and 3 translated per locale, plus English):
    • Each fallback shows the notice in its route locale, before the H1, with the H1 and body marked en and every TOC link marked en.
    • None of the 21 translated samples has a notice or any element-level lang.
    • On every page, the Copy Markdown label and the "On this page" heading match the locale's table.
    • The browser console shows 0 errors on all of them, so there was no hydration mismatch.
  • Copy Markdown, read back from the clipboard:
    • English operate/backup, de fallback and zh-Hans fallback each copy 8996 B, identical to /docs/operate/backup.mdx.
    • The de, zh-Hans, zh-Hant and ja translations of build/agents copy that locale's Markdown: 9510 B / 8138 B / 8206 B / 10535 B, starting with "Agents sind die KI-Assistenten", "Agents 是供你的终端用户对话", "Agents 是供你的終端使用者對話" and "エージェントは、エンドユーザーが" respectively.
  • Screenshots:
    • Fallback and translated pages in zh-Hans and de, at 1440 and 390.
    • The ja search dialog: empty, with the 検索 placeholder; and with no results, showing 結果が見つかりませんでした.
    • The ja language picker, headed 言語を選択.

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 .rsc files. English and fallback pages carry none. These are static prerender assets, not Worker code. wrangler deploy --dry-run on this build reports Total 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

gate verdict line tree
All on 0e5d348.
gate verdict line
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 exit 0, ✓ Types generated successfully (fumadocs-mdx && next typegen && tsc --noEmit)
pnpm turbo run build --force (NEXT_PRIVATE_STANDALONE=true) exit 0, ✓ 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 nonce
pnpm turbo run test --force ✓ 10 self-test(s) passed
smoke-docs.mjs --base (local preview) ✓ smoke: 4 page(s) rendered against http://127.0.0.1:8798, negative control demonstrated red
Worker size (wrangler deploy --dry-run) Total Upload: 49082.42 KiB (budget 61440)
check-translations.mjs ✓ translations gate passed
check-translation-output.mjs --self-test / --files exit 0 / ✓ 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, with TRANSLATION_BOT_LOGIN set, 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 by git diff HEAD being empty and by matching blob hashes.

  • An extra key staleKey in ui-text/de.json makes tsc exit 2 (TS2345 at lib/ui-text.ts:95).
  • Deleting notTranslated from ui-text/de.json makes tsc exit 2.
  • Hand-editing ui-text/zh-Hant.json (搜尋 → 搜索) makes gen-zh-hant --check exit 1 with differs from generator output: apps/docs/lib/ui-text/zh-Hant.json.
  • After the restore, tsc and --check both 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:

  • Introduction, Why ObjectOS, Extend Existing Systems, Quickstart, Architecture
  • Packages
  • AI Service, Data Sources, Localization, Notifications, Runtime Configuration
  • Deployment, Docker, Kubernetes, Air-gapped Deployment
  • Operate, Production Readiness, Audit Logs, Backup and Disaster Recovery, Upgrade and Rollback, Troubleshooting
  • CLI Reference, Environment Variables, Runtime Capabilities, Security & Compliance
  • Changelog & Versioning, FAQ, Glossary, Support

zh-Hant has the same 29. ja, de, es, fr and ko have 53 each.

Still English, and outside Fumadocs' Translations API. Fumadocs-ui 16.8.12 hardcodes these accessible names: Open Search, Open Sidebar, Collapse Sidebar, Toggle Theme, Copy Anchor Link and Copy 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:

  • On a translated page, "Open in ChatGPT/Claude" still asks the model to read the English .mdx URL, because the Markdown surface is English-only.
  • The language switcher still lists all 8 locales.
  • The 404 copy table in app/not-found.tsx has 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 --check enforces the rule in CI regardless. Its "62 of 79" Simplified coverage count is stale (50 of 79 at 53ba2f3).

Local next start is not usable for verification. With this build, next start 307-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

claude added 2 commits October 6, 2026 10:18
… 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants