feat(locale): derive the initial Settings language from the browser

A first visit resolved to Chinese regardless of the browser: LocaleService
read `dsh.locale` and fell straight back to `zh` when nothing was stored,
ignoring the languages the browser already states it reads.

The initial locale now resolves through three ordered sources — the persisted
preference, then `navigator` (first entry of the ordered language list whose
primary subtag names a shipped locale, so `zh-Hans-CN` -> zh and `en-GB` ->
en), then `FALLBACK_LOCALE`. An explicit choice still wins and nothing writes
the detected locale back to storage, so "has the user chosen?" stays a
question only the stored value answers.

Specs asserting the shipped Chinese copy now state the browser they assume:
the web e2e scenarios open their page with `locale: ZH_BROWSER_LOCALE`, and
package specs pin it through the new `pinBrowserLanguages` test helper.
`settings-chrome.e2e.ts` gains an English-browser scenario as the
assembled-app proof.
This commit is contained in:
creatixchu
2026-07-31 15:26:46 +08:00
parent ef01e2e9af
commit cb754a0319
39 changed files with 350 additions and 55 deletions
@@ -38,6 +38,7 @@ export { TestWorkspaces } from './workspaces.ts'
export { conversationSnapshot, workspaceListState } from './fixtures.ts'
export type { SessionBehaviorOverrides, SessionFixture, Stabilizer } from './fixtures.ts'
export { makeTranslate } from './translate.ts'
export { pinBrowserLanguages } from './locale-env.ts'
/** Erased register face for the internal root call (the public declare seam holds the typing). */
type ErasedRegister = (options: object, component: unknown) => () => void
@@ -0,0 +1,25 @@
/**
* Browser-language pin for specs that assert localized copy. A fresh
* LocaleService with no stored preference opens in the language `navigator`
* asks for, and jsdom reports the runner's own (`en-US`) — so a spec asserting
* the product's Chinese copy states the browser it assumes instead of
* inheriting the machine's.
*/
/**
* Override `navigator.languages`/`navigator.language` for the current spec.
* @param primary - most preferred BCP 47 tag; also becomes `navigator.language`.
* @param rest - further tags in preference order.
* @returns restore function handing the properties back to the environment.
*/
export function pinBrowserLanguages(primary: string, ...rest: string[]): () => void {
Object.defineProperty(navigator, 'languages', { value: [primary, ...rest], configurable: true })
Object.defineProperty(navigator, 'language', { value: primary, configurable: true })
return () => {
// Deleting the own properties uncovers the environment's own accessors
// again (Navigator declares both readonly, hence the erased receiver).
const own = navigator as unknown as Record<string, unknown>
delete own.languages
delete own.language
}
}