Skip to content

fix(console): after a language switch, date cells format in the new language, not the stale server locale - #27624

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-b2omyd
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-b2omyd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Requested by Bill · project thread
Fixes #27498
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:头像菜单切换语言后,console 的本地化提供者在「本人 sys_user 行被写入」时重新读取 /auth/me/localization;日期格式化器仍只有一个(formatDate 经 useDisplayLocale),未新增第二个。
  • 为什么改:切换语言会先换界面语言、触发 console 重挂载,再写 sys_user.locale;重挂载发出的读取可能先于写入到达服务器,拿回旧的 en-US 并整个会话保留,于是中文界面下表格日期显示 Oct 11,整页加载才变 2026年10月11日。
  • 风险与回滚:只动 console 一个文件,写入本人 sys_user 行时多一次 GET;回滚即 revert 本 PR。
  • 先看:ui/apps/console/src/LocalizationFetchProvider.tsx(新的 useDataInvalidation 依赖与 signal 一行);LocalizationFetchProvider.test.tsx 新的 describe 块。
  • 两分钟验证:pnpm --dir ui exec vitest run apps/console/src/LocalizationFetchProvider.test.tsx,预期 18 个测试全绿,其中 4 个新用例名以 re-reads after a write 开头。
  • 需要你做的:无,席位落地。

What changes

LocalizationFetchProvider (console) now reads GET /api/v1/auth/me/localization again whenever the signed-in user's own sys_user row is written through the data-invalidation bus, and that re-read opts out of joining a request still in flight from before the write.

Cause, measured on a running stack. The endpoint's locale is the caller's language: their own sys_user.locale when set, else the request's Accept-Language, else the deployment default. useDisplayLocale ranks it above the active UI language, and every grid / record date renderer formats with it. The avatar-menu switch (useLanguageSelection) changes the UI language first and writes sys_user.locale second. The UI-language change remounts the metadata subtree (key={language} in ConnectedShellInner), which remounts this provider, and the remount's read can reach the server before the column write commits. It then answers the previous language (for a user with no stored language yet, the browser's Accept-Language, en-US), and nothing ever read again. Result: Chinese headers, chips and footer (all t-driven) beside Oct 11 / Nov 1 / Nov 18 dates, until a full page load re-read the now-written column.

Why the record page differed. It is not a second formatter and not grid-specific. The record page reads the same useDisplayLocale; the walkthrough's record page was a fresh page load, whose boot read sees the committed column. Reached in the same session by clicking the row, the record page also read Oct 11, 2026 before this change (measured below).

Fix at the cause. The provider subscribes with useDataInvalidation('sys_user', userId) (the same bus useSignedInUserLocale already reads; every adapter write reaches it through useMutationInvalidationBridge). The language switch's own write therefore triggers a read that starts after the write has settled. Because sharedGetJson shares a promise with any overlapping GET of the same URL, a re-read passes an AbortController signal, which opts it out of sharing (and aborts it if superseded or unmounted); the boot read keeps sharing with seedTenantLanguage() (objectui#5544). The existing cancelled guard drops a stale answer that settles late; the catch now returns first when cancelled so an abort is not treated as a transient failure. The profile page's language card writes the same row and is covered by the same subscription.

The ruling "Format grid date cells through the same locale-aware formatter the record page uses. No second date formatter." holds: no formatter, renderer or useDisplayLocale line changed.

Tests

New describe block in ui/apps/console/src/LocalizationFetchProvider.test.tsx, probing useDisplayLocale() plus formatDate('2026-10-11', undefined, { locale, alwaysShowYear: true }), the face a grid cell paints:

  • a read that lost the race with the language write is replaced after the write: boot read answers en-US (Oct 11, 2026), a sys_user write for the signed-in user re-reads, the probe shows zh-CN|2026年10月11日.
  • the re-read does not join a request still in flight from before the write: the boot read is held pending; the write's re-read issues its own request and lands zh-CN; the held request settling late with en-US is dropped.
  • a write to another user's row reads nothing again and signed out, a sys_user write reads nothing again.

The block resets the shared in-flight registry after each case, so a pending request from a failing case cannot be joined by the next one.

Targeted runs and ablations (head eda50a4, before the main merge; re-run after it in Verification)
  • pnpm --dir ui exec vitest run apps/console/src/LocalizationFetchProvider.test.tsx → Tests 18 passed (18); with apps/console/src/__tests__/localizationBootDedup.test.tsx → Test Files 2 passed (2), Tests 21 passed (21).
  • Ablation 1, through scripts/ablation-replace.mjs: effect dependencies [endpoint, userRowWrites] → [endpoint] (anchor 1 → 0, blob changed). Red: a read that lost the race… and the re-read does not join…. Restored; file hash equal to HEAD's.
  • Ablation 2: delete signal: reread?.signal, (anchor 1 → 0). Red: the re-read does not join a request still in flight from before the write only (Tests 1 failed | 17 passed). Restored; hash equal to HEAD's.
  • No dist involved: the console suite resolves the provider and @object-ui/* from source.

Acceptance notes

  • Same symptom, second door, not fixed here (reported to the seat): a signed-in user with no stored sys_user.locale whose UI language comes from the device (for example 中文 picked on the sign-in page) gets Nov 18 dates under Chinese headers on every load, deterministically: the endpoint answers the Accept-Language rung (en-US), and useDisplayLocale ranks it above the UI language. Measured on this stack with the column reset to null and objectui-locale = zh. Closing it means changing useDisplayLocale's documented precedence or having the endpoint say which rung answered, a design call outside this card.
  • The useDisplayLocale docblock says the tenant locale is "Frequently undefined"; on this stack an authenticated read always answered a locale (en-US from the header or deployment rung). Not edited here.
  • The object description staying English under zh-CN (A unit of work inside a project.) is the showcase bundle's coverage, out of scope per the card.

Verification

Live reproduction, app-showcase on this branch (backend :4127 --fresh, console vite :5197 from ui/apps/console source)

Script: sign in as admin in an en-US browser, open /apps/showcase_app/showcase_task (All Tasks), avatar menu → Language → 中文, read the Due Date cells; click "Audit current IA" (same session); then load that record URL fresh. The sys_user PATCH was held 1.5 s by a Playwright route to make the race deterministic; a second run held it 0 s. Column reset to null before each run.

Before the fix (head 556f323, delay 1.5 s):

  • /auth/me/localization answers: en-US, en-US, en-US (remount read), then the PATCH {"locale":"zh-CN"} lands.
  • Header 截止日期; cells Nov 18, Nov 8, Nov 1, Nov 28, Oct 11, Oct 31.
  • Record page reached by click: Oct 11, 2026. Same record by URL load: 2026年10月11日, and the grid behind it 11月18日, 11月8日….

After the fix (delay 1.5 s):

  • Answers: en-US, en-US, PATCH released, en-US (the remount's stale read), zh-CN (the re-read).
  • Cells 11月18日, 11月8日, 11月1日, 11月28日, 10月11日, 10月31日; record by click 2026年10月11日; by URL load 2026年10月11日.

After the fix, delay 0: cells 11月18日…, record by click and by URL 2026年10月11日.

Local gates on head 416229c (after merging origin/main); the farm is CI's

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (3 paths vs merge base 1ed3360). Reconciliation --ran: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN.

  • 32 of the 33 derived commands: exit 0 (every check:*, check-sdui-manifest.mjs and its --self-test, the changeset gates, check-closing-keyword-parity.mjs, the release self-tests).
  • pnpm --dir ui exec vitest run scripts/: exit 1, 2 failed | 4899 passed; both failures are Test timed out in 15000ms in tree-wide scans run while a dev backend, a vite server and the gate loop shared the box (check-hand-rolled-comment-mask.test.ts "exits 0 as a process, which is what CI reads", check-vi-mock-inherit.test.ts "no @object-ui/i18n factory freezes the surface"). Re-run alone under the verification lock: Test Files 2 passed (2), Tests 105 passed (105). The diff adds one vi.mock('@object-ui/auth', …) factory, which spreads importOriginal().
  • Outside the derivation: pnpm --dir ui exec vitest run apps/console/src/ → Test Files 262 passed (262), Tests 2599 passed (2599); pnpm exec eslint --no-inline-config on both touched files → exit 0; pnpm --filter @object-ui/console type-check → exit 0 after pnpm --filter '@object-ui/console^...' build (a first run before that build exited 2 on unresolved @object-ui/* declarations only). The console tsconfig.json includes src with no exclude, so the test file is compiled.
  • Not run locally, CI's: the Type Check UI / workspace type-check lanes, the 11 wide-population families, the artifact rosters, pnpm lint.

Generated by Claude Code

…wn row is written

The language switch remounts LocalizationFetchProvider and writes
sys_user.locale afterwards; the remount's read could answer the old
language, so grid date cells stayed English under zh-CN until a reload.

Claude-Session: https://claude.ai/code/session_01Utp6CakU47s2AYQsrXttMo
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 29, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

席位核对

ACCEPT · head 416229c1bbe508d13cf64b35dec7475d309583ba · 2026-10-29T15:23Z

  1. Shape: draft → main, Fixes #27498 only, attribution block on top, three files (ui/apps/console/src/LocalizationFetchProvider.tsx + its test, one changeset). No assignee, no reviewer.
  2. Premise measured: the seat read the diff. LocalizationFetchProvider subscribes to useDataInvalidation('sys_user', <signed-in user id>) and re-reads GET /api/v1/auth/me/localization on each write to that row. The re-read carries its own AbortController signal, so it does not join a request still in flight from before the write. The effect re-runs on [endpoint, userRowWrites]. The single date formatter (formatDate through useDisplayLocale) is untouched. This matches triage 6495313192. No packages/spec/** path.
  3. Gates (check-runs on this head): 44 success, 11 skipped, 0 red. These include Lint & Repo Gates 123539861592, TypeScript Type Check 123539306104 and Test UI 123539344121. No commit statuses.
  4. Contract review: not owed. Clause-②: no (no exported surface changes, console app only) and Metadata-protocol: none.
  5. Accepted deviations / out of scope: one extra GET per write to the signed-in user's own row, as stated in the PR. The changeset was read sentence by sentence against the diff and matches it: FROM → TO, the race, the re-read trigger, and the opt-out from in-flight joining.
  6. Landing: not governed (check-governed-merges --pr 27624: NOT governed, 0 of 3 paths), 156 lines < 3000. The seat lands it: ready + auto-merge now.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 15:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 91e9797 Oct 29, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-b2omyd branch October 29, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

zh-CN: list grid date cells render English month abbreviations ("Oct 11", "Nov 1") while the same record's page renders 2026年10月11日

1 participant