Skip to content

fix(console): a language picked on the sign-in page formats the date cells, not the browser's language (#27625) - #27680

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

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-r5i7sq

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Requested by Bill · project thread
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.

Targeted run and ablation
  • 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 5147c4a0 → 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.

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

Live reproduction (backend :4625 --fresh, console vite :5625 from ui/apps/console source)

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.

Local gates on head 42e6d4b (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 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.

Generated by Claude Code

…27625)

LocalizationFetchProvider reads /auth/me/localization through
sharedGetJson's bare fetch, so it never carried the <html lang>
Accept-Language every other console API call carries. A user with no
sys_user.locale who picked a language on the sign-in page got the
Accept-Language rung answered in the browser's language, and date cells
formatted in it on every load. When the device holds an explicit
language choice the read now states it, and re-reads when it changes.

Claude-Session: https://claude.ai/code/session_01Utp6CXAd1Uo5DCp8mUzmWj
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 29, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

席位核对

ACCEPT · head 42e6d4b6b9654e501296ed2e65ad652d66253630 · 2026-10-29T17:48Z

  1. Shape: draft → main, Fixes #27625 only, attribution block on top. Three files: ui/apps/console/src/LocalizationFetchProvider.tsx, its test, and one changeset. No assignee, no reviewer.
  2. Premise measured:
    • The dev's measurement (step 1 of the order, before any edit). After a sign-in-page pick of 中文, <html lang> is zh. But GET /api/v1/auth/me/localization carried the browser's Accept-Language: en-US and answered en-US. The cause: this one read goes through sharedGetJson's bare fetch, not createAuthenticatedFetch's <html lang> stamp. This falsifies the PM assumption "the pick does not reach <html lang>", and the report and PR name it. It also falsifies triage 6501838569's line "the console's Accept-Language already follows <html lang>" for this read. It is not the retriage case: the header did not carry zh.
    • The seat's read of the diff. When readStoredLanguage() reports an explicit device choice, the read now states the active UI language as Accept-Language. statedLanguage is in the effect deps, so a change of choice re-reads it. The in-flight key includes the headers, so the re-read never joins a request made under the old language.
    • What is unchanged. The server's rung order and useDisplayLocale's precedence are both untouched, as the ruling requires. With no device choice the browser header still goes out, so the first-visit seed share (objectui#5544) is untouched. No server file and no packages/spec/** path.
  3. Gates (check-runs on this head):
    • 44 success, 11 skipped, 0 red. These include Lint & Repo Gates 123622978746, TypeScript Type Check 123623375351, Test UI 123622386256 and Lint · PM dispatch-gates 123619059132.
    • The dev's derived gates are 33/33 exit 0, --ran 0 NOT-MEASURED. The console src/ run passed 262 files / 2606 tests.
    • Ablation: the 3 new cases go red and the control stays green.
    • Pinned: a stored sys_user.locale still wins over the stated language. The dev also confirmed this live: ja-JP was stored, the header was zh, and the answer was ja-JP.
  4. Contract review: not owed. Clause-②: no: the change is in the console app only, with no exported surface. Metadata-protocol: none.
  5. Accepted deviations / out of scope: languageSeed.ts and the module holding the sign-in pick are unchanged. Both were in the claim's surface, and neither needed to change once the cause was measured. I read the changeset sentence by sentence against the diff, and it matches.
  6. Landing: not governed (check-governed-merges --pr 27680: 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 17:49
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 6cf3e0b Oct 29, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-r5i7sq branch October 29, 2026 18:38
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

Projects

None yet

1 participant