Files
lucky_shop/scripts/i18n-check.js
ZF sun 5d3663786c feat: add multi-language page configs and fix i18n issues
1. 新增多套多语言页面翻译文件,覆盖文件列表、案例展示、商户相关、印章相关、分销、小程序发货等页面
2. 修复语言包拼写错误:loginTpis → loginTips
3. 重构语言包合并逻辑,使用深度合并替代浅合并避免子key丢失
4. 优化导航栏标题设置,统一使用$langConfig.title替代硬编码标题
5. 修复底部导航栏语言切换不响应的问题,改为响应式获取当前语言
6. 完善英文通用语言包,补充订单、购物车、会员相关翻译
7. 修复商品详情页默认标题为空的问题,设置为"商品详情"
2026-06-25 14:52:39 +08:00

281 lines
8.4 KiB
JavaScript

#!/usr/bin/env node
/**
* i18n 国际化检查脚本
*
* 功能:
* 1. 检查 zh-cn / en-us 语言包文件结构是否一致
* 2. 检查同文件下中英文 key 是否一致
* 3. 统计 key 总数和差异数
*
* 用法:
* node scripts/i18n-check.js
*
* 退出码:
* 0 - 检查通过(无差异 或 只有警告级差异)
* 1 - 存在错误级差异
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const LANG_DIR = path.join(ROOT, 'lang');
const LOCALES = ['zh-cn', 'en-us'];
// 递归获取目录下所有 .js 文件(相对路径)
function getAllLangFiles(dir) {
const result = [];
function walk(current, rel) {
const entries = fs.readdirSync(current, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(current, entry.name);
const relPath = rel ? path.join(rel, entry.name) : entry.name;
if (entry.isDirectory()) {
walk(fullPath, relPath);
} else if (entry.name.endsWith('.js')) {
result.push(relPath.replace(/\\/g, '/'));
}
}
}
walk(dir, '');
return result.sort();
}
// 解析语言包 js 文件,提取所有 key(支持嵌套,用 . 连接)
// 注意: 用正则粗略提取,不做完整 AST 解析,对本项目语言包格式足够
function extractKeys(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const keys = new Set();
// 简单的对象 key 提取:匹配 key: 'value' 或 key: { 形式
// 用栈跟踪嵌套层级,记录路径
const stack = [];
const lines = content.split('\n');
let braceDepth = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 跳过注释
const trimmed = line.trim();
if (trimmed.startsWith('//')) continue;
if (trimmed.startsWith('/*')) continue;
if (trimmed.startsWith('*')) continue;
// 匹配 key: 形式
// 匹配: identifier: 或 'key': 或 "key":
const keyMatch = line.match(/^\s*['"]?(\w[\w]*)['"]?\s*:/);
if (keyMatch) {
const key = keyMatch[1];
// 跳过 export const lang = { 这种
if (stack.length === 0 && key === 'lang') {
// 往下找第一个 {
if (line.includes('{')) braceDepth++;
if (braceDepth > 0) stack.push('');
continue;
}
// 计算当前路径
const pathParts = stack.filter(p => p);
const fullKey = [...pathParts, key].join('.');
// 判断这是字符串值还是对象值
const afterColon = line.substring(line.indexOf(':') + 1);
const isObject = afterColon.includes('{');
const isString = afterColon.match(/['"`]/);
if (isObject && !isString) {
// 对象 key,入栈
stack.push(key);
keys.add(fullKey + '.__object__');
} else {
// 叶子 key
keys.add(fullKey);
}
}
// 处理闭合 },出栈
const closeBraces = (line.match(/\}/g) || []).length;
const openBraces = (line.match(/\{/g) || []).length;
const netClose = closeBraces - openBraces;
if (line.includes('{') && keyMatch && !line.includes('}')) {
// 这行是 key: {,已经在上面处理了入栈
}
// 简单处理:遇到 } 且栈非空就出栈
if (netClose > 0 && stack.length > 0) {
for (let j = 0; j < netClose && stack.length > 0; j++) {
stack.pop();
}
}
}
// 去掉 __object__ 标记
const result = new Set();
for (const k of keys) {
if (!k.endsWith('.__object__')) {
result.add(k);
}
}
return result;
}
// 更可靠的 key 提取:用 Node.js require + 递归遍历对象
function extractKeysByRequire(filePath) {
try {
// 清除 require 缓存
delete require.cache[require.resolve(filePath)];
const mod = require(filePath);
const langObj = mod.lang || mod.default?.lang || mod.default;
if (!langObj || typeof langObj !== 'object') return new Set();
const keys = new Set();
function walk(obj, prefix) {
for (const key of Object.keys(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
const val = obj[key];
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
walk(val, fullKey);
} else if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
keys.add(fullKey);
}
}
}
walk(langObj, '');
return keys;
} catch (e) {
console.error(` 解析失败: ${filePath} - ${e.message}`);
return new Set();
}
}
function main() {
console.log('========================================');
console.log(' i18n 国际化检查');
console.log('========================================\n');
const localeDirs = {};
const localeFiles = {};
for (const locale of LOCALES) {
const dir = path.join(LANG_DIR, locale);
if (!fs.existsSync(dir)) {
console.error(`错误: 找不到语言目录 ${dir}`);
process.exit(1);
}
localeDirs[locale] = dir;
localeFiles[locale] = new Set(getAllLangFiles(dir));
}
// ---- 检查 1: 文件结构一致性 ----
console.log('【1/2】文件结构检查\n');
const zhFiles = localeFiles['zh-cn'];
const enFiles = localeFiles['en-us'];
const onlyZh = [...zhFiles].filter(f => !enFiles.has(f));
const onlyEn = [...enFiles].filter(f => !zhFiles.has(f));
const commonFiles = [...zhFiles].filter(f => enFiles.has(f));
console.log(` 中文语言包文件数: ${zhFiles.size}`);
console.log(` 英文语言包文件数: ${enFiles.size}`);
console.log(` 共同文件数: ${commonFiles.length}`);
if (onlyZh.length > 0) {
console.log(`\n ⚠️ 只有中文没有英文的文件 (${onlyZh.length}):`);
for (const f of onlyZh) {
console.log(` - ${f}`);
}
}
if (onlyEn.length > 0) {
console.log(`\n ⚠️ 只有英文没有中文的文件 (${onlyEn.length}):`);
for (const f of onlyEn) {
console.log(` - ${f}`);
}
}
if (onlyZh.length === 0 && onlyEn.length === 0) {
console.log(' ✅ 文件结构完全一致');
}
// ---- 检查 2: key 一致性 ----
console.log('\n【2/2】Key 一致性检查\n');
let totalZhKeys = 0;
let totalEnKeys = 0;
let filesWithDiff = 0;
const diffDetails = [];
for (const file of commonFiles) {
const zhPath = path.join(localeDirs['zh-cn'], file);
const enPath = path.join(localeDirs['en-us'], file);
const zhKeys = extractKeysByRequire(zhPath);
const enKeys = extractKeysByRequire(enPath);
totalZhKeys += zhKeys.size;
totalEnKeys += enKeys.size;
const missingInEn = [...zhKeys].filter(k => !enKeys.has(k));
const missingInZh = [...enKeys].filter(k => !zhKeys.has(k));
if (missingInEn.length > 0 || missingInZh.length > 0) {
filesWithDiff++;
diffDetails.push({
file,
missingInEn,
missingInZh,
zhCount: zhKeys.size,
enCount: enKeys.size,
});
}
}
console.log(` 中文 key 总数: ${totalZhKeys}`);
console.log(` 英文 key 总数: ${totalEnKeys}`);
console.log(` 有差异的文件数: ${filesWithDiff} / ${commonFiles.length}`);
if (filesWithDiff > 0) {
console.log(`\n ⚠️ Key 差异详情:`);
for (const diff of diffDetails) {
console.log(`\n 📄 ${diff.file}`);
console.log(` 中文: ${diff.zhCount} 个, 英文: ${diff.enCount} 个`);
if (diff.missingInEn.length > 0) {
console.log(` 英文缺失 (${diff.missingInEn.length}):`);
for (const k of diff.missingInEn.slice(0, 10)) {
console.log(` - ${k}`);
}
if (diff.missingInEn.length > 10) {
console.log(` ... 还有 ${diff.missingInEn.length - 10} 个`);
}
}
if (diff.missingInZh.length > 0) {
console.log(` 中文缺失 (${diff.missingInZh.length}):`);
for (const k of diff.missingInZh.slice(0, 10)) {
console.log(` - ${k}`);
}
if (diff.missingInZh.length > 10) {
console.log(` ... 还有 ${diff.missingInZh.length - 10} 个`);
}
}
}
} else {
console.log(' ✅ 所有共同文件的 key 完全一致');
}
// ---- 总结 ----
console.log('\n========================================');
console.log(' 检查完成');
console.log('========================================');
const hasError = filesWithDiff > 0 || onlyZh.length > 0;
if (hasError) {
console.log('\n ❌ 存在差异,请修复后再提交');
process.exit(1);
} else {
console.log('\n ✅ 全部通过');
process.exit(0);
}
}
main();