Repository navigation
fix(app-shell): three console dates read the display locale, not the UI language (objectui#10331) - #10369
Conversation
…the display locale InstalledList's install date, MarketplacePage's relative published time and CloudConnectionPanel's bound-since date formatted with the UI language (`language || undefined`, `language || 'en'`, `language`). They now take their tag from `useDisplayLocale()`, so a regional display locale reaches them and no site falls back to the machine locale or a literal 'en'. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC Co-authored-by: Claude <noreply@anthropic.com>
An English UI with a de-CH display locale must format the install date, the relative published time and the bound-since date as de-CH; an en-US display locale is the control, and the runtime tripwire checks the tag each formatter received. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC Co-authored-by: Claude <noreply@anthropic.com>
…y locale Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ed import list
"all five owning components bind t from a bare useObjectTranslation" matched
the single-name literal `import { useObjectTranslation } from '@object-ui/i18n'`,
so MarketplacePage going red on `import { useDisplayLocale, useObjectTranslation }`
(objectui#10331) was the pin's spelling, not a lost binding. It now matches a
named import list from `@object-ui/i18n` that carries the bare, un-aliased name,
names the file's actual imports from that package on failure, and has a control
that accepts the three list shapes and refuses a list that dropped the name, a
longer name, an alias, a type-only import and another module.
Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #10331
Clause-②: no
Three console date faces now take their locale from
useDisplayLocale()instead of the UI language. Implemented bysession_01BA3nKVUwKQJf8DBxrSVtNC(seatdomain:ui#1, a dispatched dev subagent), on base0651e7ab4.Premise, re-derived on
0651e7ab4All three call sites were on
origin/mainexactly as the card and the triage grading (5820846444) quote them:InstalledListWidget.tsx:new Date(entry.installedAt).toLocaleString(language || undefined)MarketplacePage.tsx(insideuseRelativeFormatter):new Intl.RelativeTimeFormat(language || 'en', { numeric: 'auto' })CloudConnectionPanel.tsx:new Date(conn.bound_at).toLocaleString(language)The contract, from
packages/i18n/src/useDisplayLocale.ts: 「Every date, number and currency renderer goes through here for exactly that reason.」What changed
InstalledList(InstalledListWidget.tsx) andCloudConnectionPanelcalluseDisplayLocale()at the top of the component, next touseObjectTranslation(), and format the date with it.languagehad no other use in either file, so it is no longer destructured there.MarketplacePage.tsx: the formatter lives inuseRelativeFormatter. That is a custom hook called from the component body, not a plain helper, so it callsuseDisplayLocale()itself and no locale parameter had to be threaded.languagestays inMarketplacePageforlocalizePackage(pkg, language): that picks the package's translated name and description, which is a translation choice, not a formatting locale.undefined(the machine locale) or to a literal'en'any more. The hook owns the last-resort tag.packages/i18n/**is not touched except one test pin this change broke (see the seat edit below), and no other console face is touched.patchon@object-ui/app-shell. It states the behaviour change: dates on these three faces now follow the display locale.Tests
New pins, one
describeper face. Every case mounts the realI18nProviderwith an English UI (defaultLanguage: 'en') and declares the display locale throughLocalizationProvider, so the display locale is the only thing that varies:packages/app-shell/src/console/marketplace/__tests__/marketplaceDates.displayLocale-10331.test.tsx:InstalledListandMarketplacePagepackages/app-shell/src/console/cloud-connection/__tests__/CloudConnectionPanel.displayLocale-10331.test.tsx:CloudConnectionPanelEach face has four cases:
de-CHpin: a literal4.3.2020orvor 3 Tagen, plus equality with the runtime's ownde-CHrendering;en-UScontrol:3/4/2020or3 days ago;de-CHrendering differs from theen-USone, which no runner locale can satisfy;@object-ui/test-supporttripwire: every call of the face's formatter receivedde-CH, and no call got the machine locale.The relative-time fixture is three days old. That gives "vor 3 Tagen" against "3 days ago", and it never reaches the translated "today" bucket.
Runs from the repo root at
4c08ed0a6:pnpm exec vitest run packages/app-shell/src/console/marketplace/ packages/app-shell/src/console/cloud-connection/ packages/app-shell/src/console/home/__tests__/HomePage.marketplaceDisabled.test.tsx packages/app-shell/src/console/__tests__/AppContent.pseudoRouteSegments.test.tsx packages/i18n/src/__tests__/machineLocaleCensus-9909.test.tsgivesTest Files 18 passed (18)andTests 134 passed (134). That covers the 12 new pins, every marketplace and cloud-connection neighbour, the two other suites that mount these components, and the objectui#9909 census, which scans these files.pnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json) exits 0, after building the app-shell dependency closure (turbo run build --filter="@object-ui/app-shell^...", 28 of 28 tasks).tsc -p tsconfig.test.json --listFileslists both new pin files.Ablation: fix committed first, every leg restored with on-disk proof
Each leg puts the pre-fix UI-language value back at ONE face through
ablation-replace.mjs, then runs both pin files. The anchor isconst displayLocale = useDisplayLocale();, and it must hit exactly once.useObjectTranslation().language || undefineduseObjectTranslation().language || 'en'useObjectTranslation().languageThe red readings:
expected 'Installed 3/4/2020, 12:00:00 PM' to match /4\.3\.2020/expected '3 days ago' to be 'vor 3 Tagen'expected '3/4/2020, 12:00:00 PM' to match /4\.3\.2020/The
en-UScontrol stays green on every leg. It cannot tellenfromen-US, which is why it is the control and not a pin.On-disk proof for each leg:
d2eaeb1d83d6to2747907f61b3).d2eaeb1d83d6,d01379567cfd,a3083f630e87) andgit diff HEADis empty.git status --porcelainwas empty at the end.No
dist/is on the path: the pins import the components by relative path, and vitest aliases@object-ui/i18nand@object-ui/test-supporttosrc.Gates
dispatch-gates.mjsonly works in objectstack, so these were derived by hand from this repo'spackage.jsonand.github/workflows/:pnpm check:control-bytes: OK.pnpm check:new-line-citations:VERDICT new-cross-file-line-citations: 0 new citation(s).node scripts/check-changeset-presence.mjs: 5 source files of 1 released package changed, and 1 changeset is declared.pnpm check:changeset-claims: the self-contradiction reading is clean. The went-false reading flags.changeset/6681-declared-lazy-marketplace-routes.md, which namesconsole/marketplace/InstalledListWidget.tsx. I read it: the paragraph is about how the chunks are split around the barrel's SDUI-registration import. This change adds one named import from a package the file already imports, so the paragraph still holds.check-vi-mock-override-shape,check-vi-mock-specifiers,check-vi-mock-inherit,check-test-path-roots,check-changeset-no-majorandcheck-changeset-overwrite. All OK.pnpm lintis left to CI. ESLint was run on the 5 changed files frompackages/app-shell, whose ownlintscript iseslint ..no-explicit-anyandset-state-in-effect), the same counts as their base blobs.eslint --print-configresolves all 5 files, so none of them is ignored.eslint.config.jsconfigures no type-aware linting (noparserOptions.projectorprojectService), and the local rules read no other files. So this diff cannot change the lint result of a file it does not touch.Why the objectui#9909 census missed these, and a cheap extension
machineLocaleCensus-9909.test.tssorts the first argument into bare,undefined,'default', a BCP-47 literal, or an expression. It deliberately skips expressions; its header says "A call that passes an EXPRESSION is not judged here". All three sites passed an expression.|| undefined,?? undefinedor|| 'tag'is the machine locale or a literal in disguise. The census could classify it as theundefinedorliteralshape. A single-linegit grepfor that shape overpackagesandapps(tests excluded) at0651e7ab4finds exactly two sites: this card'sInstalledListWidgetandMarketplacePage. After this PR, that extension would land green with no new exemption. The grep is an approximation: the census reads a 60-character window across lines, and the grep only reads single lines.languageargument (the UI-language channel) cannot be told apart from a correctly named variable without data flow. The same grep withlanguageori18n.languageas the first argument finds three sites at0651e7ab4: this card'sCloudConnectionPaneland the two faces below. That rule would land red.Neither extension is built here, as the card asks.
Seat edit (2026-09-24T20:59Z): a pin this change broke, fixed at
55f546173CI at
4c08ed0a6was red on one test outside the claim's surface.packages/i18n/src/__tests__/marketplace-preview-namespace-3546.test.tsx(「all five owning components bind t from a bare useObjectTranslation」) matched the literalimport { useObjectTranslation } from '@object-ui/i18n', andMarketplacePage.tsxnow imports{ useDisplayLocale, useObjectTranslation }. The respin fixes the pin, not the import. The check is now a regex that accepts a named-import list from@object-ui/i18nholding the bare, un-renameduseObjectTranslation. A new control test accepts single-name, two-name and multi-line lists, and rejects a missing name,useObjectTranslationSafe, anasrename,import type,@object-ui/i18n-extraand./i18n. Mutating the import to{ useDisplayLocale }reddens exactly this pin. The seat widened the claim's surface to this one test file.At
55f546173:vitest run packages/i18n/ packages/app-shell/src/console/gives 157 files and 1748 tests passed. The 8 other test files naming these sources give 539 passed. Theapp-shellandi18ntype-checks exit 0, andcheck:i18n-keyspasses.Acceptance notes
apps/console/src/pages/system/ApprovalsInboxPage.tsx:formatRelative's bucket for items older than 30 days,toLocaleDateString(language).packages/collaboration/src/CommentThread.tsx:formatAbsoluteDate(date, language), for comments older than 7 days. Its doc comment chose "the session language" under objectui#3441.content/docschange. For the app-shell half this follows the objectui#10232 precedent; the changeset carries the user-facing sentence.Generated by Claude Code