Repository navigation
Commit 6fa842a
fix(app-shell): keep a language picked on the sign-in page when someone else last used the browser (#30397)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg8EjqGExu8hoAXQLwFvdu5t)_
domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG
## 维护者速读
- 改了什么:在登录页选的语言,换了一个人登录后不再被清掉,进系统后仍是刚选的语言。
- 为什么改:共用电脑上,上一个人登录过,下一个人在登录页选法语,进去却变回中文,选择还被删掉(#30338 第 3 行)。
- 风险与回滚:只改前端一个文件;上一个人留下的语言照旧清除(objectui#10193 不变)。回滚即撤本 PR。
- 先看:`useUserLocale.ts` 里新增的 `keepSignedOutPickThroughOwnerChange`,以及
`useLanguageSelection` 未登录分支。
- 两分钟验证:浏览器先用 A 登录再退出,在登录页选 Français,用 B 登录,页面应是法语。
- 需要你做的:无,席位落地。
Refs #30338 (item: keep the sign-in choice)
Clause-②: no
Metadata-protocol: none
## What changes
`@object-ui/app-shell`, `useUserLocale.ts` only.
- `useLanguageSelection`, signed out, now remembers the picked language
for this page-load (module scope, memory only) and installs one listener
on `subscribeSessionOwnerChange`.
- When `SessionUserScope.adopt` drops the previous owner's state (the
event fires after the sweep), the listener writes the remembered pick
back into the existing `objectui-locale` slot. The next page-load boots
from it like any choice.
- The pick is handed to one sign-in only: the listener consumes it, a
signed-in switch forgets it, and the shell's first successful `sys_user`
read forgets it, so it cannot reach a later owner.
No new storage key, no new export from the package barrel, no request
added.
## Why
Measured on `origin/main` `fdad1786`, the card's premise does not hold:
`useLanguageResetOnOwnerChange` is not on the failing path. It runs
inside `useSignedInUserLocale`, which the console mounts only in
`AppContent` (the `/apps/:appName/*` route), and the sign-in page is
outside it. The drop is in the sign-in page-load itself:
1. The footer `LocaleSwitcher` switch persists `objectui-locale` through
the i18n provider's `languageChanged` choke point.
2. `signIn` sets the user; `AuthProvider` calls
`SessionUserScope.adopt(B)`; the persisted pointer names A, so
`purgePreviousUserClientState` sweeps every key outside
`DEVICE_SCOPED_KEYS`, the pick included.
3. `LoginPage`'s post-login effect leaves by `window.location.assign`.
The next page-load has no choice and no seed, so it boots in the browser
language. Its pointer already names B, so no owner change fires there.
The card asked whether the previous person's residue and a pick made
after they signed out can be told apart without a new stored marker. For
keeping the pick, they can: the pick and the sweep happen in the same
page-load, so the pick is known in memory. objectui#10193 ruling B is
unchanged: with no pick since, the previous person's language is still
swept (pinned below).
## Acceptance notes
- **Bound:** memory lasts one page-load. A pick followed by a reload of
the sign-in page, or by a sign-in that leaves the page first (an
identity provider's redirect), is still swept.
- **Not in this PR, by the seat's ruling:** "saved as their own
preference if they have none". The knowledge that the slot came from a
sign-in pick ends at the post-login full navigation, and in the next
page-load "column unset and slot set" is not a usable signal: the
profile card's "use the deployment default" writes `locale: null` and
leaves the slot set, so saving the slot there would undo that choice on
every boot. The seat routes the save half to its own card (await the
existing `sys_user` save before the sign-in page navigates), blocked by
#30157, which holds the sign-in page files. A new stored marker was not
adopted. The save half is #30399.
- **Sign-in language list left as it is.** Signed out, the console
requests no list (objectui#12034), so the menu offers the built-in
packs. #30001's `PLATFORM_LOCALES` is a static platform table, not the
deployment's list (the card's deployment offers `vi`, which neither the
table nor the built-ins have), and moving the menu onto it is phase 4 of
#29428, its own card. The fix does not need the list. Row 4 measures
unchanged (below).
- A column the person already has still wins after sign-in, as
objectui#10059 rules; the kept pick only decides while the column is
unset.
## Tests
New
`packages/app-shell/src/hooks/__tests__/userLocaleSignInPick-30338.test.tsx`,
five cases: B picks then signs in (pick stored, A's seed swept); control
with no pick (A's language swept, ruling B); the pick does not come back
at a later owner change; a signed-in switch replaces it; the shell
mounted with no navigation keeps it live and stored after the reset.
<details><summary>vitest, the locale hook files and the LocaleSwitcher
layout suite (merged head 1ec0fe4)</summary>
```
pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/hooks/__tests__/userLocaleSignInPick-30338.test.tsx packages/app-shell/src/hooks/__tests__/userLocaleOwnerChange-10193.test.tsx packages/app-shell/src/hooks/__tests__/userLocale-10059.test.tsx packages/app-shell/src/layout/
Test Files 143 passed (143)
Tests 926 passed (926)
os-verify-lock: VERDICT command-exit 0
```
</details>
<details><summary>Ablation: useUserLocale.ts set to origin/main, new
file run, then restored from HEAD (hash checked, git diff HEAD
empty)</summary>
```
× B signs in after picking: the pick stays stored for the next page-load, A’s seed does not
× the pick goes to the next sign-in only: it does not come back at a later change of owner
× with the shell mounted (no navigation), B keeps the pick live and stored after the reset
Test Files 1 failed (1)
Tests 3 failed | 2 passed (5)
os-verify-lock: VERDICT command-exit 1
```
The two that stay green are the ruling-B control and the
signed-in-switch guard, both expected to pass with or without the fix.
</details>
<details><summary>Type-check</summary>
```
pnpm exec turbo run type-check --filter=@object-ui/app-shell --concurrency=2
Tasks: 68 successful, 68 total
os-verify-lock: VERDICT command-exit 0
```
`@object-ui/app-shell`'s `type-check` runs `tsc --noEmit` and `tsc -p
tsconfig.test.json`, so the new test file is type-checked.
</details>
## Verification
Local boot: `examples/app-showcase` `objectstack dev --seed-admin
--fresh`, console from `ui/apps/console` via vite, Playwright with
browser language `zh-CN`. A = the seeded admin, B = a second user. Each
row is a fresh browser context; "signed out" is the auth sign-out call
plus the session token and cookies removed (the owner pointer stays, as
on a real sign-out). The before run swapped `useUserLocale.ts` to
`origin/main` and restored it from HEAD.
| Case | Choice | Before (origin/main) | After (this PR) |
|:--|:--|:--|:--|
| Same person who last used the browser | Français | fr, slot fr | fr,
slot fr |
| Clean browser | Français | fr, slot fr | fr, slot fr |
| A different person last used the browser | Français | **zh-CN, slot
removed** | **fr, slot fr** (and fr after a reload) |
| Control: different person, no pick (A had picked Deutsch) | none |
zh-CN, slot removed | zh-CN, slot removed |
| Clean browser | Deutsch | de; locales en, zh-CN | de; locales en,
zh-CN (unchanged, list left alone) |
`sys_user.locale` read back empty in every row before and after: the
save half is not in this PR.
<details><summary>Derived gates</summary>
On head `1ec0fe4c` (after merging `origin/main` `870ebce3`), `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, each exit code captured before any pipe:
```
exit 0 node scripts/check-adr-0087-registration.mjs --base origin/main
exit 0 node scripts/check-adr-0087-registration.mjs --self-test
exit 0 node scripts/check-changeset-no-major.mjs --base origin/main
exit 0 node scripts/check-closing-keyword-parity.mjs
exit 0 node scripts/check-closing-keyword-parity.mjs --self-test
exit 0 node scripts/check-comment-mask-corpus.mjs
exit 0 node scripts/check-empty-changeset.mjs --base origin/main
exit 0 node scripts/check-empty-changeset.mjs --self-test
exit 0 node scripts/check-sdui-manifest.mjs
exit 0 node scripts/check-sdui-manifest.mjs --self-test
exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test
exit 0 node scripts/release-pending-publish.mjs --self-test
exit 0 pnpm --dir ui exec vitest run scripts/
exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys
exit 0 pnpm check:changeset-gate-self-tests
exit 0 pnpm check:closed-ceremony-text
exit 0 pnpm check:cross-package-test-inputs
exit 0 pnpm check:dispatcher-error-vocabulary
exit 0 pnpm check:driver-memory-census
exit 0 pnpm check:engine-double-contract
exit 0 pnpm check:gitlink-declared
exit 0 pnpm check:nul-bytes
exit 0 pnpm check:published-files
exit 0 pnpm check:query-options-erasure
exit 0 pnpm check:refd-timer-probe
exit 0 pnpm check:registry-log-declared
exit 0 pnpm check:rest-log-spy-declared
exit 0 pnpm check:skill-identifier-liveness
exit 0 pnpm check:test-source-alias
exit 0 pnpm check:tier-file-adoption
exit 0 pnpm check:type-check-coverage
exit 0 pnpm check:type-check-debt
exit 0 pnpm check:undeclared-dep-imports
exit 0 pnpm check:watch-hint-literal
exit 0 pnpm check:where-matcher
```
`dispatch-gates --ran`: 35 derived famil(ies) accounted for, 35 run, 0
NOT-MEASURED. The type-check lanes, wide-population and roster families
are CI's run.
main was red at dispatch on `check:liveness` (`query/searchFields`,
#30367, fix in PR #30368, not landed at this merge); a `Lint UI` red on
that row is not this PR's.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [ ] 3. No formatter ran; `pnpm lint` passes. (no formatter ran; `pnpm
lint` is not run locally, it is CI's run)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (not touched)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (the maintainer brief
above is in Chinese by the repo's PR rule)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CZGT41gdDg2656as2b)_
<!-- signed: domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG
-->
Co-authored-by: Claude <noreply@anthropic.com>1 parent 54b491a commit 6fa842a
3 files changed
Lines changed: 344 additions & 5 deletions
File tree
- .changeset
- ui/packages/app-shell/src/hooks
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
63 | | - | |
64 | | - | |
65 | | - | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
66 | 68 | | |
67 | 69 | | |
68 | 70 | | |
| |||
87 | 89 | | |
88 | 90 | | |
89 | 91 | | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
90 | 164 | | |
91 | 165 | | |
92 | 166 | | |
| |||
304 | 378 | | |
305 | 379 | | |
306 | 380 | | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
307 | 384 | | |
308 | 385 | | |
309 | 386 | | |
| |||
370 | 447 | | |
371 | 448 | | |
372 | 449 | | |
373 | | - | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
374 | 453 | | |
375 | 454 | | |
376 | 455 | | |
| |||
389 | 468 | | |
390 | 469 | | |
391 | 470 | | |
392 | | - | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
393 | 477 | | |
394 | 478 | | |
395 | 479 | | |
| |||
0 commit comments