check-coverage.js 2.97 KB
/**
 * 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()
}