Repository navigation
Commit 6cf3e0b
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 #27625
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:console 读取 `/auth/me/localization` 时,若本设备有明确的语言选择(如登录页选了中文),请求的
`Accept-Language` 就写这门语言;选择变了就重读一次。服务器档位顺序与 `useDisplayLocale` 的优先级都没动。
- 为什么改:这条请求走的是裸 `fetch`,没有像其它 console
请求那样带上界面语言,于是没存语言的用户在登录页选了中文后,服务器按浏览器的 `en-US` 作答,表格日期每次加载都是 `Nov 18`。
- 风险与回滚:只动 console 一个文件;本设备有语言选择时这条请求多一个请求头,语言变化时多一次 GET。回滚即 revert 本
PR。
-
先看:`ui/apps/console/src/LocalizationFetchProvider.tsx`(`statedLanguage`
一行与 headers 里的 `Accept-Language`);`LocalizationFetchProvider.test.tsx`
新的 describe 块。
- 两分钟验证:`pnpm --dir ui exec vitest run
apps/console/src/LocalizationFetchProvider.test.tsx`,预期 22 个测试全绿,其中 4 个在
`states the device's chosen language` 块下。
- 需要你做的:无,席位落地。
## What changes
`LocalizationFetchProvider` (console) now states the device's chosen UI
language as the `Accept-Language` of its `GET
/api/v1/auth/me/localization` read whenever the device holds an explicit
language choice (`readStoredLanguage()`, the slot the sign-in page's
globe and the avatar menu both write), and reads again when that
language changes. With no choice on the device, nothing is stated and
the browser's own header goes out, as before.
**Cause, measured on a running stack (step 1 of the order).** On
`examples/app-showcase`, admin with `sys_user.locale` unset, a browser
context in `en-US`:
| | before (main 144702c) | after |
|---|---|---|
| html `lang` attribute after the sign-in page pick of 中文 | `zh` | `zh`
|
| … after sign-in, on the grid, after reload | `zh` | `zh` |
| `Accept-Language` on the localization read (grid load, reload) |
`en-US` | `zh` |
| answer | `{authenticated:true, locale:"en-US", currency:null,
timezone:"UTC"}` | `{… locale:"zh" …}` |
| All Tasks date cells, both loads | `Oct 11, Nov 18, Nov 1, Nov 10` |
`10月11日, 11月18日, 11月1日, 11月10日` |
So the pick already reached the html `lang` attribute; the read never
stated it. Every other console API call goes through
`createAuthenticatedFetch`, which stamps that attribute as
`Accept-Language`; this provider reads through `sharedGetJson`'s bare
`fetch`, so it carried the browser's header, the server answered its
`Accept-Language` rung in English, and `useDisplayLocale` formatted with
that answer.
**Within the ruling (triage 6501838569).** No server file, no new key,
no new server field; the server's rung order and `useDisplayLocale`'s
precedence are untouched. The triage's diagnosis "the read the console
makes … does not state the active UI language" is what this changes. Two
assumptions of the order were falsified by the measurement and named
here: the triage line "The console's `Accept-Language` already follows"
the html `lang` attribute does not hold for this one read, and the
sign-in pick does reach that attribute (so `languageSeed.ts` and the
module holding the pick, `@object-ui/app-shell`'s `LocaleSwitcher`
through `useLanguageSelection`, signed out a bare `changeLanguage`, are
unchanged). The seat agreed the route before the fix.
**Why gate the header on an explicit choice.** Without one, the boot is
exactly the one on which `seedTenantLanguage()` asks the same endpoint
with no header, and the two share one request only while their headers
match (`sharedGetJson` keys on them; objectui#5544). Stating a language
there would split them into two requests on every first visit. Without a
choice the UI language came from that seed or the browser, so the
browser's header is the right signal on those boots.
**Re-read on a change.** `statedLanguage` is an effect dependency, so a
change of the chosen language reads again. That read cannot join a
request still in flight under the old language: the in-flight key
includes the headers. The #27498 re-read after a write to the user's own
`sys_user` row, and its in-flight opt-out, are unchanged.
## Tests
New `describe` block `states the device's chosen language
(objectstack#27625)` in
`ui/apps/console/src/LocalizationFetchProvider.test.tsx`, under a real
`I18nProvider`, probing `useDisplayLocale()` plus
`formatDate('2026-10-11', undefined, { locale, alwaysShowYear: true })`:
- `a language picked before sign-in is the one the read states, and the
dates follow it`: explicit-choice slot `zh`, no column; the request
states `zh`, the probe shows `zh|2026年10月11日`, one request.
- `with no choice on this device, the read states nothing and the
browser's header goes out`.
- `a change of the chosen language reads again, stating the new one`:
`en` then `changeLanguage('zh')`; two requests stating `en` then `zh`,
the probe follows.
- `a stored sys_user.locale still wins over the stated language`: the
request states `zh`, the server answers its column rung `de-DE`, and the
dates format in `de-DE`, not Chinese.
The #27498 block (`re-reads after a write to the signed-in user's own
row`, 4 cases) and `localizationBootDedup.test.tsx` (objectui#5544, 3
cases) are unchanged and green.
<details><summary>Targeted run and ablation</summary>
- `pnpm --dir ui exec vitest run --reporter=verbose
apps/console/src/LocalizationFetchProvider.test.tsx
apps/console/src/__tests__/localizationBootDedup.test.tsx
apps/console/src/languageSeed.test.ts` (head 861694d) → `Test Files 3
passed (3)`, `Tests 33 passed (33)`.
- Ablation through `scripts/ablation-replace.mjs`, fix committed first:
`const statedLanguage = readStoredLanguage() ? language : undefined;` →
`… readStoredLanguage() && !'ABLATED_27625' ? language : undefined;`
(anchor 1 → 0, blob 5147c4a → a88b06c9). Red: the
picked-before-sign-in, change-reads-again and stored-column cases
(`Tests 3 failed | 19 passed (22)`); the no-choice case stays green, as
it should. Restored: blob equal to HEAD's, `git diff HEAD` empty.
- No `dist` involved: the console suite resolves the provider and
`@object-ui/*` from source.
</details>
## Acceptance notes
- Stored rung, read live on the same stack (signed-in requests, explicit
`Accept-Language`): column unset + `zh` → `locale:"zh"`; column `ja-JP`
+ `zh` → `ja-JP`; column `ja-JP` + `en-US` → `ja-JP`; column reset +
`zh` → `zh`. The column still answers first.
- A language picked on the sign-in page is still not written to
`sys_user.locale` at sign-in (signed out the globe is the bare switch,
by design in `useLanguageSelection`). Not changed here; with this fix
that no longer leaves the dates in the browser's language on this
device.
- Assignee and reviewer: none set on this PR, per the maintainer's rule
(nobody is asked before the seat's ACCEPT and review); the harness's
post-PR assign/review step was skipped deliberately.
## Verification
<details><summary>Live reproduction (backend :4625 --fresh, console vite
:5625 from ui/apps/console source)</summary>
Playwright, browser context `en-US`: open `/login`, footer globe → 中文,
sign in as admin through the form, open
`/apps/showcase_app/showcase_task`, wait, read the html `lang`
attribute, the `objectui-locale` slot, every `/auth/me/localization`
request's `Accept-Language` and its answer, and the date lines; then
reload and read them again. Before: see the table above (head 144702c).
After: the same script on the branch with the fix, the table's right
column. The one header-less localization request on `/login` before the
pick is `seedTenantLanguage()` on a device with no choice yet, answered
`{authenticated:false}`, in both runs.
</details>
<details><summary>Local gates on head 42e6d4b (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 b761968).
Reconciliation `--ran`: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN.
- All 33 derived commands: exit 0, among them `node
scripts/check-sdui-manifest.mjs` and its `--self-test`, the changeset
gates, `check-closing-keyword-parity.mjs`, and `pnpm --dir ui exec
vitest run scripts/` (`Test Files 169 passed | 2 skipped (171)`, `Tests
4902 passed | 2 skipped (4904)`, run with the dev servers stopped).
- Outside the derivation: `pnpm check:changeset-fixed` (its roster lives
under `.changeset/`) → exit 0; `pnpm --dir ui exec vitest run
apps/console/src/` → `Test Files 262 passed (262)`, `Tests 2606 passed
(2606)`; `pnpm --filter @object-ui/console run type-check` → exit 0
after `pnpm --filter '@object-ui/console^...' build`; `pnpm exec eslint
--no-inline-config` on both touched files → exit 0.
- 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_01Utp6CXAd1Uo5DCp8mUzmWj)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent aad9ad5 commit 6cf3e0b
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 | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
21 | 28 | | |
22 | 29 | | |
23 | 30 | | |
| |||
358 | 365 | | |
359 | 366 | | |
360 | 367 | | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
361 | 464 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
48 | 68 | | |
49 | 69 | | |
50 | | - | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
51 | 77 | | |
52 | 78 | | |
53 | 79 | | |
| |||
87 | 113 | | |
88 | 114 | | |
89 | 115 | | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
90 | 120 | | |
91 | 121 | | |
92 | 122 | | |
| |||
113 | 143 | | |
114 | 144 | | |
115 | 145 | | |
116 | | - | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
117 | 150 | | |
118 | 151 | | |
119 | 152 | | |
| |||
164 | 197 | | |
165 | 198 | | |
166 | 199 | | |
167 | | - | |
| 200 | + | |
168 | 201 | | |
169 | 202 | | |
170 | 203 | | |
0 commit comments