Skip to content

docs: localize fumadocs' accessible names, mark English page-tree entries, zh-Hant 404, Open-in links follow the page shown - #314

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #305

Locale pages still announced fumadocs' built-in English accessible names, named untranslated pages in English with no language marked, served an English 404 to Traditional Chinese readers, and sent "Open in ChatGPT / Claude" on a translated page to the English Markdown. This PR fixes all four. Same defect family as #298, and built on its lib/ui-text and its fallback detection.

What changed

1. Accessible names come from lib/ui-text in every locale. fumadocs-ui 16.8.12 hard-codes nine names as English literals, outside its Translations API: Open Search, Toggle Theme, Open Sidebar, Collapse Sidebar, Copy Anchor Link, Copy Text, Copied Text, Toggle Menu, and Radix's default "Main" on the legal pages' header. patches/fumadocs-ui@16.8.12.patch (pnpm patchedDependencies, next to the existing fumadocs-core pin) makes each component read its name from the i18n context it already reads for text.search. The default is the old literal, so English is unchanged.

  • Eight of the keys are a backport of upstream's own fix: fumadocs-ui 16.9.0 ships the same key names (searchOpen, themeToggle, sidebarOpen, sidebarCollapse, headingCopyAnchor, codeBlockCopy, codeBlockCopied, menuToggle) with the same defaults. I checked this by unpacking the published tarball.
  • navMain is the one key upstream does not have. It was still unlocalized in 16.16.2.
  • Why a patch and not slot overrides: I checked each name against the DocsLayout and HomeLayout slot API first. None can be reached everywhere it renders without re-implementing the component that renders it. For example, the collapsed-sidebar panel imports SearchTrigger directly, and both "Collapse Sidebar" buttons and the drawer's "Open Sidebar" live inside the Sidebar slot, which is a fork of about 200 lines. The patch header has the analysis name by name, plus when the patch can be deleted (on upgrading to 16.9.0 or later).
  • lib/ui-text.ts adds the nine strings, and fumadocsTranslations() passes them to RootProvider. The six translated tables gain them, and ui-text/zh-Hant.json is regenerated by gen-zh-hant.

2. Untranslated page-tree entries carry lang="en". app/[lang]/docs/layout.tsx transforms the locale's page tree before handing it to DocsLayout. That one transform reaches the sidebar, the breadcrumb and the previous/next footer.

3. zh-Hant 404 text. The 404 copy moved from a constant in app/not-found.tsx into lib/ui-text (notFound). The Traditional string is now generated from the Simplified one by gen-zh-hant, and --check covers it like every other zh-Hant string. The exact-key table type makes every locale carry the string.

4. "Open in ChatGPT / Claude" follows the page shown. I chose to point the assistant at the shown locale's content, matching #298's Copy Markdown rule:

  • English pages and fallbacks still send the English .mdx, which is the page on screen.
  • A real translation sends the translated page's own URL (/ja/docs/...).
  • Inlining the locale Markdown into the prompt was measured and rejected. Across the 230 locale pages, the query string runs from 2089 to 25630 characters, with 165 over 8 KiB, and neither assistant publishes a URL limit. The .mdx surface stays English-only, as AGENTS.md rule 1 and check-locale-surface require.

Branch and rebase

Measurements (built HTML, JS off)

main cec227a this PR on cec227a (1f5ea81) this PR on 5d2f837 (4da46c5)
English chrome names on locale pages (per locale, 82 pages) 1375–1387 0 0
untranslated page-tree entries without lang="en", zh-Hans / zh-Hant 687 0 (of 687) 0 (of 717)
the same, ja / de / es / fr / ko (each) 920 0 (of 920) 0 (of 950)
translated entries wrongly marked lang="en" 0 0 0
zh-Hant 404 text English 找不到此頁面。 找不到此頁面。
  • Oracle: "untranslated" is read from the content tree (whether a SLUG.LOCALE.mdx file exists), not from the app's own detection.
  • Coverage: all 7 locales × 82 pages for names, and every locale docs page for the tree. Folder names and breadcrumb items are all translated, with 0 English among 639 folder buttons and 95 breadcrumb items per locale, on both tips.
  • RSC payload on /de/docs/operate/backup, counting lang="en" spans:
  • Hydrated pages: checked in a browser on the Worker preview (opennextjs-cloudflare preview) at 4da46c5, for every locale, on one fallback and one translated page:
    • 0 English names, including after collapsing the sidebar, opening the drawer and the search dialog, and copying a code block ("Text kopiert" / "已复制文本").
    • Tree marks correct, and 0 hydration errors.
    • Open-in prompts read .../docs/operate/backup.mdx on the fallback and .../LOCALE/docs/build/data on the translation.
    • The 404 shows the locale text with lang set in all 8 locales.
    • The legal header on /de/privacy announces Hauptnavigation, Suche öffnen, Menü ein- oder ausblenden, Design wechseln and Sprache wählen.
  • English is unchanged (measured against cec227a): the served HTML outside the RSC payload and asset hashes matches main's byte for byte on /docs/build/data and /privacy. On /docs/operate/backup the only difference is one shiki token colour inside a code block (see Acceptance notes).

Verification at 4da46c5 (rebased tip)

  • pnpm install --frozen-lockfile: exit 0
  • pnpm turbo run type-check --continue --force: 1 successful
  • pnpm turbo run build --force (NEXT_PRIVATE_STANDALONE=true): 1 successful, 1038/1038 static pages
  • pnpm turbo run test --force: ✓ 10 self-test(s) passed
  • gen-zh-hant --check: ✓ 60 generated file(s) match the zh-Hans sources byte for byte
  • check-locale-surface, check-positioning: ✓
  • check-search-locales: ✓ all 8 locales answer 200, find "permissions", find every own page by its title within the first 3 pages, and find nothing for a nonce
  • translations, with the workflow argv (git diff --name-status --no-renames origin/main...HEAD, 15 files):
    • Ownership, actor hotlong, bot login unset: reports, exit 0.
    • Ownership, actor hotlong, bot login set: ✓ no translation artifacts touched.
    • Control, actor = the bot login: ✗ "translation PRs may only touch translation artifacts", exit 1, as it should.
    • Freshness ✓; Output --files ✓ (256 pre-existing findings reported, from main's corpus; this diff touches no content); Output self-test ✓.
  • check-node-floor (+ self-test) and the half-state sweeper self-test pass.
  • Worker: opennextjs-cloudflare build --skipNextBuild plus wrangler deploy --dry-run gives Total Upload 47077.66 KiB, under the 61440 KiB budget.
  • smoke-docs.mjs (Docs polish from the 2026-10-06 audit: muted-text contrast, the License table on phones, FAQ formatting, search ranking, glossary order, licence/license spelling #301's version) against the local preview: ✓ 4 page(s) rendered, negative control demonstrated red, search answered in 8 locale(s) and found nothing for a nonce.
  • Reverse check, at 1f5ea81 (the commit is patch-identical after the rebase):
    • Removing navMain from fumadocsTranslations() turns tsc red with TS2741 "Property 'navMain' is missing … required in type 'FumadocsTranslations'". Only the patched .d.ts requires that key.
    • Restored with git checkout HEAD: blob equals HEAD and git diff HEAD is empty.
  • axe 4.14.0 ran on one fallback and one translated page per locale. It reports no violation this change introduces: every violation it lists also appears on the English page with the same node count.

Screenshots (1440 and 390), re-taken at 4da46c5 because #301 changed the header:

fumadocs draws no visual tooltip, so the de shots overlay each control's accessible name as a labelled "(aria-label)" annotation.

Acceptance notes

  • Not filed: one shiki token (a ; in operate/backup's first code block) is coloured #032F62 on main's build and #24292E on this one, from identical content. This change does not touch highlighting. The evidence is two builds only.
  • Not filed: the prompt sentence "Read URL, I want to ask questions about it." is English on every locale. Upstream 16.9.0 makes it a translation key (pageActionsOpenInLLMPrompt). Carrier: none.
  • Out of scope, reported to the seat (same family, not this card's four items): /LOCALE/privacy and /LOCALE/terms on de, ja, es, fr, ko and zh-Hant render the English body inside html lang="LOCALE" with no lang="en" anywhere (0 on each page). zh-Hant gets English there rather than a generated Traditional version of the zh-Hans text.
  • No CI gate in this PR pins the scans above. staged/issue-308 ("read the built pages for English chrome names and unmarked page-tree entries") is that gate's own card.

Generated by Claude Code

objectstack-fleet Bot and others added 10 commits October 6, 2026 10:36
…ds or fewer

Adds a `seoTitle:` frontmatter line to every English page whose built
`<title>` read as one or two words plus the ` | ObjectOS` suffix, so the
tab/result title carries the terms a reader searches for while the H1,
sidebar and breadcrumb keep the short noun (#166's mechanism).

- 67 pages: each gains exactly one line; no `title`, `description` or
  body line changes.
- Every title leads with the page's own specific term, lifted from its
  description and headings, and renders at 52–60 characters including the
  suffix (measured on the built HTML).
- The three duplicated titles (Approvals, Dashboards, Notifications —
  each used by a build/configure page and a use page) are now distinct.
- English only. Locale siblings are translation artifacts that a
  non-translator commit may not modify (AGENTS.md, Translation workflow;
  check-translation-ownership.mjs); the translation pass carries the new
  key over, and the output report lists the 163 siblings now missing it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…AQ headings, title-weighted per-locale search, consistency pass

- Light-mode muted foreground to hsl(0 0% 40%); code comments recoloured in
  both shiki themes.
- Tables get an always-drawn scrollbar and a scroll-driven trailing fade.
- FAQ and License FAQ questions become headings.
- /api/search builds one locale's index on that locale's first search and
  weights title > heading > text; check-search-locales gains own-title-buried;
  smoke-docs asks /api/search in every locale with a nonce control.
- Consistency: one data-residency table, one license-validation sentence,
  ObjectSchema.create, "license" spelling, Configure title, glossary order plus
  AI seat and Position, logo to the docs home, a translated llms.txt example,
  three unsourced configure/ai claims removed (with their locale siblings),
  release-following lines pointed at a populated feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…comments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ix apiMethods primitives

The Views page declared views inside a defineObject call under an
object-level `view` key. ObjectStack provides no defineObject, and
ObjectSchema.create rejects `view` as an unknown key. Its views are a
defineView container ({ list, listViews, form, formViews }, each view
bound through `data`) registered on the stack with `views: [...]`, which
both samples now show; both parse with @objectstack/spec 17.6.0.

The REST API page's allowed apiMethods values drop the eight retired ones
the spec strips at parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ype-check

`next typegen` loads next.config.mjs, and fumadocs-mdx 15.0.7's createMDX()
starts init() without awaiting it (dist/next/index.js:14-20). init rewrites
every .source/*.ts with fs.writeFile (dist/core-DlDe_Eze.js:232-236), which
truncates first. typegen ends in process.exit(0), so it can exit inside that
window and leave .source/server.ts empty for tsc: TS2306, CI run 37478051451.

Running the fumadocs-mdx CLI after typegen makes the CLI, which awaits its
writes, the last writer before tsc. typegen does not read .source.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ries, zh-Hant 404, Open-in links follow the page shown

- patches/fumadocs-ui@16.8.12.patch: the nine accessible names fumadocs-ui
  16.8.12 hard-codes in English (Open Search, Toggle Theme, Open/Collapse
  Sidebar, Copy Anchor Link, Copy/Copied Text, Toggle Menu, and Radix's
  "Main") read from its i18n context with the old literal as default. Eight
  keys are a backport of upstream 16.9.0's own names; lib/ui-text supplies
  all nine through RootProvider in every locale.
- app/[lang]/docs/layout.tsx: page-tree entries (sidebar, breadcrumb,
  prev/next footer) for pages a locale has no translation of carry
  lang="en", using the docs page's own translatedLocales detection.
- app/not-found.tsx: the 404 copy moves into lib/ui-text (notFound), so the
  zh-Hant string is generated from zh-Hans by gen-zh-hant and checked by
  --check like every other Traditional string.
- Open in ChatGPT / Claude on a translated page sends the assistant to the
  translated page itself; English pages and fallbacks keep the English .mdx.

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