Skip to content

290 English-fallback locale URLs render English under lang="xx" with no notice, and the docs chrome (search, TOC, Copy Markdown, language picker) is English in every locale #298

Closed

Description

@objectstack-fleet

Found by the seat's site-wide audit of docs.objectos.ai on 2026-10-06, which built main @ b754364. Priority: P1. This affects accessibility (WCAG 3.1.1 and 3.1.2) and how honest the site is with readers.

What happens today

Pages that show English under another language's URL

  • 290 locale URLs fall back to English: 48 pages in each of de/es/fr/ja/ko, and 25 in each of zh-Hans/zh-Hant. They include index, why, quickstart, architecture, glossary, faq and security, whose siblings docs: one positioning constant, one gate, ObjectOS as Cloud or Enterprise (PR 1 of #171) #293 deleted.
  • They render English inside <html lang="de"> and so on, so screen readers read English in the wrong voice.
  • Cause: apps/docs/app/[lang]/layout.tsx:32 sets lang from the route segment.

What tells the reader

  • The language switcher always lists all 8 languages.
  • No page says "not translated yet".
  • The zh-Hans sidebar mixes English labels ("Runtime Configuration", "Data Sources", "Localization", "Notifications") in with Chinese ones.

Interface text

  • "Search", "On this page", "Copy Markdown" and "Choose a language" are English in every locale. root-provider.tsx passes no translations.
  • Copy Markdown on a translated page copies the English source.

Already correct (keep it): the canonical points to English; fallback URLs stay out of the sitemap; unknown slugs return a localized 404; and pages whose siblings were deleted show the current English, not stale text.

Done looks like

  • A fallback page renders its content with lang="en" on the content element, and shows a short localized callout ("This page is not yet translated; showing English").
  • The interface text is localized for the 8 locales, using fumadocs' i18n translations.
  • Sidebar labels come from the locale's meta where one exists, and the gap is listed where one does not.
  • Copy Markdown copies the page being shown.
  • Measured on the built site:
    • every fallback URL carries lang="en" on its content and the callout;
    • no translated page carries the callout;
    • axe html-lang-valid and valid-lang are clean on one fallback page and one translated page per locale.
  • Screenshots of one fallback page and one translated page in zh-Hans and de, at 1440 and 390.

Surface

Order

After the P0 and P1 instruction-fix cards.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop, repo:objectos seat
    Session: session_01FeA1nwBz1ohH65dvffUGKr
    Account: hotlong (the seat's linked user, as GET /user answers it; always the card's assignee)
    Branch: claude/pm-dispatch-objectos-ju9td1, the session's one designated push branch. #299 is the next PR on it. This card's work is therefore staged: built and verified in its own worktree on a local branch cut from origin/main, then rebased and pushed after #299 lands. Queue change: #298 now goes before #167, because #167 and #299 both edit English frontmatter (titles and descriptions sit on adjacent lines), while #298's surface is disjoint from #299's.
    Worktree: objectos-issue-298
    Domain: n/a (single-lane repo)
    Seat: repo:objectos (objectstack-ai/objectstack#9831)
    File surface:


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 298,
    "status": "staged",
    "branch": "staged/issue-298",
    "worktree": "/home/user/objectos-issue-298",
    "final_commit": "cbc88b3",
    "base": "53ba2f3",
    "pr": null,
    "pr_body_draft": "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/pr-body.md",
    "session": "session_01FeA1nwBz1ohH65dvffUGKr",
    "premise_still_valid": true,
    "summary": "Fallback pages (a locale URL whose page has no source file in that locale): the title, description, body and every TOC entry now carry lang=en, the document keeps the route locale, and a localized notice ("This page is not yet translated; showing English.") sits above the H1, in the route locale. Detection reuses translatedLocales() and canonicalLocale(), which is exact: fumadocs-core 16.8.12 copies the English file objects into every locale storage, so a fallback keeps the English file path. Fumadocs chrome strings (10 Translations keys) and this app's own controls are localized for all 8 locales through RootProvider i18n.translations and lib/ui-text.ts. ui-text/zh-Hant.json is generated from zh-Hans.json by gen-zh-hant (OpenCC s2twp) and covered by its --check. Copy Markdown copies the page shown: English pages and fallbacks fetch the English .mdx; a real translation copies that locale's getLLMText Markdown, inlined at build time, with no new URL surface. No meta file was changed, because every folder title is already localized; the English zh-Hans sidebar labels are the titles of 29 untranslated pages and are listed as the gap.",
    "files": [
    "apps/docs/app/[lang]/docs/[[...slug]]/page.tsx",
    "apps/docs/app/[lang]/layout.tsx",
    "apps/docs/app/[lang]/root-provider.tsx",
    "apps/docs/components/ai/page-actions.tsx",
    "apps/docs/lib/ui-text.ts (new)",
    "apps/docs/lib/ui-text/{zh-Hans,ja,de,es,fr,ko}.json (new, hand-kept UI copy)",
    "apps/docs/lib/ui-text/zh-Hant.json (new, GENERATED by gen-zh-hant)",
    "apps/docs/scripts/gen-zh-hant.mjs"
    ],
    "measurements": {
    "built_html": "All 632 prerendered docs pages at cd2af9f (turbo build hash 509d46f8e99c4078, identical at cbc88b3), checked against a content-tree oracle with script bodies stripped. Fallback pages with the notice before the H1 and lang=en on title, description, body and TOC: zh-Hans 29/29, zh-Hant 29/29, ja/de/es/fr/ko 53/53 each, 323 in total. Translated pages with no notice and no element lang: zh-Hans 50/50, zh-Hant 50/50, ja/de/es/fr/ko 26/26 each, 230 in total. Localized chrome on 553/553 locale pages. 79 English pages carry no notice and no element lang. 0 findings. Negative control with the oracle inverted: 553/553 findings.",
    "axe": "axe-core 4.14.0 rules html-has-lang, html-lang-valid and valid-lang on one fallback page (operate/backup) and one translated page (build/agents) per locale, plus the English control: 0 violations on 15/15 pages. html-lang-valid passes on 15/15. valid-lang passes with 10 nodes on each of the 7 fallback pages and is inapplicable on translated and English pages, which have no element lang. Negative control: h1 lang=english fires valid-lang, and html lang=deutsch fires html-lang-valid.",
    "browser_dom": "29 pages after hydration on the OpenNext preview (workerd): 1 fallback and 3 translated per locale, plus English. Each fallback has the notice in its route locale before the H1, with the H1, body and all TOC links marked en. 21/21 translated samples have no notice and no element lang. The Copy Markdown label and the On-this-page title match the locale table on every page. 0 console errors, so no hydration mismatch.",
    "copy_markdown": "Clipboard read-back. English operate/backup, the de fallback and the zh-Hans fallback each copy 8996 B, identical to /docs/operate/backup.mdx. The de, zh-Hans, zh-Hant and ja build/agents translations copy that locale's Markdown: 9510 B, 8138 B, 8206 B and 10535 B.",
    "copy_markdown_cost": "Inlined Markdown totals 1,696,702 B across the 230 translated pages (mean 7.4 KB per page), which is 4.53% 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.",
    "worker_size": "wrangler deploy --dry-run on this build: Total Upload: 49104.80 KiB / gzip: 7593.60 KiB, against a budget of 61440. No delta is claimed, because same-tree noise is a few KiB per ci.yml.",
    "fallback_count_drift": "The card counted 290 fallback URLs at b754364. At 53ba2f3 there are 323 (29 x 2 + 53 x 5), after #302 and #303 deleted more siblings."
    },
    "gates": [
    "gen-zh-hant --check @cbc88b3: "✓ zh-Hant: 62 generated file(s) match the zh-Hans sources byte for byte." exit 0",
    "pnpm turbo run type-check --continue --force: exit 0 (fumadocs-mdx, next typegen, tsc --noEmit). Hash 8c0dd8ec38b60655 is identical at cbc88b3",
    "NEXT_PRIVATE_STANDALONE=true pnpm turbo run build --force: exit 0, 1052/1052 static pages. Hash 509d46f8e99c4078 is identical at cbc88b3. os-verify-lock VERDICT command-exit 0",
    "check-locale-surface: "✓ every advertised URL has a source file and every source file is advertised; ..." exit 0",
    "check-positioning: "✓ positioning: 3 copies equal the constant; the brand is right in 659 pages and 2 llms bodies; ..." exit 0",
    "check-search-locales: "✓ search locales: all 8 locales answer 200, find permissions, find every own page by its title, and find nothing for a nonce" exit 0",
    "pnpm turbo run test --force @cbc88b3: "✓ 10 self-test(s) passed". os-verify-lock VERDICT command-exit 0",
    "smoke-docs.mjs --base http://127.0.0.1:8798 (opennextjs-cloudflare preview): "✓ smoke: 4 page(s) rendered ..., negative control demonstrated red" exit 0",
    "Worker size: wrangler deploy --dry-run reports Total Upload 49104.80 KiB (budget 61440)",
    "check-translations @cbc88b3: "✓ translations gate passed" exit 0",
    "check-translation-output --self-test: exit 0. --files changed-list: "✓ translation output gate passed (98 pre-existing finding(s) reported)" exit 0",
    "check-translation-ownership with TRANSLATION_BOT_LOGIN set and a human actor: "✓ 13 file(s) changed, no translation artifacts touched." exit 0. Unset: exit 0",
    "check-node-floor: "✅ Every declared floor clears ..." exit 0. check-half-states --self-test: "1551 cases pass" exit 0",
    "Control-byte scan of the changed files: 0 hits. check-governed-merges --test on the 13 paths: 0 of 13 governed"
    ],
    "tests": "All of the gates above, plus three one-off reverse checks run from the committed tree, each restored with git checkout HEAD -- (git diff HEAD empty, blob hashes match). (1) An extra key in ui-text/de.json: tsc exit 2 (TS2345 at lib/ui-text.ts:95). (2) A missing key in ui-text/de.json: tsc exit 2. (3) A hand edit to ui-text/zh-Hant.json: gen-zh-hant --check exit 1, "differs from generator output". After restore, tsc and --check both exit 0. The build-scan and axe negative controls are described under measurements.",
    "screenshots": [
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/zh-Hans-fallback-1440.png",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/zh-Hans-fallback-390.png",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/zh-Hans-translated-1440.png",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/zh-Hans-translated-390.png",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/de-fallback-1440.png",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/de-fallback-390.png",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/de-translated-1440.png",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/de-translated-390.png",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/ja-search-dialog-empty-1440.png (placeholder 検索)",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/ja-search-dialog-noresult-1440.png (結果が見つかりませんでした)",
    "/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-298/shots/ja-language-picker-1440.png (heading 言語を選択)"
    ],
    "deviations": [
    "Item 3: zero meta.LOCALE.json edits. Every folder title already has a localized meta in every locale, and the root meta title is the brand. The English zh-Hans labels are frontmatter titles of 29 untranslated pages, which no meta file can rename; meta.LOCALE.json files are also translation artifacts that the ownership gate assigns to the translation account. The gap is listed in the PR body instead.",
    "I made an AGENTS.md edit (adding lib/ui-text to the map and naming ui-text/zh-Hant.json in the never-hand-edit rule), then dropped it from the final commit. check-governed-merges flagged AGENTS.md as governed, which would turn this product PR into a Tier-H human merge. gen-zh-hant --check enforces the rule anyway.",
    "Commit trailers are model-free (Co-Authored-By: Claude, plus Claude-Session) per the agent contract. The harness reminder asked for a model-named Co-Authored-By; that reminder is reported here, not followed.",
    "Browser checks ran on opennextjs-cloudflare preview (workerd), not next start. With this build, next start 307-loops unprefixed English URLs because the middleware rewrite is answered as an external redirect; the preview, which CI smoke uses, serves them 200.",
    "Copy Markdown on translations inlines the text instead of adding a locale .mdx URL, to keep the English-only Markdown surface recorded in page.tsx (#211). The cost is measured above.",
    "The build, post-build gates, browser checks and Worker size ran on cd2af9f. The final cbc88b3 differs only by the AGENTS.md drop; the turbo build and type-check hashes are identical, and no gate reads AGENTS.md. The cheap gates were re-run on cbc88b3."
    ],
    "open_questions": [
    {
    "question": "Copy Markdown on a translated page: keep the locale Markdown inlined in the page, or publish a locale-aware .mdx surface?",
    "options": [
    "A: keep inline. No new URL, the Markdown surface stays English-only, and the cost is 1.70 MB of static HTML across 230 pages plus the same in .rsc.",
    "B: publish /LOCALE/docs/PAGE.mdx and fetch on click. This reverses the English-only Markdown policy recorded in page.tsx (#211) and needs robots, sitemap and llms decisions."
    ],
    "recommendation": "A, because it delivers what the card asks for without changing a site-wide surface, and it can be swapped for B later in one component if the maintainer reverses the policy."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: the built /de/docs/build/agents (and every locale page) serves English accessible names inside html lang=de: aria-label "Open Search" x2, "Collapse Sidebar" x2, "Open Sidebar", "Toggle Theme", "Copy Anchor Link" x9, "Copy Text" x4. fumadocs-ui 16.8.12 hardcodes them (for example dist/layouts/shared/slots/search-trigger.js:19), outside its Translations API, so they need a fumadocs-ui patch or slot overrides. Same family as #298; suggest a sub-issue · evidence: aria-label scan of apps/docs/.next/server/app/de/docs/build/agents.html at cd2af9f · dedupe words: fumadocs aria-label English, Open Search, Toggle Theme, Copy Anchor Link, chrome localization",
    "class: a · reach: on /zh-Hans/docs/operate/backup the sidebar, breadcrumb and prev/next footer render English page-tree names (Backup and Disaster Recovery, and 28 more in zh-Hans; 53 per locale in ja, de, es, fr and ko) with no lang=en under html lang=zh-Hans. axe has no language-of-parts rule, so it stays green. A page-tree transform in docs/layout.tsx could mark them. Same family as #298; suggest a sub-issue · evidence: screenshot zh-Hans-fallback-1440.png sidebar; names come from frontmatter titles · dedupe words: sidebar English labels lang attribute, page tree fallback names, language of parts",
    "carrier: none · noted, not filed: the 404 COPY table in app/not-found.tsx has no zh-Hant entry, so zh-Hant shows English 404 text. On translated pages, Open in ChatGPT/Claude still points at the English .mdx. The language switcher lists all 8 locales. AGENTS.md says "62 of 79" Simplified coverage; it is 50 at 53ba2f3. Local next start 307-loops unprefixed English routes."
    ],
    "mcp_calls": "0",
    "api_writes": "1: POST repos//issues/298/comments (this os-dev-report, through scripts/pm/post-stamped.mjs and the fleet-write relay)",
    "cleanup": "The next start server (pgid 11741) and the opennextjs-cloudflare preview (pgid 18403) were stopped by process group, and the port answers 000. The worktree is kept as instructed. Nothing was pushed and no PR was opened."
    }

  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 298,
    "status": "done",
    "addendum_to": "#298 (comment)",
    "branch": "claude/pm-dispatch-objectos-ju9td1",
    "pr": "#306",
    "session": "session_01FeA1nwBz1ohH65dvffUGKr",
    "premise_still_valid": true,
    "head": "0e5d348 (0e5d348) on main 601bb37",
    "summary": "I rebased the staged work onto 601bb37 (#304) with no conflict. One added commit, 0e5d348, moves the sidebar Legal/Privacy/Terms labels that #304 added into lib/ui-text.ts. This was a mechanical move of three literals into three keys: English is unchanged, 7 locales are localized, and zh-Hant is regenerated by gen-zh-hant. Pushed to the designated branch with the lease d6431d6; the remote head now reads 0e5d348. Draft PR #306 was opened through the fleet relay pr_create; its body read back intact to the session footer. hotlong is assigned via label-write, and the read-back matches. It is not ready, not merged, has no reviewers requested and no auto-merge.",
    "gates_at_0e5d348": [
    "type-check --force: exit 0, "✓ Types generated successfully" (hash f943bb873ca4e2d0)",
    "CI-shaped build --force (NEXT_PRIVATE_STANDALONE=true): exit 0, "✓ Generating static pages using 3 workers (1052/1052)" (hash ffce3c965f8cecea). os-verify-lock VERDICT command-exit 0",
    "turbo run test --force: "✓ 10 self-test(s) passed"",
    "check-positioning: "✓ positioning: 4 copies equal their constants; the brand is right in 659 pages and 2 llms bodies; ..." exit 0",
    "check-locale-surface: "✓ every advertised URL has a source file and every source file is advertised; ..." exit 0",
    "check-search-locales: "✓ search locales: all 8 locales answer 200, find permissions, find every own page by its title, and find nothing for a nonce" exit 0",
    "gen-zh-hant --check: "✓ zh-Hant: 62 generated file(s) match the zh-Hans sources byte for byte." exit 0",
    "ownership (workflow argv: git diff --name-status --no-renames origin/main...HEAD; TRANSLATION_BOT_LOGIN set, human actor): "✓ 14 file(s) changed, no translation artifacts touched." exit 0. Unset: exit 0",
    "check-translations: "✓ translations gate passed". check-translation-output --self-test: exit 0. --files: "✓ translation output gate passed (98 pre-existing finding(s) reported)"",
    "built-HTML oracle scan: fallback 323/323 and translated 230/230 OK, 0 findings. Inverted-oracle control: 553 findings, exit 1",
    "smoke-docs on the preview: "✓ smoke: 4 page(s) rendered ..., negative control demonstrated red". Worker dry-run: Total Upload 49082.42 KiB (budget 61440)",
    "browser re-run on the preview: axe 0 violations on 15/15 pages, DOM checks 29/29, Copy Markdown 7/7 PASS, 0 console errors. The legal links render localized in all 7 locales (de: Datenschutz / Nutzungsbedingungen)"
    ],
    "open_question_resolution": "A (keep inline), per PM.",
    "deviations": [
    "The browser script gained one navigation retry, because the local workerd preview stalled once on a single navigation in the first re-run (the page answered 200 to curl afterwards). The second run needed no retry."
    ],
    "api_writes": "4: POST pulls (pr_create via fleet relay, draft forced) · assignees via label-write relay (PR 306 assigned hotlong) · this addendum comment · plus the earlier os-dev-report comment. git push is not counted as REST",
    "mcp_calls": "0",
    "cleanup": "The preview (pgid 3034) was stopped and the port answers 000. node_modules were removed, the worktree /home/user/objectos-issue-298 was removed with no --force, and the local branch staged/issue-298 was deleted"
    }

  4. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #306 on head 0e5d34852a09c40196ce187f511ee360fa3e0ac7 · 2026-10-06T10:33Z. Landing it now: no governed path is touched, it changes 470 lines (under 5,000), and its checks are green.

    Path surface (get_files): 14 files, +446/−24. All of them are app code:

    • app/[lang]/docs/[[...slug]]/page.tsx, app/[lang]/docs/layout.tsx, app/[lang]/layout.tsx, root-provider.tsx, components/ai/page-actions.tsx
    • the new lib/ui-text.ts, plus lib/ui-text/{de,es,fr,ja,ko,zh-Hans}.json (hand-written) and zh-Hant.json (generated by gen-zh-hant, and covered by its --check)
    • scripts/gen-zh-hant.mjs

    No content/docs/** file changes, which is why the path-filtered Translations workflow did not run.

    Checks on 0e5d348: CI build and Node floor succeeded; Deploy docs was skipped. mergeable_state is clean on base 601bb37.

    What it does, as measured by the dev and reviewed by the seat:

    • Fallback pages (a locale URL whose page has no source file in that locale):
      • The title, description, body and every TOC entry carry lang="en".
      • A notice in the route's locale sits above the H1. In zh-Hans it reads 「本页尚未翻译,以下显示英文原文。」.
      • Detection reuses translatedLocales() and canonicalLocale(), so it is exact.
      • On the built site, an oracle scan found 323/323 fallback pages and 230/230 translated pages correct, with 0 findings. Inverting the oracle produced 553 findings, so the scan does detect errors.
    • Interface text: fumadocs' Translations keys and this app's own controls are localized in all 8 locales. That includes the Privacy and Terms sidebar labels that docs: a short /docs description, legal-page metadata, share cards that fit, no MDX comments in llms bodies #304 added. I checked the screenshots for zh-Hans: 搜索, 本页内容, 复制 Markdown.
    • Copy Markdown: it copies the page being shown. A translation copies its own locale's Markdown, inlined at build time (open question A). English pages and fallbacks copy the English .mdx.
    • axe: 0 violations on 15 of 15 pages, and valid-lang passes on every fallback page.

    Gaps carried, not fixed here:

    • fumadocs' hard-coded English accessible names.
    • English page-tree names with no lang.
    • No zh-Hant 404 copy.
    • Open-in-ChatGPT/Claude points at the English .mdx.

    These are filed as #305. The sidebar labels that are the titles of untranslated pages belong to the translation pipeline (#68 / #75).


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed and live · 2026-10-06T10:39Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions