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

View File

@@ -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 packages/client/locale/README.md
README.md: c2adbcabc77def740094288da4643032873aa5b8
README.zh.md: c6ecb31e21d7513a4e7d579b17588107ccd7ea59
README.md: 7f780092af9bc7079cc5080c06e986bef2dfdbce
README.zh.md: 62c037977115d33b834fe60b042431e44d208524

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Locale plugin: LocaleService — the browser locale preference (`zh`/`en`, persisted under `dsh.locale`; `locale/change` fires on switches only) plus the ns×locale dictionary registry (typed `register(ns, {zh, en})` checked against `LocaleNamespaceMap`, `bind(ns)``TranslateNS<ns>`; lookup chain ns → common → zh → key). The service implements the slot system's `LocaleFace` and installs itself through `ctx.slots.installLocale`, backing the framework-injected `t` standard seat (`Translate`/`TranslateNS` are ui-slots types; import them from there — this package only re-exports for dictionary owners' convenience).
Locale plugin: LocaleService — the browser locale preference (`zh`/`en`, persisted under `dsh.locale`; with nothing persisted a fresh browser opens in the language `navigator` asks for — matched on the primary subtag, `zh` when it asks for none this app ships; `locale/change` fires on switches only) plus the ns×locale dictionary registry (typed `register(ns, {zh, en})` checked against `LocaleNamespaceMap`, `bind(ns)``TranslateNS<ns>`; lookup chain ns → common → zh → key). The service implements the slot system's `LocaleFace` and installs itself through `ctx.slots.installLocale`, backing the framework-injected `t` standard seat (`Translate`/`TranslateNS` are ui-slots types; import them from there — this package only re-exports for dictionary owners' convenience).
## Model Experience

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
locale 插件LocaleService——浏览器 locale 偏好(`zh``en`,以 `dsh.locale` 持久化;`locale/change` 仅在切换语言时触发),加上 ns×locale 字典注册表(类型化 `register(ns, {zh, en})``LocaleNamespaceMap` 校验,`bind(ns)``TranslateNS<ns>`;查找链 ns → common → zh → key。该服务实现 slot 系统的 `LocaleFace` 并经 `ctx.slots.installLocale` 自行安装,支撑框架注入的 `t` 标准席位(`Translate``TranslateNS` 是 ui-slots 的类型;请从那里导入——本包的再导出仅为字典所有者提供便利)。
locale 插件LocaleService——浏览器 locale 偏好(`zh``en`,以 `dsh.locale` 持久化;未持久化偏好时,全新浏览器以 `navigator` 请求的语言开场——按主子标签匹配,若其请求的语言本应用都不提供则为 `zh``locale/change` 仅在切换语言时触发),加上 ns×locale 字典注册表(类型化 `register(ns, {zh, en})``LocaleNamespaceMap` 校验,`bind(ns)``TranslateNS<ns>`;查找链 ns → common → zh → key。该服务实现 slot 系统的 `LocaleFace` 并经 `ctx.slots.installLocale` 自行安装,支撑框架注入的 `t` 标准席位(`Translate``TranslateNS` 是 ui-slots 的类型;请从那里导入——本包的再导出仅为字典所有者提供便利)。
## 模型体验

View File

@@ -83,7 +83,7 @@ declare module 'cordis' {
}
}
/** Fallback locale consulted after the active locale misses (also the default). */
/** Fallback locale consulted after the active locale misses (also the last-resort initial locale). */
export const FALLBACK_LOCALE: LocaleId = 'zh'
/** Shared namespace for shell-level texts. */
@@ -123,7 +123,7 @@ export class LocaleService {
*/
constructor(ctx: Context) {
this.ctx = ctx
this.snapshot = Object.freeze({ active: restorePreference(), locales: LOCALES, revision: 0 })
this.snapshot = Object.freeze({ active: resolveInitialLocale(), locales: LOCALES, revision: 0 })
}
/**
@@ -288,17 +288,45 @@ export class LocaleService {
}
}
/** Read the persisted locale id; unknown or unreadable values fall back to zh. */
function restorePreference(): LocaleId {
/**
* The locale a fresh service opens with: an explicit preference the user
* already chose wins over the browser's own language, which in turn wins over
* {@link FALLBACK_LOCALE} (non-browser boots and browsers set to a language
* this app does not ship).
*/
function resolveInitialLocale(): LocaleId {
return restorePreference() ?? detectBrowserLocale() ?? FALLBACK_LOCALE
}
/** Read the persisted locale id; unknown or unreadable values read as no preference. */
function restorePreference(): LocaleId | undefined {
// Non-browser runs (node e2e booting the client tree) have no localStorage.
if (typeof localStorage === 'undefined') return FALLBACK_LOCALE
if (typeof localStorage === 'undefined') return undefined
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored === 'zh' || stored === 'en') return stored
} catch {
// Storage access can throw (privacy mode); the default below covers it.
// Storage access can throw (privacy mode); an unreadable store simply
// records no preference, and the browser language decides instead.
}
return FALLBACK_LOCALE
return 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.
*/
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]) {
const primary = tag.toLowerCase().split('-')[0]
const match = LOCALES.find(locale => locale.id === primary)
if (match) return match.id
}
return undefined
}
/** Persist the locale id; storage failures are non-fatal (preference resets next boot). */

View File

@@ -2,7 +2,7 @@
* Language row registration, snapshot projection into the row store, and
* recovery after an HMR collapse of the declaring entry. */
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import { apply, inject, SETTINGS_NS } from '@deepseek-ai/dsh-client-locale/client'
import type { LanguageRowInjected, LocaleService } from '@deepseek-ai/dsh-client-locale/client'
@@ -36,6 +36,16 @@ function faceOf(slots: SlotsService) {
}
describe('locale apply', () => {
// A fresh service opens in the browser's language, so these wiring specs
// pin one to keep their zh baseline independent of the test environment.
beforeEach(() => {
vi.stubGlobal('navigator', { languages: ['zh-CN'], language: 'zh-CN' })
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('declares the slot service', () => {
expect(inject).toEqual(['slots'])
})

View File

@@ -1,5 +1,5 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import type { LocaleSnapshot } from '@deepseek-ai/dsh-client-locale/client'
import { LocaleService, STORAGE_KEY } from '@deepseek-ai/dsh-client-locale/client'
@@ -11,9 +11,20 @@ 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. */
const stubLanguages = (...tags: string[]): void => {
vi.stubGlobal('navigator', { languages: tags, language: tags[0] ?? '' })
}
describe('LocaleService', () => {
beforeEach(() => {
localStorage.clear()
// A Chinese browser is the baseline these specs assert their zh state on.
stubLanguages('zh-CN')
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('translates through the active-locale -> zh -> key chain', () => {
@@ -132,23 +143,45 @@ describe('LocaleService', () => {
expect(() => { svc.setLocale('fr') }).toThrow('not registered')
})
it('restores a persisted locale and falls back to zh on garbage', () => {
it('restores a persisted locale over the browser language, and garbage reads as no preference', () => {
localStorage.setItem(STORAGE_KEY, 'en')
expect(make().svc.getLocale().active).toBe('en')
localStorage.setItem(STORAGE_KEY, 'fr')
expect(make().svc.getLocale().active).toBe('zh')
})
it('runs without localStorage (node boots): defaults on read, no-op on write', () => {
it('opens in the browser language when nothing is persisted, matching regional variants on their primary subtag', () => {
stubLanguages('en-GB', 'zh-CN')
expect(make().svc.getLocale().active).toBe('en')
stubLanguages('zh-Hant-TW')
expect(make().svc.getLocale().active).toBe('zh')
// 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).
vi.stubGlobal('navigator', { languages: [], 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', () => {
vi.stubGlobal('localStorage', undefined)
try {
const { svc } = make()
expect(svc.getLocale().active).toBe('zh')
svc.setLocale('en')
expect(svc.getLocale().active).toBe('en')
} finally {
vi.unstubAllGlobals()
}
vi.stubGlobal('navigator', undefined)
const { svc } = make()
expect(svc.getLocale().active).toBe('zh')
svc.setLocale('en')
expect(svc.getLocale().active).toBe('en')
})
it('keeps the browser language out of the way once a preference exists', () => {
stubLanguages('en-US')
const { svc } = make()
svc.setLocale('zh')
expect(localStorage.getItem(STORAGE_KEY)).toBe('zh')
expect(make().svc.getLocale().active).toBe('zh')
})
it('exposes the two shipped locales with self-described labels', () => {

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -51,6 +51,7 @@
"devDependencies": {
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-plan-mode": "workspace:^",
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",

View File

@@ -14,8 +14,8 @@
// guards would mask. Rendering-path acceptance lives in
// chat-toolview-slot.spec.tsx.
import { describe, expect, it, vi } from 'vitest'
import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { SlotTestRuntime, pinBrowserLanguages } 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'
@@ -25,6 +25,12 @@ import type {
} from '@deepseek-ai/dsh-client-ui-conversation/client'
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() })
const ROOT = 'root-1' as SessionId
type ChatInstance = ReturnType<ReturnType<typeof createChatStore>['create']>

View File

@@ -24,9 +24,15 @@ 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 } from '@deepseek-ai/dsh-client-test-runtime'
import { SlotTestRuntime, pinBrowserLanguages } 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() })
const SID = 's1' as SessionId
/** jsdom has no ResizeObserver; the composer seat publishes its height through one. */

View File

@@ -8,13 +8,19 @@
// machinery spec (chat-toolview-slot.spec.tsx) and the shell e2e; this spec
// stops at the assembly surface.
import { describe, expect, it, vi } from 'vitest'
import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { SlotTestRuntime, pinBrowserLanguages } 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'
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() })
const ROOT = 'root-1' as SessionId
const CHILD = 'child-1' as SessionId

View File

@@ -51,6 +51,7 @@
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-schema-form": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",

View File

@@ -1,13 +1,20 @@
/** Models section registration: declaration-aware deferral, the locale-following label thunk, and HMR recovery. */
import { Context } from 'cordis'
import { describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, 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 { 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() })
async function bench() {
const ctx = new Context()
await ctx.plugin(SlotsService).await()

View File

@@ -48,6 +48,7 @@
"devDependencies": {
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",

View File

@@ -1,13 +1,20 @@
/** Ownerless-copy registrations: the four seats, the dictionaries, thunked labels, and HMR recovery. */
import { Context } from 'cordis'
import { describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, 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 { 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() })
/** The four seats this plugin fills (slot name → expected component). */
const SEATS = [
['settings.trigger', TriggerContent],

View File

@@ -8,12 +8,18 @@
* holes (sidebar.workspaces / sidebar.settings) have no registrant here, so
* the snapshots pin the shell chrome itself.
*/
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, waitFor } from '@testing-library/react'
import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
import { SlotTestRuntime, pinBrowserLanguages } 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() })
afterEach(cleanup)
/**

View File

@@ -6,13 +6,20 @@
* sessionId, and unregisters on fiber teardown.
*/
import { Context } from 'cordis'
import { describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, 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 { 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'
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() })
const sid = (k: string): SessionId => k as SessionId
async function bench() {

View File

@@ -44,6 +44,7 @@
"devDependencies": {
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -2,14 +2,21 @@
* locale service, declaration-aware Appearance row registration, snapshot
* projection into the row store, and HMR collapse recovery. */
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import { afterEach, beforeEach, 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 { 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'
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() })
const SLOT = 'settings.general.item'
async function bench() {

View File

@@ -1,12 +1,19 @@
import { Context } from 'cordis'
import { describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, 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 { 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'
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() })
async function bench() {
const ctx = new Context()
await ctx.plugin(SlotsService).await()

View File

@@ -14,10 +14,16 @@ 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 } from '@deepseek-ai/dsh-client-test-runtime'
import { SlotTestRuntime, pinBrowserLanguages } 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() })
const SID = 's1' as SessionId
afterEach(cleanup)