Repository navigation
Commit 91e9797
fix(console): after a language switch, date cells format in the new language, not the stale server locale (#27624)
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgP6XkfpjCfyzzG2KwQPUZfy)_
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.
<details><summary>Targeted runs and ablations (head eda50a4, before the
main merge; re-run after it in Verification)</summary>
- `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.
</details>
## 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
<details><summary>Live reproduction, app-showcase on this branch
(backend :4127 --fresh, console vite :5197 from ui/apps/console
source)</summary>
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日`.
</details>
<details><summary>Local gates on head 416229c (after merging
origin/main); the farm is CI's</summary>
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`.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CakU47s2AYQsrXttMo)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a888ba5 commit 91e9797
3 files changed
Lines changed: 152 additions & 4 deletions
File tree
- .changeset
- ui/apps/console/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
20 | | - | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
21 | 26 | | |
22 | 27 | | |
23 | 28 | | |
| |||
26 | 31 | | |
27 | 32 | | |
28 | 33 | | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
29 | 41 | | |
30 | 42 | | |
31 | 43 | | |
| |||
255 | 267 | | |
256 | 268 | | |
257 | 269 | | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
258 | 361 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
28 | 48 | | |
29 | 49 | | |
30 | 50 | | |
31 | | - | |
| 51 | + | |
| 52 | + | |
32 | 53 | | |
33 | 54 | | |
34 | 55 | | |
| |||
62 | 83 | | |
63 | 84 | | |
64 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
65 | 90 | | |
66 | 91 | | |
67 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
68 | 97 | | |
69 | 98 | | |
70 | 99 | | |
| |||
85 | 114 | | |
86 | 115 | | |
87 | 116 | | |
| 117 | + | |
88 | 118 | | |
89 | 119 | | |
90 | 120 | | |
| |||
116 | 146 | | |
117 | 147 | | |
118 | 148 | | |
| 149 | + | |
| 150 | + | |
119 | 151 | | |
120 | 152 | | |
121 | 153 | | |
| |||
130 | 162 | | |
131 | 163 | | |
132 | 164 | | |
| 165 | + | |
133 | 166 | | |
134 | | - | |
| 167 | + | |
135 | 168 | | |
136 | 169 | | |
137 | 170 | | |
0 commit comments