123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
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<string, unknown>)
|
|
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<string, unknown>
|
|
}
|
|
|
|
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<unknown>((node, part) => {
|
|
if (node && typeof node === 'object' && part in node) {
|
|
return (node as Record<string, unknown>)[part]
|
|
}
|
|
return undefined
|
|
}, locale)
|
|
if (typeof value === 'string') {
|
|
expect(value.trim().length, `${language}:${namespace}.${key}`).toBeGreaterThan(0)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
function flattenValues(value: unknown, prefix = ''): Record<string, string> {
|
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
return prefix && typeof value === 'string' ? { [prefix]: value } : {}
|
|
}
|
|
|
|
const out: Record<string, string> = {}
|
|
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
|
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([])
|
|
}
|
|
)
|
|
})
|