check-coverage.js
2.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* check-coverage.js —— 英语第一公民门禁。
* 以 `en` 为基准列出各语言缺失 key + 私有(多余)key。
* CI 用:`en` 自身零缺失为硬约束(en 是基准,无对照故恒为空);
* 其余语言「缺失 key」一期允许非空(仅告警);「私有 key」(en 没有的 key)任何语言出现都判失败——防 key 树漂移。
*
* 退出码:发现任何语言有私有 key → 退出码 1(key 树漂移,硬失败)。否则 0。
* 缺失 key 仅打印告警,不改退出码(一期允许部分语言不全)。
*/
import { messages } from '../src/messages/index.js'
import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../src/locales.js'
/**
* 把嵌套 message 对象拍平成点路径 key 集合(叶子为字符串)。
* @param {Record<string, unknown>} obj
* @param {string} [prefix]
* @returns {Set<string>}
*/
export function flattenKeys(obj, prefix = '') {
/** @type {Set<string>} */
const out = new Set()
for (const [k, v] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${k}` : k
if (v && typeof v === 'object' && !Array.isArray(v)) {
for (const nested of flattenKeys(/** @type {Record<string, unknown>} */ (v), path)) {
out.add(nested)
}
} else {
out.add(path)
}
}
return out
}
/**
* 以 en 为基准计算各语言的缺失 / 私有 key。
* @returns {{ baseKeyCount:number, perLocale: Record<string, { missing:string[], extra:string[] }> }}
*/
export function computeCoverage() {
const baseKeys = flattenKeys(messages[DEFAULT_LOCALE])
/** @type {Record<string, { missing:string[], extra:string[] }>} */
const perLocale = {}
for (const code of SUPPORTED_LOCALES) {
const keys = flattenKeys(messages[code])
const missing = [...baseKeys].filter((k) => !keys.has(k))
const extra = [...keys].filter((k) => !baseKeys.has(k))
perLocale[code] = { missing, extra }
}
return { baseKeyCount: baseKeys.size, perLocale }
}
function main() {
const { baseKeyCount, perLocale } = computeCoverage()
let hasDrift = false
console.log(`[i18n coverage] 基准 en 共 ${baseKeyCount} 个 key`)
for (const code of SUPPORTED_LOCALES) {
const { missing, extra } = perLocale[code]
if (extra.length > 0) {
hasDrift = true
console.error(` ✗ ${code} 含 ${extra.length} 个私有 key(en 无): ${extra.join(', ')}`)
}
if (missing.length > 0 && code !== DEFAULT_LOCALE) {
console.warn(` ◐ ${code} 缺失 ${missing.length} 个 key: ${missing.join(', ')}`)
}
if (missing.length === 0 && extra.length === 0) {
console.log(` ✓ ${code} 完整且同构`)
}
}
if (perLocale[DEFAULT_LOCALE].missing.length > 0) {
hasDrift = true
console.error(` ✗ en 基准缺失 key(不应发生): ${perLocale[DEFAULT_LOCALE].missing.join(', ')}`)
}
process.exit(hasDrift ? 1 : 0)
}
// 直接执行时跑门禁;被 import 时只暴露纯函数。
if (import.meta.url === `file://${process.argv[1]}`) {
main()
}