从 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)。
129 lines
4.5 KiB
TypeScript
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()
|
|
})
|
|
})
|