Files
apes-sidebar/tests/locales.spec.ts
T
figmar 0e91e99f49 feat(apes-sidebar): Phase 0 工程骨架 — host/client 分裂 + i18n + 不变量面
从 0 重写的 v2.0.0 DSH 侧边栏插件第一阶段:
- 工程骨架:tsdown 双构建(host ESM + client CJS closure)、vitest、strict TS
- host 半:namespace 插件形状(name/inject/apply,无 default),minimal apply
- client 半:closure factory 注册 5 个 apes:* tab 占位 + zh/en i18n
- 不变量面:assert/semver/peer guard(dsh-better-sidebar >= 0.18.0)
- 依赖策略:dsh-better-sidebar=peerDependency(不打包),cordis=devDependency(仅类型)
- 门禁:pnpm typecheck + build + test 全绿(14 单测通过)
- AGENTS.md 工程约定 + .gitignore(排除 lib/node_modules/maxkb-integration)

模板:dsh-better-sidebar。作用域锁定信息层(tasks/agents/apps/messages/profile),
不含 file/terminal/browser(归 better-sidebar)、不含 team/room(归 background-agents)。
2026-09-04 17:14:22 +08:00

129 lines
4.5 KiB
TypeScript

/**
* Locale contract for the client half. The apes:* tabs are bilingual
* (zh default, en fallback) — the same shape tutti-os ships. This spec pins
* the i18n surface against the REAL locale module API:
* - t() resolves the active locale, falling back en → raw key
* - registerLocales() syncs activeLocale from a mock ApesLocaleService and
* keeps it in sync as the runtime emits new snapshots
* - subscribeLocale() re-render hook + clean detach
* The React hook (useApesLocale) needs a renderer; it is a thin
* useSyncExternalStore wrapper over the same getSnapshot/subscribe pair and
* is exercised indirectly here.
*/
import { describe, expect, it } from 'vitest'
import {
t,
getActiveLocale,
subscribeLocale,
registerLocales,
} from '../src/client/locales.ts'
import type { ApesLocaleService } from '../src/context-types.ts'
/** Build a mock ApesLocaleService whose active locale we can flip. */
function makeLocaleService(initialActive = 'zh'): {
service: ApesLocaleService
setActive: (loc: string) => void
registered: Array<{ ns: string; locale: string }>
} {
let active = initialActive
const subs = new Set<() => void>()
const registered: Array<{ ns: string; locale: string }> = []
const service: ApesLocaleService = {
getSnapshot: () => ({ active }),
subscribe: (fn) => {
subs.add(fn)
return () => subs.delete(fn)
},
register: (ns, locale) => {
registered.push({ ns, locale })
return () => {}
},
}
return {
service,
setActive: (loc) => {
active = loc
for (const fn of [...subs]) fn()
},
registered,
}
}
describe('locales module (real API)', () => {
it('resolves keys in the default zh locale', () => {
// Before any registerLocales call, activeLocale is the module default.
const active = getActiveLocale()
expect(['zh', 'en']).toContain(active)
// Every registered key resolves to a non-empty string.
expect(t('tab.tasks.title').length).toBeGreaterThan(0)
expect(t('tab.agents.title').length).toBeGreaterThan(0)
// A key in neither locale falls back to the raw key (flag the gap).
expect(t('does.not.exist')).toBe('does.not.exist')
})
it('registerLocales registers zh+en under the apesSidebar namespace', () => {
const { service, registered } = makeLocaleService('zh')
const dispose = registerLocales(service)
expect(registered.map((r) => r.locale).sort()).toEqual(['en', 'zh'])
expect(registered.every((r) => r.ns === 'apesSidebar')).toBe(true)
dispose() // clean disposer, no throw on double-run
expect(() => dispose()).not.toThrow()
})
it('registerLocales seeds activeLocale from the runtime snapshot', () => {
const { service } = makeLocaleService('en')
const dispose = registerLocales(service)
expect(getActiveLocale()).toBe('en')
expect(t('tab.tasks.title')).toBe('Tasks')
dispose()
})
it('keeps activeLocale in sync as the runtime emits a new active locale', () => {
const { service, setActive } = makeLocaleService('zh')
const dispose = registerLocales(service)
expect(getActiveLocale()).toBe('zh')
const before = t('tab.tasks.title')
setActive('en')
expect(getActiveLocale()).toBe('en')
expect(t('tab.tasks.title')).toBe('Tasks')
// The value actually changed.
expect(before).not.toBe(t('tab.tasks.title'))
dispose()
})
it('subscribeLocale notifies on locale change and detaches cleanly', () => {
const { service, setActive } = makeLocaleService('zh')
const dispose = registerLocales(service)
const seen: string[] = []
const unsub = subscribeLocale(() => seen.push(getActiveLocale()))
setActive('en')
expect(seen).toEqual(['en'])
unsub()
setActive('zh')
// After detach, no further observation.
expect(seen).toEqual(['en'])
dispose()
})
it('is a no-op (clean disposer) when the locale runtime is absent', () => {
const dispose = registerLocales(undefined)
expect(typeof dispose).toBe('function')
expect(() => dispose()).not.toThrow()
})
it('tolerates a runtime whose subscribe throws (early boot guard)', () => {
const service: ApesLocaleService = {
getSnapshot: () => ({ active: 'en' }),
subscribe: () => {
throw new Error('runtime not ready')
},
register: () => () => {},
}
// The try/catch in registerLocales swallows the subscribe failure and
// still returns a working disposer.
const dispose = registerLocales(service)
expect(typeof dispose).toBe('function')
expect(() => dispose()).not.toThrow()
})
})