diff --git a/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.i18n.yaml index 65fdff8572..d1fb6cb6c7 100644 --- a/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.md -2026-07-31-browser-derived-initial-locale.md: 7668171f32642248cc0be92741eb3dc90e0669cc -2026-07-31-browser-derived-initial-locale.zh.md: 568737285e3c00d6c9792a0e6e79e38428db18c3 +2026-07-31-browser-derived-initial-locale.md: 0c49a6bbfec0ab33a5aa3ce53dde0cac747f3816 +2026-07-31-browser-derived-initial-locale.zh.md: c013d24dcd3bb49d176eaddd42ff41dde320ff1f diff --git a/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.md b/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.md index 7668171f32..0c49a6bbfe 100644 --- a/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.md +++ b/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.md @@ -12,7 +12,9 @@ The Settings Language row opened every first visit in Chinese: `LocaleService` r **The initial locale resolves through three ordered sources: the persisted preference, then the browser, then `FALLBACK_LOCALE`.** `resolveInitialLocale()` in `packages/client/locale/src/client/index.ts` runs at service construction and is the only place the order is expressed; `restorePreference()` now returns `LocaleId | undefined` (an absent, unparseable, or unreachable store reads as *no preference*) so the next source can speak. -**Browser matching is on the primary subtag, over the ordered list.** `detectBrowserLocale()` walks `[...navigator.languages, navigator.language]` and returns the first entry whose primary subtag names a shipped locale, so `zh-Hans-CN` and `zh-TW` both land on `zh` and `en-GB` on `en`, while a browser asking only for languages this app does not ship (`fr`, `de`) yields nothing and leaves `FALLBACK_LOCALE` in charge. `navigator.language` trails the list because a browser may expose only the former; a runtime without `navigator` at all (node booting the client tree) resolves to the fallback like any unresolvable case. +**Browser matching is on the primary subtag, over the ordered list.** `detectBrowserLocale()` walks `[...(navigator.languages ?? []), navigator.language]` and returns the first entry whose primary subtag names a shipped locale, so `zh-Hans-CN` and `zh-TW` both land on `zh` and `en-GB` on `en`, while a browser asking only for languages this app does not ship (`fr`, `de`) yields nothing and leaves `FALLBACK_LOCALE` in charge. `navigator.language` trails the list and covers its absence on hosts that ship a Navigator without `languages` — the DOM lib types it as always present, so that tolerance carries a narrow lint exception, the same environment-boundary distrust the `localStorage` guards already express. + +**`window`, not `navigator`, is the browser test.** Node ≥ 21 exposes a global `navigator` reporting the machine's own language (`en-US` on the CI runners), so gating on `navigator` would have let a node boot of the client tree resolve to `en` instead of the documented fallback. Gating on `window` keeps every non-browser run on `FALLBACK_LOCALE`. **An explicit choice is permanent.** `setLocale` persistence is untouched, and the persisted value is consulted first, so a user who picked a language keeps it even when travelling between browser profiles or system languages. Nothing writes the detected locale back to storage: detection is re-derived every boot and stays invisible to the "has the user chosen?" question. @@ -30,5 +32,5 @@ The Settings Language row opened every first visit in Chinese: `LocaleService` r - A first visit from an English browser lands in English, and the Language row still shows the same two self-described options, so the escape hatch is unchanged in either direction. - `FALLBACK_LOCALE` narrows to its real job — the dictionary fallback and the no-signal answer — and stops standing in for "the user has not chosen". -- Tests that construct a `LocaleService` under jsdom now depend on the environment's `navigator`: the package specs stub it (`zh-CN` as the baseline; the detection spec varies it), and any future spec asserting a default must pin one too. +- Tests that construct a `LocaleService` under jsdom now depend on the environment's `navigator`: specs asserting localized copy declare their browser with one suite-level `usePinnedBrowserLanguages('zh-CN')` (dsh-client-test-runtime), and any future spec asserting a default must do the same. This package's own specs stub the globals directly, because they need shapes the helper deliberately cannot express (absent `languages`, a list decoupled from `language`, no `window` at all). - Detection cost is one array walk per service construction, and no storage write, so boot behavior and the persisted-state surface are unchanged. diff --git a/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.zh.md b/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.zh.md index 568737285e..c013d24dcd 100644 --- a/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.zh.md @@ -12,7 +12,9 @@ Status: implemented **初始 locale 依次经三个来源解析:已持久化的偏好、浏览器、`FALLBACK_LOCALE`。** `packages/client/locale/src/client/index.ts` 中的 `resolveInitialLocale()` 在服务构造时运行,是这一顺序的唯一表达处;`restorePreference()` 现在返回 `LocaleId | undefined`(存储项缺失、无法解析或不可访问,一律读作*没有偏好*),后一个来源才有开口的机会。 -**浏览器匹配按主子标签进行,且遍历有序列表。** `detectBrowserLocale()` 遍历 `[...navigator.languages, navigator.language]`,返回主子标签命中已提供 locale 的首个条目,因此 `zh-Hans-CN` 与 `zh-TW` 同归 `zh`、`en-GB` 归 `en`;而只请求本应用不提供的语言(`fr`、`de`)的浏览器则什么都匹配不到,交由 `FALLBACK_LOCALE` 接管。`navigator.language` 排在列表之后,因为有的浏览器只暴露前者;完全没有 `navigator` 的运行环境(node 启动客户端树)与任何无法解析的情形一样落到回落值。 +**浏览器匹配按主子标签进行,且遍历有序列表。** `detectBrowserLocale()` 遍历 `[...(navigator.languages ?? []), navigator.language]`,返回主子标签命中已提供 locale 的首个条目,因此 `zh-Hans-CN` 与 `zh-TW` 同归 `zh`、`en-GB` 归 `en`;而只请求本应用不提供的语言(`fr`、`de`)的浏览器则什么都匹配不到,交由 `FALLBACK_LOCALE` 接管。`navigator.language` 排在列表之后,并兜住那些 Navigator 上没有 `languages` 的宿主——DOM 库把它标注为必然存在,所以这份容忍带一条窄口径 lint 例外,与 `localStorage` 守卫表达的环境边界不信任同源。 + +**判定浏览器用的是 `window` 而非 `navigator`。** Node ≥ 21 暴露全局 `navigator` 并报告机器自身语言(CI runner 上是 `en-US`),因此以 `navigator` 把关会让 node 启动客户端树时解析成 `en`,而非文档约定的回落值。以 `window` 把关可使所有非浏览器运行都停留在 `FALLBACK_LOCALE`。 **显式选择是永久的。** `setLocale` 的持久化未作改动,且持久化值最先被查询,因此选过语言的用户即便在不同浏览器配置或系统语言之间辗转也保留原选择。没有任何代码把探测到的 locale 写回存储:探测在每次启动时重新推导,对"用户是否做过选择"这一问题始终不可见。 @@ -30,5 +32,5 @@ Status: implemented - 来自英文浏览器的首访落在英文界面,而语言行依然呈现同样两个以自身语言自述的选项,两个方向的脱身通道都未改变。 - `FALLBACK_LOCALE` 收窄回它真正的职责——字典回落与无信号时的答案——不再兼职充当"用户尚未选择"。 -- 在 jsdom 下构造 `LocaleService` 的测试现在依赖环境的 `navigator`:包内测试对其打桩(以 `zh-CN` 为基线,探测用例则逐个变换),今后任何断言默认值的用例同样必须钉住它。 +- 在 jsdom 下构造 `LocaleService` 的测试现在依赖环境的 `navigator`:断言本地化文案的用例以一行套件级 `usePinnedBrowserLanguages('zh-CN')`(dsh-client-test-runtime)声明其浏览器,今后任何断言默认值的用例同样如此。本包自己的用例直接给全局打桩,因为它们需要该 helper 刻意不表达的形状(`languages` 缺失、列表与 `language` 解耦、完全没有 `window`)。 - 探测的代价是每次服务构造遍历一次数组,且不写存储,因此启动行为与持久化状态面均无变化。 diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index c9d6bc3367..a1d77657b8 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -232,7 +232,10 @@ describe('web e2e: settings modal and General preferences', () => { const dialog = enPage.getByRole('dialog', { name: 'Settings' }) await dialog.waitFor({ timeout: 10_000 }) await dialog.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 }) + // This page has no closing inventory spec to sweep its console, so the + // scenario clears both tripwire channels itself. expect(enTripwire.pageErrors).toEqual([]) + expect(enTripwire.warnings).toEqual([]) } finally { await enPage.close() } diff --git a/packages/client/locale/src/client/index.ts b/packages/client/locale/src/client/index.ts index 83fa3a48a9..c5e54c5be9 100644 --- a/packages/client/locale/src/client/index.ts +++ b/packages/client/locale/src/client/index.ts @@ -315,13 +315,20 @@ function restorePreference(): LocaleId | undefined { /** * The first shipped locale the browser asks for, matched on the primary * subtag so every regional variant lands on its language (`zh-Hans-CN` -> zh, - * `en-GB` -> en). `navigator.language` trails the ordered `languages` list - * because a browser may expose only the former. + * `en-GB` -> en). `window` is the browser test, not `navigator`: Node exposes + * a global `navigator` reporting the machine's own language, which would + * otherwise decide the locale for non-browser runs (node e2e booting the + * client tree). `navigator.language` trails the ordered `languages` list and + * covers its absence on hosts that expose only the single tag. */ function detectBrowserLocale(): LocaleId | undefined { - // Non-browser runs (node e2e booting the client tree) have no navigator. - if (typeof navigator === 'undefined') return undefined - for (const tag of [...navigator.languages, navigator.language]) { + if (typeof window === 'undefined') return undefined + /* oxlint-disable-next-line typescript/no-unnecessary-condition -- + * The DOM lib types `languages` as always present; embedders and older + * WebViews ship a Navigator without it, and spreading undefined would + * throw at boot. Same environment-boundary distrust as the localStorage + * guards below. */ + for (const tag of [...(navigator.languages ?? []), navigator.language]) { const primary = tag.toLowerCase().split('-')[0] const match = LOCALES.find(locale => locale.id === primary) if (match) return match.id diff --git a/packages/client/locale/tests/locale.spec.ts b/packages/client/locale/tests/locale.spec.ts index fcab3683bc..442701dbb3 100644 --- a/packages/client/locale/tests/locale.spec.ts +++ b/packages/client/locale/tests/locale.spec.ts @@ -11,7 +11,13 @@ const make = (): { ctx: Context; svc: LocaleService; events: LocaleSnapshot[] } return { ctx, svc: new LocaleService(ctx), events } } -/** Pin the browser environment a fresh service reads its initial locale from. */ +/** + * Pin the browser environment a fresh service reads its initial locale from. + * This package's own specs stub the globals directly instead of using + * `usePinnedBrowserLanguages` (dsh-client-test-runtime): they need the shapes + * that helper deliberately cannot express — a missing `languages` list, a + * list decoupled from `language`, and a non-browser run with no `window`. + */ const stubLanguages = (...tags: string[]): void => { vi.stubGlobal('navigator', { languages: tags, language: tags[0] ?? '' }) } @@ -158,18 +164,24 @@ describe('LocaleService', () => { // An unshipped language walks the list to the first one this app ships. stubLanguages('fr-FR', 'en-US') expect(make().svc.getLocale().active).toBe('en') - // Only `language` populated (browsers that expose no ordered list). + // Only `language` populated: an empty ordered list, and a host that + // exposes no `languages` property at all. vi.stubGlobal('navigator', { languages: [], language: 'en-US' }) expect(make().svc.getLocale().active).toBe('en') + vi.stubGlobal('navigator', { language: 'en-US' }) + expect(make().svc.getLocale().active).toBe('en') // No shipped language anywhere in the browser's preferences: zh remains // the product default rather than an arbitrary near-match. stubLanguages('fr-FR', 'de') expect(make().svc.getLocale().active).toBe('zh') }) - it('runs without localStorage or navigator (node boots): defaults on read, no-op on write', () => { + it('runs outside a browser (node boots): the fallback decides, the machine language does not, writes no-op', () => { vi.stubGlobal('localStorage', undefined) - vi.stubGlobal('navigator', undefined) + vi.stubGlobal('window', undefined) + // Node exposes its own global navigator; without a window it must not + // reach the resolution at all. + stubLanguages('en-US') const { svc } = make() expect(svc.getLocale().active).toBe('zh') svc.setLocale('en') diff --git a/packages/client/test-runtime/src/index.ts b/packages/client/test-runtime/src/index.ts index 6e227539cd..5ef5350434 100644 --- a/packages/client/test-runtime/src/index.ts +++ b/packages/client/test-runtime/src/index.ts @@ -38,7 +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' +export { usePinnedBrowserLanguages } 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 diff --git a/packages/client/test-runtime/src/locale-env.ts b/packages/client/test-runtime/src/locale-env.ts index ff961e3888..f938557132 100644 --- a/packages/client/test-runtime/src/locale-env.ts +++ b/packages/client/test-runtime/src/locale-env.ts @@ -5,21 +5,25 @@ * the product's Chinese copy states the browser it assumes instead of * inheriting the machine's. */ +import { afterEach, beforeEach } from 'vitest' /** - * Override `navigator.languages`/`navigator.language` for the current spec. + * Pin `navigator.languages`/`navigator.language` for every test in the + * calling file (or describe block), restoring the environment's own values + * afterwards. Call at suite level, like the other vitest hooks. * @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 () => { +export function usePinnedBrowserLanguages(primary: string, ...rest: string[]): void { + beforeEach(() => { + Object.defineProperty(navigator, 'languages', { value: [primary, ...rest], configurable: true }) + Object.defineProperty(navigator, 'language', { value: primary, configurable: true }) + }) + afterEach(() => { // 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 delete own.languages delete own.language - } + }) } diff --git a/packages/client/ui-conversation/tests/apply-inject.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.spec.tsx index bb0b1ce6f3..45a944b393 100644 --- a/packages/client/ui-conversation/tests/apply-inject.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.spec.tsx @@ -14,8 +14,8 @@ // guards would mask. Rendering-path acceptance lives in // chat-toolview-slot.spec.tsx. -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { SlotTestRuntime, pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { describe, expect, it, vi } from 'vitest' +import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import type { SessionBehaviorOverrides } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import type { ISession, SessionId } from '@deepseek-ai/dsh-client-runtime/client' @@ -27,9 +27,7 @@ import type { createChatStore } from '../src/client/stores.ts' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') const ROOT = 'root-1' as SessionId diff --git a/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx index 9c00621809..3ddaef873e 100644 --- a/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx +++ b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx @@ -24,14 +24,12 @@ import { cleanup, fireEvent, waitFor, within } from '@testing-library/react' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import type { ISession, SessionId, TodoItem, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' -import { SlotTestRuntime, pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { apply, inject } from '@deepseek-ai/dsh-client-ui-conversation/client' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') const SID = 's1' as SessionId diff --git a/packages/client/ui-conversation/tests/chat-apply.spec.tsx b/packages/client/ui-conversation/tests/chat-apply.spec.tsx index d17c6a022a..c8efd38a71 100644 --- a/packages/client/ui-conversation/tests/chat-apply.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-apply.spec.tsx @@ -8,8 +8,8 @@ // machinery spec (chat-toolview-slot.spec.tsx) and the shell e2e; this spec // stops at the assembly surface. -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { SlotTestRuntime, pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { describe, expect, it, vi } from 'vitest' +import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' @@ -17,9 +17,7 @@ import { apply, inject } from '@deepseek-ai/dsh-client-ui-conversation/client' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') const ROOT = 'root-1' as SessionId const CHILD = 'child-1' as SessionId diff --git a/packages/client/ui-models/tests/apply.spec.ts b/packages/client/ui-models/tests/apply.spec.ts index e4e7287502..1f44cb487c 100644 --- a/packages/client/ui-models/tests/apply.spec.ts +++ b/packages/client/ui-models/tests/apply.spec.ts @@ -1,19 +1,17 @@ /** Models section registration: declaration-aware deferral, the locale-following label thunk, and HMR recovery. */ import { Context } from 'cordis' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' -import { pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { apply, inject, refreshIfLoaded } from '@deepseek-ai/dsh-client-ui-models/client' import { ModelsSection } from '../src/client/ModelsSection.tsx' import { DeepSeekOnboardingDialog } from '../src/client/DeepSeekOnboardingDialog.tsx' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') async function bench() { const ctx = new Context() diff --git a/packages/client/ui-settings-general/tests/apply.spec.ts b/packages/client/ui-settings-general/tests/apply.spec.ts index 0089bc3467..5678821c20 100644 --- a/packages/client/ui-settings-general/tests/apply.spec.ts +++ b/packages/client/ui-settings-general/tests/apply.spec.ts @@ -1,19 +1,17 @@ /** Ownerless-copy registrations: the four seats, the dictionaries, thunked labels, and HMR recovery. */ import { Context } from 'cordis' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' -import { pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client' import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx' import { GeneralSection } from '../src/client/GeneralSection.tsx' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') /** The four seats this plugin fills (slot name → expected component). */ const SEATS = [ diff --git a/packages/client/ui-sidebar/tests/sidebar-snapshot.spec.tsx b/packages/client/ui-sidebar/tests/sidebar-snapshot.spec.tsx index dece40302b..847e6ca6e5 100644 --- a/packages/client/ui-sidebar/tests/sidebar-snapshot.spec.tsx +++ b/packages/client/ui-sidebar/tests/sidebar-snapshot.spec.tsx @@ -8,17 +8,15 @@ * holes (sidebar.workspaces / sidebar.settings) have no registrant here, so * the snapshots pin the shell chrome itself. */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, waitFor } from '@testing-library/react' -import { SlotTestRuntime, pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import { apply, inject } from '@deepseek-ai/dsh-client-ui-sidebar/client' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') afterEach(cleanup) diff --git a/packages/client/ui-slash/tests/apply.spec.ts b/packages/client/ui-slash/tests/apply.spec.ts index eb15feaa54..7121f79441 100644 --- a/packages/client/ui-slash/tests/apply.spec.ts +++ b/packages/client/ui-slash/tests/apply.spec.ts @@ -6,9 +6,9 @@ * sessionId, and unregisters on fiber teardown. */ import { Context } from 'cordis' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' -import { pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { createScope, scopeOf, SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' import { apply, inject, SlashService } from '@deepseek-ai/dsh-client-ui-slash/client' @@ -16,9 +16,7 @@ import type { MenuViewInjected } from '@deepseek-ai/dsh-client-ui-slash/client' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') const sid = (k: string): SessionId => k as SessionId diff --git a/packages/client/ui-theme/tests/apply.spec.ts b/packages/client/ui-theme/tests/apply.spec.ts index 59e6e8924e..a4da553516 100644 --- a/packages/client/ui-theme/tests/apply.spec.ts +++ b/packages/client/ui-theme/tests/apply.spec.ts @@ -2,10 +2,10 @@ * locale service, declaration-aware Appearance row registration, snapshot * projection into the row store, and HMR collapse recovery. */ import { Context } from 'cordis' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' -import { pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { apply, inject, SETTINGS_NS } from '@deepseek-ai/dsh-client-ui-theme/client' import type { AppearanceRowInjected, ThemeService } from '@deepseek-ai/dsh-client-ui-theme/client' import { AppearanceRow } from '../src/client/AppearanceRow.tsx' @@ -13,9 +13,7 @@ import type { createAppearanceRowStore } from '../src/client/settings-store.ts' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') const SLOT = 'settings.general.item' diff --git a/packages/client/ui-workspace/tests/apply.spec.ts b/packages/client/ui-workspace/tests/apply.spec.ts index 28153e607d..4c62678f4c 100644 --- a/packages/client/ui-workspace/tests/apply.spec.ts +++ b/packages/client/ui-workspace/tests/apply.spec.ts @@ -1,8 +1,8 @@ import { Context } from 'cordis' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' -import { pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { apply, inject } from '@deepseek-ai/dsh-client-ui-workspace/client' import type { WorkspaceBrowserInjected, WorkspacePickerInjected } from '@deepseek-ai/dsh-client-ui-workspace/client' import { WorkspaceBrowser } from '../src/client/WorkspaceBrowser.tsx' @@ -10,9 +10,7 @@ import { WorkspacePicker } from '../src/client/WorkspacePicker.tsx' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') async function bench() { const ctx = new Context() diff --git a/packages/client/ui-workspace/tests/rename-assembly.spec.tsx b/packages/client/ui-workspace/tests/rename-assembly.spec.tsx index 9138b88a6c..44b812532e 100644 --- a/packages/client/ui-workspace/tests/rename-assembly.spec.tsx +++ b/packages/client/ui-workspace/tests/rename-assembly.spec.tsx @@ -14,15 +14,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, waitFor, within } from '@testing-library/react' import type { ISession, SessionId, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' -import { SlotTestRuntime, pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import { apply, inject } from '@deepseek-ai/dsh-client-ui-workspace/client' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') const SID = 's1' as SessionId diff --git a/packages/host/directory-picker-browse/tests/client-flow.spec.tsx b/packages/host/directory-picker-browse/tests/client-flow.spec.tsx index 57e8c009cb..cde35bff03 100644 --- a/packages/host/directory-picker-browse/tests/client-flow.spec.tsx +++ b/packages/host/directory-picker-browse/tests/client-flow.spec.tsx @@ -1,20 +1,18 @@ // @vitest-environment jsdom import { Context } from 'cordis' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render, screen } from '@testing-library/react' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import type { DirectoryListing } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' -import { pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import type { DirectoryFlowOwnerProps } from '@deepseek-ai/dsh-client-ui-workspace/client' import { apply, inject } from '../src/client/index.ts' import { BrowseDirectoryFlow } from '../src/client/flow.ts' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. -let restoreLanguages: () => void -beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') }) -afterEach(() => { restoreLanguages() }) +usePinnedBrowserLanguages('zh-CN') afterEach(cleanup)