Files
Literature-Management/tests/unit/i18n/locale-parity.test.ts
2026-10-21 14:42:59 +08:00

62 lines
2.3 KiB
TypeScript

// tests/unit/i18n/locale-parity.test.ts
//
// 守卫:zh-CN.json / en-US.json 顶层键结构必须严格一致。
// 行为:把两文件扁平化到 dot 键(递归 object),两侧集合不同就 fail。
// 目的:防止漏译 / 拼错键名 —— 双 locale 同时被引,i18next 在缺的 key 上抛
// 警告并把 zh 当 fallback,renderer 看似正常但英文版回退中文,debug 极难。
//
// 已知 i18n stub:en-US 部分技术字面量(loadingDots '…')和 zh 相同是
// 预期 —— 不因此 fail。
//
// 历史:audit-r156 (2026-09-?) 起一直被守;r165-hotfix 收紧"两边 dict
// 完整性"语义后从 1 例失败拉到 1 例通过(去掉 en-US 9 个多余键)。
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { describe, expect, it } from 'vitest';
interface LocaleJson {
[key: string]: unknown;
}
const LOCALES_DIR = resolve(__dirname, '../../../src/renderer/src/i18n/locales');
function flatKeys(value: LocaleJson, prefix = ''): string[] {
const out: string[] = [];
for (const [k, v] of Object.entries(value)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
out.push(...flatKeys(v as LocaleJson, key));
} else {
out.push(key);
}
}
return out;
}
describe('i18n locale parity', () => {
const zhPath = resolve(LOCALES_DIR, 'zh-CN.json');
const enPath = resolve(LOCALES_DIR, 'en-US.json');
const zh = JSON.parse(readFileSync(zhPath, 'utf8')) as LocaleJson;
const en = JSON.parse(readFileSync(enPath, 'utf8')) as LocaleJson;
const zhKeys = new Set(flatKeys(zh));
const enKeys = new Set(flatKeys(en));
it('zh-CN.json 和 en-US.json 顶层都读得到且是合法 JSON', () => {
expect(typeof zh).toBe('object');
expect(typeof en).toBe('object');
});
it('两 locale 的扁平键集合严格一致', () => {
const onlyZh = [...zhKeys].filter((k) => !enKeys.has(k)).sort();
const onlyEn = [...enKeys].filter((k) => !zhKeys.has(k)).sort();
expect({ onlyZh, onlyEn }).toEqual({ onlyZh: [], onlyEn: [] });
});
it('两 locale 都有 _meta.code 字段(自我描述)', () => {
const zhMeta = (zh as { _meta?: { code?: string } })._meta;
const enMeta = (en as { _meta?: { code?: string } })._meta;
expect(zhMeta?.code).toBe('zh-CN');
expect(enMeta?.code).toBe('en-US');
});
});