Files
toor 01d9e301fc
Build / build (push) Canceled after 0s
Playwright / browser (push) Canceled after 0s
initialize Electron desktop app and remove unused files
2026-10-25 01:09:01 +08:00

111 lines
5.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
// 直接 import 未 merge 的裸 locale 对象。
//
// 刻意**不**用 `@/i18n/messages` 的 messages:那里 zh 已经和 en 做过
// mergeMessagesWithFallback(见 packages/client/src/i18n/messages.ts),
// zh 缺的 key 会自动从 en 补上,于是「zh 缺 key」这件事在运行时永远看不出来。
// 本文件要卡的是**源文件层面**的对等,所以必须用裸对象。
import en from '@/i18n/locales/en'
import zh from '@/i18n/locales/zh'
type Messages = Record<string, unknown>
/**
* 关于 tests/client/i18n-coverage.test.ts 里的
* `ALLOWED_MISSING_KEYS = { 'changelog.new_0_5_4_7', 'chat.sessionNotFound' }`:
*
* 实测结论(Phase 008 §3.1 逐 key 核对 en.ts / zh.ts 原文):
* - `chat.sessionNotFound` —— **en 与 zh 都有**(en.ts:665 / zh.ts:665,
* 分别在各自 `chat` 命名空间下)。它出现在 i18n-coverage 的豁免表里属于
* 历史遗留/过宽豁免,对本测试**不适用**,因此这里不做任何豁免。
* - `changelog.new_0_5_4_7` —— **en 与 zh 都没有**。它是 changelog 的版本条目名
* (packages/client/src/data/changelog.ts 的数据),从来不是 locale key;
* i18n-coverage 把 changelog 的 change 文案也当成 key 去查 en,才会需要豁免。
* 两个 locale 同时缺失,不构成「单边缺 key」,因此这里同样不需要豁免。
*
* 换句话说:这两个 key 在本测试里**都不是**单边差异,豁免它们只会把真实缺陷
* 藏起来。若将来真的出现单边差异,正确做法是补齐缺失一侧的 locale 文件,
* 而不是往这里加豁免。
*/
type LeafKind = 'string' | 'object' | 'other'
/** 递归 flatten:返回 完整 key 路径 -> 叶子类型(命名空间本身也记为 'object') */
function flatten(source: Messages, prefix = '', out = new Map<string, LeafKind>()) {
for (const [key, value] of Object.entries(source)) {
const fullKey = prefix ? `${prefix}.${key}` : key
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
out.set(fullKey, 'object')
flatten(value as Messages, fullKey, out)
} else {
out.set(fullKey, typeof value === 'string' ? 'string' : 'other')
}
}
return out
}
const enKeys = flatten(en as Messages)
const zhKeys = flatten(zh as Messages)
const missingInZh = [...enKeys.keys()].filter(key => !zhKeys.has(key)).sort()
const missingInEn = [...zhKeys.keys()].filter(key => !enKeys.has(key)).sort()
const typeMismatches = [...enKeys.keys()]
.filter(key => zhKeys.has(key) && zhKeys.get(key) !== enKeys.get(key))
.sort()
.map(key => `${key}: en=${enKeys.get(key)} / zh=${zhKeys.get(key)}`)
describe('i18n locale parity between zh and en', () => {
it('flattens a non-trivial key space from both locales', () => {
// 防止 import 路径写错、或 flatten 逻辑改坏导致两边都变成空集合、永远绿灯
expect(enKeys.size).toBeGreaterThan(1000)
expect(zhKeys.size).toBeGreaterThan(1000)
// 抽查几个嵌套命名空间,确认递归真的展开了层级
for (const key of ['chat.sessionNotFound', 'chat.renameSession']) {
expect(enKeys.get(key), `en 缺少嵌套 key ${key}`).toBe('string')
expect(zhKeys.get(key), `zh 缺少嵌套 key ${key}`).toBe('string')
}
})
it('defines every English key in Chinese', () => {
expect(
missingInZh,
missingInZh.length === 0 ? '' : [
`${missingInZh.length} 个 key 在 en.ts 里有、zh.ts 里没有:`,
...missingInZh.map(key => ` ${key}`),
'',
'修复方式:在 packages/client/src/i18n/locales/zh.ts 对应的命名空间下补上中文。',
'注意不要改本文件来豁免它们——单边缺失会让线上退化成英文文案。',
].join('\n'),
).toEqual([])
})
it('defines every Chinese key in English', () => {
expect(
missingInEn,
missingInEn.length === 0 ? '' : [
`${missingInEn.length} 个 key 在 zh.ts 里有、en.ts 里没有:`,
...missingInEn.map(key => ` ${key}`),
'',
'修复方式:在 packages/client/src/i18n/locales/en.ts 对应的命名空间下补上英文。',
'英文是 fallback 语言,缺失会让中文用户在切换语言时看到空白。',
].join('\n'),
).toEqual([])
})
it('keeps the leaf type of every shared key consistent', () => {
// 例如 en 里某个 key 是命名空间对象、zh 里却是字符串:运行时会在插值时炸掉,
// 或者某一侧多出一层嵌套导致 t('a.b') 查不到值。
expect(
typeMismatches,
typeMismatches.length === 0 ? '' : [
'以下 key 在 en 与 zh 的叶子类型不一致(string / object / other):',
...typeMismatches.map(entry => ` ${entry}`),
'',
'修复方式:把两侧的层级结构改成一致(要么都是字符串,要么都是同层命名空间)。',
].join('\n'),
).toEqual([])
})
})