import { describe, expect, it } from 'vitest' import { I18N_NAMESPACES } from '../../src/i18n' import { APP_LANGUAGES, type AppLanguage } from '../../src/i18n/detectLocale' function flattenKeys(value: unknown, prefix = ''): string[] { if (value === null || typeof value !== 'object' || Array.isArray(value)) { return prefix ? [prefix] : [] } const entries = Object.entries(value as Record) if (entries.length === 0) return prefix ? [prefix] : [] return entries.flatMap(([key, nested]) => { const next = prefix ? `${prefix}.${key}` : key if (nested !== null && typeof nested === 'object' && !Array.isArray(nested)) { const childKeys = flattenKeys(nested, next) return childKeys.length > 0 ? childKeys : [next] } return [next] }) } async function loadLocale(namespace: string, language: AppLanguage) { const mod = await import(`../../src/i18n/locales/${language}/${namespace}.json`) return mod.default as Record } describe('i18n locale key parity', () => { for (const namespace of I18N_NAMESPACES) { for (const language of APP_LANGUAGES) { it(`${language} keys match en for ${namespace}`, async () => { const [locale, en] = await Promise.all([loadLocale(namespace, language), loadLocale(namespace, 'en')]) const localeKeys = flattenKeys(locale).sort() const enKeys = flattenKeys(en).sort() expect(localeKeys).toEqual(enKeys) for (const key of localeKeys) { const value = key.split('.').reduce((node, part) => { if (node && typeof node === 'object' && part in node) { return (node as Record)[part] } return undefined }, locale) if (typeof value === 'string') { expect(value.trim().length, `${language}:${namespace}.${key}`).toBeGreaterThan(0) } } }) } } }) function flattenValues(value: unknown, prefix = ''): Record { if (value === null || typeof value !== 'object' || Array.isArray(value)) { return prefix && typeof value === 'string' ? { [prefix]: value } : {} } const out: Record = {} for (const [key, nested] of Object.entries(value as Record)) { const next = prefix ? `${prefix}.${key}` : key if (nested !== null && typeof nested === 'object' && !Array.isArray(nested)) { Object.assign(out, flattenValues(nested, next)) } else if (typeof nested === 'string') { out[next] = nested } } return out } /** Product names, URLs, and format tokens that stay the same in every locale. */ const SETTINGS_ALLOW_SAME_AS_EN = new Set([ 'language.zhCN', 'language.zhTW', 'language.en', 'language.ja', 'language.ko', 'language.vi', 'language.ru', 'nav.ssh', 'about.appVersion', 'about.githubDesc', 'about.license', 'about.licenseLinkDesc', 'about.wechat', ]) describe('settings copy is translated', () => { it.each(APP_LANGUAGES.filter((language) => language !== 'en'))( '%s does not leave English settings/about strings', async (language) => { const [locale, en] = await Promise.all([loadLocale('settings', language), loadLocale('settings', 'en')]) const localeValues = flattenValues(locale) const enValues = flattenValues(en) const leftover = Object.keys(enValues).filter((key) => { if (SETTINGS_ALLOW_SAME_AS_EN.has(key)) return false return localeValues[key] === enValues[key] }) expect(leftover, leftover.join(', ')).toEqual([]) } ) }) /** Product name and shared layout templates in Agent CLI. */ const AI_ALLOW_SAME_AS_EN = new Set(['panel.title', 'panel.modelConfigured', 'panel.sshReady', 'step.confirmMessage']) describe('ai copy is translated', () => { it.each(APP_LANGUAGES.filter((language) => language !== 'en'))( '%s does not leave English Agent CLI strings', async (language) => { const [locale, en] = await Promise.all([loadLocale('ai', language), loadLocale('ai', 'en')]) const localeValues = flattenValues(locale) const enValues = flattenValues(en) const leftover = Object.keys(enValues).filter((key) => { if (AI_ALLOW_SAME_AS_EN.has(key)) return false return localeValues[key] === enValues[key] }) expect(leftover, leftover.join(', ')).toEqual([]) } ) })