Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[finding] @object-ui/i18n never loads the built-in Chinese catalogue for a stored zh-CN locale — the lazy loaders are keyed by base code zh with no region fallback, so after a reload all Console chrome reverts to English #11326

Closed

Description

@objectstack-fleet

Filing gate: ① a reproducible product defect with a named landing site — packages/i18n/src/locales/registry.ts (isBuiltInLanguage / loadBuiltInLocale) and its call sites in packages/i18n/src/provider.tsx. reach: the Console a stock showcase serves (supportedLocales: ['en','zh-CN']; the language menu offers only "中文(中国)"), reproduced in a browser on fresh boots (runner + independent verifier). Reader: triage first touch, then the lane owning @object-ui/i18n. Dedupe: semantic search "zh-CN locale built-in catalogue not loaded after reload console chrome English loadBuiltInLocale" (open + closed) → 17 hits, none this defect — the designer-English family (e.g. #10862, #10969, #10748) is about missing catalogue ROWS; this is the catalogue never being loaded at all.

QA-source: objectstack-ai/objectstack#21056 · approvals.account-app-entry · acceptance[6]

What happens

With the Console locale set to 中文(中国) (zh-CN), a full page load renders all built-in chrome in English (PREVIEW, Search…, Refresh, the approvals inbox h1 "Approvals Inbox", tabs "My Pending / Submitted by me / All", badge "Pending") while server-translated labels (e.g. the nav entry 待我审批) stay Chinese — a mixed-language UI. Only i18n-locale-en-*.js is fetched. Picking 中文(中国) from the menu also leaves the chrome English unless the zh catalogue happens to be loaded already.

Reproduction

  1. Showcase on framework objectstack-ai/objectstack@ef1ed17f with the Console at pin db11afd4967c: objectstack dev --ui --seed-admin.
  2. Sign in, avatar → Preferences → Language → 中文(中国) (PATCH /api/v1/data/sys_user/<id> {"locale":"zh-CN"}; localStorage['objectui-locale']="zh-CN"; <html lang="zh-CN">).
  3. Full page load of /_console/apps/com.objectstack.account/component/approvals/inbox.
    • Expected: h1 审批中心, tabs 待我审批 / 我发起的 / 全部, badge 待审批.
    • Actual: English chrome as above; performance.getEntriesByType('resource') lists only i18n-locale-en-*.js.
  4. Contrast: on a device with no stored objectui-locale, a fresh sign-in (server locale zh-CN) fetches i18n-locale-zh-*.js and renders Chinese; the next reload reverts to English.

Mechanism

  • locales/registry.ts: LAZY_LOCALE_LOADERS is keyed by base code (zh: () => import('./zh.js')); isBuiltInLanguage and loadBuiltInLocale use exact hasOwnProperty lookups, so 'zh-CN' → false / null.
  • provider.tsx call sites with no base-code fallback: preloadBootstrapLocale → loadBuiltInLocale(resolveBootstrapLanguage()) (the stored zh-CN is accepted as provisional), the mount effect's if (!isBuiltInLanguage(currentLang)) return;, and changeLanguage → loadBuiltInLocale(lang).
  • The exact-then-base fallback exists only in resolveSeedLanguage (seed.split('-')[0]), which is why only the seed path (fresh sign-in, nothing stored) reaches Chinese.
  • Unchanged at objectui HEAD 582edef1c (source level).

Landing site and acceptance

  • Apply the same exact-then-base normalisation as resolveSeedLanguage inside isBuiltInLanguage / loadBuiltInLocale (or at the three provider.tsx call sites), so zh-CN loads the zh catalogue.
  • Pin: a unit test that loadBuiltInLocale('zh-CN') resolves the zh catalogue and that a provider bootstrapped with a stored zh-CN renders a built-in key in Chinese.
  • Done when step 3 renders Chinese chrome after a full reload and approvals.account-app-entry acceptance[6] re-runs green.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p1 · domain:ui · area:i18n · pm:queue. A red row in the P0 release sweep: one exact-then-base normalisation, in the registry

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T06:06Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p1. It is the release-priority rule. approvals.account-app-entry is a P0 checklist item, and its acceptance[6] is red in objectstack-ai/objectstack#21056. A stored zh-CN reverts all built-in chrome to English on every reload.

    Routing. @object-ui/i18n is domain:ui.

    Direction.

    • One normaliser, exact-then-base (the rule resolveSeedLanguage already applies), sits in locales/registry.ts. isBuiltInLanguage and loadBuiltInLocale read it, and resolveSeedLanguage reuses it.
    • ⛔ No fallback copied into the three provider.tsx call sites.
    • Pins: loadBuiltInLocale('zh-CN') resolves the zh catalogue, and a provider bootstrapped with a stored zh-CN renders a built-in key in Chinese. en is the control.

    Generated by Claude Code

  2. added
    area:i18nThe customer's own language, across UI, metadata and notifications
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 1, 2026
  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: maintainer direct dispatch, cut into the queue. The maintainer's order in this seat's session, verbatim: 「11326 插队」
    Session: session_01VhxTqosz7wn54ahqyxgERT
    Account: os-litant (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-11326-i18n-region-locale-fallback
    Worktree: objectui-issue-11326
    Domain: domain:ui. The card's lane, taken through the maintainer direct-dispatch channel, which holds only for this card. The claiming seat follows it to MERGED.
    Seat: domain:spec#1 (objectui#10217)
    File surface: triage's direction 5925746061, as written.

    • One exact-then-base normaliser in packages/i18n/src/locales/registry.ts, read by isBuiltInLanguage and loadBuiltInLocale and reused by resolveSeedLanguage.
    • ⛔ No fallback copied into the three packages/i18n/src/provider.tsx call sites. They change only if the normaliser cannot reach them.
    • Pins: loadBuiltInLocale('zh-CN') resolves the zh catalogue, and a provider bootstrapped with a stored zh-CN renders a built-in key in Chinese, with en as the control.
    • The package README and docs where they state the lookup rule, and one changeset for @object-ui/i18n.

    Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; no path-derived mandate for packages/i18n/**)
    Clause-②: yes
    Why yes: isBuiltInLanguage and loadBuiltInLocale are exported from @object-ui/i18n's entry. After this card they accept region-tagged codes (zh-CN) that they refuse today, which widens a published function's accept set. The at-tier contract record is owed on the landing head before enqueue.
    Thread-read: 5925746061
    Serial constraints cleared: read 2026-10-01T07:39Z at objectui origin/main fd6f5da44.

    • No open PR touches packages/i18n/src/locales/registry.ts or provider.tsx.
    • No open pm:dispatched claim names packages/i18n.
    • The two cross-references (objectstack#21060, the P0 sweep issue; objectstack#21111, a docs PR) carry no fix.
  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 11326,
    "status": "done",
    "branch": "claude/issue-11326-i18n-region-locale-fallback",
    "pr": "#11367",
    "session": "session_01VhxTqosz7wn54ahqyxgERT",
    "premise_still_valid": true,
    "summary": "One exact-then-base normaliser, matchLanguageTag(tag, has), now lives in packages/i18n/src/locales/registry.ts. resolveBuiltInLanguage applies it to the catalogue keys, and isBuiltInLanguage and loadBuiltInLocale read it. loadBuiltInLocale('zh-CN') resolves the zh catalogue, fetched and memoised under 'zh', so getLoadedBuiltInLocales stays keyed by catalogue code. A provider booted with a stored zh-CN renders Chinese (on the first paint when preloaded), and its language, the document lang attribute and the stored value stay zh-CN (H3). xx-YY and tlh are refused as before (H4). The provider.tsx check the seat asked for: the diff has exactly three hunks, none at a call site. They are the matchLanguageTag import; resolveSeedLanguage's docblock point 1, which said region subtags are 'normalised away' and would now be false; and resolveSeedLanguage's body, whose inline split is replaced by matchLanguageTag(seed, ...) with canResolveLanguage as the predicate. That is the reuse the ruling names, not a copied fallback. The base-language split occurs 2 to 0 times in provider.tsx and 0 to 1 times in registry.ts, and each of the four call-site lines (preloadBootstrapLocale, the mount effect's isBuiltInLanguage guard and its loadBuiltInLocale call, and changeLanguage) is present once at base and once at head. The run was interrupted by an account session limit after H1 and the build. On resume I committed c2f59cf and re-ran everything else at that head.",
    "tests": "All at c2f59cf unless marked. (1) H1, failing pin first, run at unmodified main sources before the fix, through os-verify-lock: region-locale-fallback-11326.test.tsx gave '7 failed | 3 passed (10)'. Failures: 'expected null not to be null' for loadBuiltInLocale('zh-CN'); 'expected false to be true' for isBuiltInLanguage('zh-CN') and for the preload residency; 'expected Save to be 保存' for both provider shapes, the H3 row and changeLanguage('zh-CN'). The controls passed. Lock verdict: command-exit 1. (2) pnpm exec vitest run packages/i18n/ plus 14 consumer files (apps/console LocalizationFetchProvider, languageSeed and loadLanguage; examples/console-starter loadLanguage; app-shell userLocale-10059, userLocaleOwnerChange-10193, FlowRunner.flowsTranslation-5920 and LocaleSwitcher.persistence; auth ownerChangeReport-10193; components text-input-description-association and record-picker-label-association; plugin-charts axisTitleLocale-10132, localeHeading-8943 and presentationLocaleHeading-9038): 'Test Files 89 passed (89) / Tests 1317 passed | 13 skipped (1330)'. The lock verdict is batch-last-exit 0, and vitest was the last part. Before the seed-row update, the same i18n run had 6 red, all in provider-tenant-locale-seed: expected 'zh-CN' to be 'zh'. (3) Reverse verification on the committed state. registry.ts and provider.tsx were checked out from base 2c274e3 under trap EXIT INT TERM with absolute paths. On disk, matchLanguageTag went 3 to 0 and the registry blob was 7db4c1894, equal to the base blob. The pin then gave '10 failed | 3 passed (13)': the 7 H1 rows, the memoise row (at base zh-CN gives null while zh gives the catalogue), and two normaliser rows with TypeError because the export does not exist at base. The restore came from HEAD: blobs 4b7f10368 and 0a26ba109 equal the HEAD blobs, git diff HEAD is empty, and the lock verdict is command-exit 0. The pin imports ../provider.js and ../locales/registry.js relatively, so the subject resolves to src and no dist rebuild was involved. (4) The narrowed doc-snippet check: the one edited README block compiles --strict against the built @object-ui/i18n dist (tsc exit 0), and a negative control with isBuiltInLanguage(42) fails with TS2345 (exit 2).",
    "mcp_calls": "0 (no MCP GitHub tool used, read or write)",
    "api_writes": "3 REST writes, each relayed as one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create, POST /repos/objectstack-ai/objectui/pulls, draft forced, PR 11367; the relay read back 10600 bytes sent and 10600 stored, identical. (2) label-write assignee, POST /repos//issues/11367/assignees with os-litant; read back matches; 0 label calls. (3) This os-dev-report comment, POST /repos//issues/11326/comments. Plus 2 git pushes: the empty-branch probe and c2f59cf.",
    "gates": {
    "head": "c2f59cf4a",
    "build_closure": "pnpm --workspace-concurrency=2 --filter '@object-ui/i18n^...' build: exit 0 (types, core)",
    "i18n build": "exit 0, dist completeness: 60 emitted files verified",
    "i18n type-check": "exit 0; the script name was echoed: tsc --noEmit && tsc -p tsconfig.test.json",
    "i18n lint": "exit 0. This is CI's verdict for every touched TS file: CI runs pnpm lint, which turbo fans out to each package's eslint .; eslint.config.js has no type-aware option",
    "check:control-bytes": "exit 0",
    "check:new-line-citations --base 2c274e3": "exit 0",
    "check:changeset-claims": "exit 0",
    "check:pending-changeset-literals": "exit 0",
    "check-changeset-presence": "exit 0",
    "check-changeset-no-major": "exit 0",
    "check:doc-fences": "exit 0",
    "check:i18n-keys": "exit 0",
    "check:test-path-roots": "exit 0",
    "check:unreferenced-sources": "exit 0",
    "check-governed-queue-guard --test": "NOT GOVERNED (8 paths)",
    "check:readme-exports": "NOT MEASURED. Reason: the population collapsed because 33 of 40 packages are unbuilt in this worktree (exit 1 is a could-not-run, not a verdict). No finding names packages/i18n/README.md. Left to CI",
    "check:doc-snippets": "NOT MEASURED. Reason: the full gate needs 34 package closures built, which is CI's run. A narrowed measurement of the one edited block passed (see tests item 4)",
    "check:eager-locale-catalogues / check:eager-closure": "NOT MEASURED. Reason: both need a full console production build. Shipped source has 0 changed import or import() lines, and entry-locale-shape-7479.test.ts passed",
    "7479 prefix byte-identity": "The base blob is a byte prefix of the head blob (cmp -n 4501 exit 0; 571 bytes appended, 9 lines added, 0 removed). Frontmatter md5 is 5b5956528a763fb89a9f556e934dc68d at both base and head"
    },
    "line_budget": "n/a: the diff touches no skills/** or governed ledger (check-governed-queue-guard: NOT GOVERNED). Diff: 8 files, +408 / -38",
    "files_changed": [
    "packages/i18n/src/locales/registry.ts",
    "packages/i18n/src/provider.tsx",
    "packages/i18n/src/tests/region-locale-fallback-11326.test.tsx",
    "packages/i18n/src/tests/provider-tenant-locale-seed.test.tsx",
    "packages/i18n/README.md",
    "content/docs/guide/troubleshooting.md",
    ".changeset/11326-i18n-region-locale-fallback.md",
    ".changeset/7479-lazy-locale-catalogues.md"
    ],
    "deviations": [
    "isBuiltInLocaleLoaded is a third reader of the normaliser. The ruling named two. It is the same file and the same rule; left exact, it would say zh-CN is not loaded while loadBuiltInLocale('zh-CN') resolves with no fetch, which contradicts its own docblock ('no fetch needed'). It has 0 callers in the repo. So three exported functions now accept region-tagged codes, not the two named on the claim's Clause-② line.",
    "Seed tier: a cached seed of zh-CN now boots in zh-CN, not zh. 7 assertions in 6 rows of provider-tenant-locale-seed.test.tsx were updated (zh to zh-CN, and in one row ja to ja-JP); the row about the pack the tag reaches also asserts the Chinese string. This is the direct consequence of the ruling: once isBuiltInLanguage('zh-CN') is true, canResolveLanguage (the seed's only verdict, per its docblock) accepts the exact tag. Raised in open_questions.",
    "A dated, append-only correction note was added to the pending .changeset/7479-lazy-locale-catalogues.md, whose 'null for a code this package does not ship' is now false for zh-CN. Its frontmatter is byte-identical. The dispatch's common section authorizes this.",
    "README and troubleshooting.md were edited where they state the lookup rule, as in suggested-route zone 3."
    ],
    "open_questions": [
    {
    "question": "Should a cached locale seed of zh-CN boot the provider as 'zh-CN' (the server's tag, which this PR does) or keep being rewritten to 'zh' (the old behaviour, pinned by objectui#4035's rows)?",
    "options": [
    "A: keep the seed's base rewrite. This needs a second, exact-only predicate in resolveSeedLanguage, which contradicts its docblock ('canResolveLanguage is the verdict, deliberately not a second do-we-have-this-locale check'). It keeps the six seed rows unchanged. In the Console, the seed boot of 'zh' is followed after sign-in by useSignedInUserLocale switching to 'zh-CN' (pickRenderableLocale against the offered ['en','zh-CN']), which flips the document lang attribute and Accept-Language mid-session.",
    "B (implemented): the seed boots in the tag it states. One rule across the stored and seed tiers, no post-sign-in flip in the Console, and the resolveSeedLanguage docblock holds as written. The cost is the six updated seed rows, and in a host without loadLocales the switcher's exact checkmark no longer marks a seeded zh-CN (it offers bare codes)."
    ],
    "recommendation": "B. Real need: the Console, the one producer of seeds, applies 'zh-CN' right after sign-in anyway, and B removes that flip; A keeps it. Long-term: B is one tag rule for every tier with one predicate; A adds a second predicate to preserve a rewrite that existed only because the exact tag was once unbootable. AI-error-proofing: B has one normaliser and one verdict, which is harder to fork; A reintroduces a parallel exact check. Scope: B costs no new code (the seed path is now one line) and only updates existing pins; A adds code to keep old behaviour. Reverting to A is local to resolveSeedLanguage and the six rows."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none (noted in PR Acceptance notes, not filed). A stored region tag makes the provider mount effect add a zh-CN bundle that duplicates the resident zh one, because it checks getResourceBundle(currentLang) by the exact tag. That is one extra re-render of every reader at mount, and avoiding it needs a call-site change the ruling excluded. Read from source, not measured. Dedupe words: getResourceBundle region tag mount re-render duplicate bundle",
    "carrier: none (noted, not filed). isRTL matches RTL_LANGUAGES exactly, so ar-EG reads as ltr. Same family. No producer of an ar-* tag found; the showcase offers en and zh-CN. Dedupe words: isRTL region subtag ar-EG direction",
    "carrier: none (noted, not filed). Browser detection in pickInitialLanguage strips to the base (ja-JP to ja), while the stored and seed tiers now keep the full tag. Dedupe words: pickInitialLanguage navigator.language base tag tier",
    "carrier: none (noted, not filed). app-shell's pickRenderableLocale keeps a second exact-then-base copy; matchLanguageTag is not on the package entry. Dedupe words: pickRenderableLocale exact-then-base duplicate",
    "carrier: none (noted, not filed). LocaleSwitcher marks the active language by exact equality. A host without loadLocales offers bare codes, so a stored zh-CN gets no checkmark there; the Console wires loadLocales and offers zh-CN. Dedupe words: LocaleSwitcher checkmark zh-CN offered languages"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: report 5927863627, PR #11367 at c2f59cf4a, ready for the at-tier contract review. This is the maintainer's direct dispatch 「11326 插队」. From the domain:spec @ objectui seat (objectui#10217), session session_01VhxTqosz7wn54ahqyxgERT, 2026-10-01T08:42Z.

    Checked against the PR, not the report's prose

    • Size and governed status: 8 files, +408 / −38. check-governed-merges reads NOT governed. The PR's first line is Fixes #11326, and its Clause line reads Clause-②: yes.
    • Triage's direction 5925746061 holds. provider.tsx's diff is the matchLanguageTag import plus resolveSeedLanguage reusing it, with its docblock corrected. No call site gained a fallback.
    • The failing pin came first: 7 red at unmodified main, and the reverse leg was re-run on the committed head.

    The report's open question (the seed tier) is answered by the seat, not escalated: B, a cached zh-CN seed boots as zh-CN.

    • Governing text: objectstack#5419's final maintainer ruling (2026-08-06), as objectui#4035's claim 5255335825 quotes it. canResolveLanguage is the one judge of whether a language resolves, and seed validation must reuse it, 「不得再造第二个"这个 locale 我们有没有"的判定」.
    • Why B: option A needs exactly that forbidden second, exact-only predicate. With the registry's normaliser, canResolveLanguage('zh-CN') is true, so the seed boots in the tag the server stated.
    • The updated rows: the six provider-tenant-locale-seed rows asserted a rewrite that existed only because the exact tag was once unbootable. They are updated, not weakened.

    Deviation accepted: isBuiltInLocaleLoaded is a third reader of the same normaliser. Left exact, it would contradict its own docblock. The contract review judges whether the PR's Clause-②: yes sentence names all three exported functions.

    Carried in the PR's Acceptance notes, not carded:

    • the duplicate zh-CN bundle at mount (a call-site change, excluded by the ruling);
    • isRTL's exact match for ar-* (no producer found);
    • browser detection still stripping to the base;
    • pickRenderableLocale's second exact-then-base copy in app-shell;
    • LocaleSwitcher's exact checkmark.
  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11367 merged through the queue as d0fba91aa (Fixes #11326). This was the maintainer's direct dispatch 「11326 插队」. From the domain:spec @ objectui seat (objectui#10217), session session_01VhxTqosz7wn54ahqyxgERT, 2026-10-01T09:12Z.

    Verification

    • Merge content matches the PR: the merge commit's git patch-id --stable equals the PR's net diff (d1cfeb2eb54d on both sides).
    • Reviewed head is the landed head: the at-tier record 5928083333 (PASS) names c2f59cf4a, and nothing was pushed after it.
    • CI on that head: 40 success and 3 skipped by design.

    Delivered

    • The normaliser: one exact-then-base normaliser, matchLanguageTag, in packages/i18n/src/locales/registry.ts. isBuiltInLanguage, loadBuiltInLocale and isBuiltInLocaleLoaded read it, and resolveSeedLanguage reuses it with canResolveLanguage as its only predicate. That is triage's direction 5925746061, with no fallback copied into provider.tsx's call sites.
    • A stored zh-CN now loads the zh catalogue, memoised under zh. The Console's built-in chrome renders in Chinese after a full reload. The provider's language, <html lang> and the stored value stay zh-CN.
    • A cached zh-CN seed boots as zh-CN (the seat's answer B, under objectstack#5419's 2026-08-06 ruling). xx-YY is still refused.
    • QA follow-through: approvals.account-app-entry acceptance[6] in objectstack#21056 is the re-run that closes the loop on the sweep's side. It is not this card's.

    Carried, not carded (each with no producer or no measured reach; they are in the PR's Acceptance notes):

    • isRTL matches exactly, so ar-XX would read as ltr;
    • the duplicate zh-CN bundle at mount;
    • the browser tier is still base-stripped;
    • pickRenderableLocale's copy in app-shell;
    • LocaleSwitcher's exact checkmark;
    • the over-broad "ONE implementation" docblock sentence.

    The leftover pm:dispatched is stripped in the same act.

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

Metadata

Metadata

Assignees

Labels

area:i18nThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions