Files
apes-sidebar/tests/locales.spec.ts
T
HuanLinOTO 9410e5bb2a fix(i18n): add worktree translation to all 19 third-language dicts (#285)
PR #285 added the zh/en 'worktree' key but the 19 third-language dicts
(de/fr/pt/ko/ar/hi/id/tr/vi/th/ru/it/nl/sv/pl/zhHK/zhTW/zhMO) were missing
it — only ja was patched in the prior commit. The dicts are typed
Record<keyof typeof zh, string> via an unchecked 'as' cast, so tsc did
not catch the gap.

This commit:
- adds the worktree translation to all 19 dicts
- strengthens tests/locales.spec.ts to assert key-set equality for EVERY
  shipped dictionary (not just zh/en/ja), preventing future regressions
2026-08-23 20:58:33 +08:00

261 lines
8.7 KiB
TypeScript

/**
* Locale-following spec: the sidebar copy follows the DSH locale service
* (`ctx.locale`, provided by @deepseek-ai/dsh-client-locale) when attached
* through `attachLocale`, and falls back to the browser language otherwise.
* Covers attach/detach, live switching, dictionary parity, and placeholder
* interpolation.
*/
import { afterEach, describe, expect, it } from 'vitest'
import { LOCALE_NS, attachBetterLocale, attachLocale, en, isZh, ja, relativeTime, t, zh, de, fr, pt, ko, ar, hi, id as idDict, tr, vi, th, ru, it as itDict, nl, sv, pl, zhHK, zhTW, zhMO } from '../src/client/locales.ts'
/** Minimal structural fake of the DSH LocaleService face the sidebar uses. */
class FakeLocale {
active: string = 'en'
getSnapshot(): { active: string } {
return { active: this.active }
}
subscribe(_fn: () => void): () => void {
return () => {}
}
register(_ns: string, _locale: string, _dict: Record<string, string>): () => void {
return () => {}
}
switchTo(id: string): void {
this.active = id
}
}
/** Point the browser-language fallback at a specific language (undefined = none). */
function stubNavigatorLanguage(lang: string | undefined): void {
Object.defineProperty(globalThis, 'navigator', {
value: lang === undefined ? undefined : { language: lang },
configurable: true,
})
}
afterEach(() => {
attachLocale(undefined)
attachBetterLocale(undefined)
stubNavigatorLanguage(undefined)
})
describe('locales (DSH i18n following)', () => {
it('falls back to the browser language without an attached service', () => {
stubNavigatorLanguage('en-US')
expect(t('explorer')).toBe('Explorer')
expect(isZh()).toBe(false)
stubNavigatorLanguage('zh-CN')
expect(t('explorer')).toBe('资源管理器')
expect(isZh()).toBe(true)
})
it('defaults to English when no locale service and no browser language are available', () => {
expect(t('explorer')).toBe('Explorer')
expect(isZh()).toBe(false)
})
it('follows the attached locale service instead of the browser language', () => {
stubNavigatorLanguage('en-US')
const locale = new FakeLocale()
attachLocale(locale)
locale.switchTo('zh')
expect(t('git')).toBe('源代码管理')
expect(isZh()).toBe(true)
// Live switch: the service's active locale wins even though the
// browser still asks for en-US.
locale.switchTo('en')
expect(t('git')).toBe('Source Control')
expect(isZh()).toBe(false)
})
it('detaches back to the browser-language fallback', () => {
const locale = new FakeLocale()
locale.switchTo('zh')
attachLocale(locale)
expect(t('terminal')).toBe('终端')
stubNavigatorLanguage('en-US')
attachLocale(undefined)
expect(t('terminal')).toBe('Terminal')
})
it('interpolates {name} placeholders in the active locale', () => {
const locale = new FakeLocale()
attachLocale(locale)
locale.switchTo('zh')
expect(t('timeMinutesAgo', { n: 5 })).toBe('5 分钟前')
locale.switchTo('en')
expect(t('timeMinutesAgo', { n: 5 })).toBe('5 min ago')
})
it('drives the relative-time chain through the active locale', () => {
const locale = new FakeLocale()
attachLocale(locale)
locale.switchTo('zh')
expect(relativeTime(new Date().toISOString())).toBe('刚刚')
locale.switchTo('en')
expect(relativeTime(new Date().toISOString())).toBe('just now')
})
it('registers a namespace distinct from DSH ui-sidebar\'s own \'sidebar\'', () => {
expect(LOCALE_NS).toBe('betterSidebar')
})
it('keeps the zh and en dictionaries key-set-equal', () => {
expect(Object.keys(en).sort()).toEqual(Object.keys(zh).sort())
})
it('keeps the ja dictionary key-set-equal to zh', () => {
expect(Object.keys(ja).sort()).toEqual(Object.keys(zh).sort())
})
it('keeps every shipped third-language dictionary key-set-equal to zh', () => {
// The third-language dicts are typed `Record<keyof typeof zh, string>` via
// an `as` cast (unchecked by tsc). This test catches missing/extra keys
// introduced when a zh key is added without updating every language.
const thirdLangs = {
de, fr, pt, ko, ar, hi, idDict, tr, vi, th, ru, itDict, nl, sv, pl, zhHK, zhTW, zhMO,
}
for (const [lang, dict] of Object.entries(thirdLangs)) {
expect(Object.keys(dict as Record<string, string>).sort(), lang).toEqual(Object.keys(zh).sort())
}
})
})
/** Minimal fake of the better-locale override store (the subset t()/isZh() read). */
class FakeBetterLocale {
active: string | undefined = undefined
private readonly dict: Record<string, Record<string, Record<string, string>>>
constructor(dict: Record<string, Record<string, Record<string, string>>>) {
this.dict = dict
}
getOverride(dshActive: string, ns: string, key: string): string | undefined {
if (this.active === undefined) return undefined
if (dshActive !== 'en') return undefined
return this.dict[ns]?.[this.active]?.[key]
}
isOverrideActive(dshActive: string): boolean {
return this.active !== undefined && dshActive === 'en'
}
subscribe(_listener: () => void): () => void {
return () => {}
}
}
describe('locales (better-locale override)', () => {
it('returns the override text when an override is active and the key exists', () => {
const locale = new FakeLocale()
locale.switchTo('en')
attachLocale(locale)
const betterLocale = new FakeBetterLocale({
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
})
betterLocale.active = 'ja'
attachBetterLocale(betterLocale)
expect(t('explorer')).toBe('エクスプローラー')
expect(isZh()).toBe(false)
})
it('falls back to the zh/en chain when override is active but the key is missing', () => {
const locale = new FakeLocale()
locale.switchTo('en')
attachLocale(locale)
const betterLocale = new FakeBetterLocale({
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
})
betterLocale.active = 'ja'
attachBetterLocale(betterLocale)
// 'git' has no ja entry in this fake; should fall back to the en text.
expect(t('git')).toBe('Source Control')
})
it('falls back to the zh/en chain when no override is active', () => {
const locale = new FakeLocale()
locale.switchTo('zh')
attachLocale(locale)
const betterLocale = new FakeBetterLocale({
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
})
// active is undefined — no override.
attachBetterLocale(betterLocale)
expect(t('explorer')).toBe('资源管理器')
expect(isZh()).toBe(true)
})
it('override is inert while DSH is on zh (override borrows the en slot)', () => {
const locale = new FakeLocale()
locale.switchTo('zh')
attachLocale(locale)
const betterLocale = new FakeBetterLocale({
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
})
betterLocale.active = 'ja'
attachBetterLocale(betterLocale)
// Override is set to 'ja' but DSH is on zh — override is inert,
// native zh text wins. The user must switch DSH to en to see ja.
expect(t('explorer')).toBe('资源管理器')
expect(isZh()).toBe(true)
})
it('returns to the zh/en chain after detaching the override store', () => {
const locale = new FakeLocale()
locale.switchTo('en')
attachLocale(locale)
const betterLocale = new FakeBetterLocale({
[LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } },
})
betterLocale.active = 'ja'
attachBetterLocale(betterLocale)
expect(t('explorer')).toBe('エクスプローラー')
attachBetterLocale(undefined)
expect(t('explorer')).toBe('Explorer')
expect(isZh()).toBe(false)
})
it('interpolates {name} placeholders in the override text', () => {
const locale = new FakeLocale()
locale.switchTo('en')
attachLocale(locale)
const betterLocale = new FakeBetterLocale({
[LOCALE_NS]: { ja: { timeMinutesAgo: '{n} 分前' } },
})
betterLocale.active = 'ja'
attachBetterLocale(betterLocale)
expect(t('timeMinutesAgo', { n: 5 })).toBe('5 分前')
})
it('ships a ja dictionary whose every key resolves to a non-empty string', () => {
// Smoke-check the shipped ja dict: every key in zh must have a non-empty
// ja entry. This catches copy-paste mistakes where a key was added to zh
// but its ja translation was left blank or missing.
const locale = new FakeLocale()
locale.switchTo('en')
attachLocale(locale)
const betterLocale = new FakeBetterLocale({ [LOCALE_NS]: { ja } })
betterLocale.active = 'ja'
attachBetterLocale(betterLocale)
for (const key of Object.keys(zh) as (keyof typeof zh)[]) {
const text = t(key)
expect(text, `ja translation for "${key}"`).toBeTruthy()
expect(text, `ja translation for "${key}"`).not.toBe(key)
}
})
})