/** * 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} obj * @param {string} [prefix] * @returns {Set} */ export function flattenKeys(obj, prefix = '') { /** @type {Set} */ 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} */ (v), path)) { out.add(nested) } } else { out.add(path) } } return out } /** * 以 en 为基准计算各语言的缺失 / 私有 key。 * @returns {{ baseKeyCount:number, perLocale: Record }} */ export function computeCoverage() { const baseKeys = flattenKeys(messages[DEFAULT_LOCALE]) /** @type {Record} */ 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() }