Files
qiming/qiming-mobile/scripts/sync-i18n-diff.js

309 lines
10 KiB
JavaScript
Raw 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.
#!/usr/bin/env node
/**
* i18n 多语言同步对比工具
*
* 对比平台导出的完整 i18n JSON 与本地 locale 文件,生成新增/修改的差异 JSON。
* 仅处理 Mobile.* 前缀的 key。支持 zh-CN / en-US / zh-TW 三种语言。
*
* 用法:
* node scripts/sync-i18n-diff.js --lang zh-CN --source ./i18n-config-zh-CN.json
* node scripts/sync-i18n-diff.js --lang en-US --source ./i18n-config-export.json --batch-size 100
*
* 输出文件(在 source 同目录):
* - i18n-diff-{lang}-added.json 新增的 key
* - i18n-diff-{lang}-modified.json 值有变化的 key
*/
const fs = require('fs');
const path = require('path');
// ── 参数解析 ──────────────────────────────────────────────
function parseArgs() {
const args = process.argv.slice(2);
const opts = { lang: 'zh-CN', batchSize: 0 };
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--lang':
opts.lang = args[++i];
break;
case '--source':
opts.source = args[++i];
break;
case '--batch-size':
opts.batchSize = parseInt(args[++i], 10) || 0;
break;
case '--help':
case '-h':
console.log(`
i18n 多语言同步对比工具
对比平台导出的完整 i18n JSON 与本地 locale 文件,生成新增/修改的差异 JSON。
仅处理 Mobile.* 前缀的 key支持 zh-CN / en-US / zh-TW 三种语言。
用法:
node scripts/sync-i18n-diff.js --lang <语言> --source <导出文件> [--batch-size <数量>]
参数:
--lang <lang> 目标语言: zh-CN | en-US | zh-TW (默认 zh-CN)
--source <file> 平台导出的 JSON 文件路径
--batch-size <n> 分批大小 (0 = 不分批,默认 0)
-h, --help 显示帮助
示例:
# zh-CN 对比(不分批)
node scripts/sync-i18n-diff.js --lang zh-CN --source .temp/i18n-platform/source/i18n-config-zh-CN.json
# en-US 对比,每批 100 个
node scripts/sync-i18n-diff.js --lang en-US --source .temp/i18n-platform/source/i18n-config-en-US.json --batch-size 100
# zh-TW 对比,每批 100 个
node scripts/sync-i18n-diff.js --lang zh-TW --source .temp/i18n-platform/source/i18n-config-zh-TW.json --batch-size 100
输出:
差异 JSON 输出到 source 文件同目录下:
i18n-diff-{lang}-added.json 新增的 key
i18n-diff-{lang}-modified.json 值有变化的 key
i18n-diff-{lang}-modified-N-of-M.json 分批输出(--batch-size > 0 时)
`);
process.exit(0);
}
}
if (!opts.source) {
console.error('Error: --source is required');
process.exit(1);
}
return opts;
}
const LANG_FILE_MAP = {
'zh-CN': 'zh-cn.uts',
'en-US': 'en-us.uts',
'zh-TW': 'zh-tw.uts',
};
// ── 解析平台导出 JSON ────────────────────────────────────
function parseExport(filePath, targetLang) {
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const configs = raw.configs || raw;
const map = {};
for (const c of configs) {
if (c.lang !== targetLang) continue;
const key = c.fieldKey;
if (!key || !key.startsWith('Mobile.')) continue;
map[key] = c.fieldValue;
}
return map;
}
// ── 解析本地 locale TS 文件 ──────────────────────────────
function parseLocaleFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const map = {};
// 匹配 "Mobile.xxx": "value" 或 "Mobile.xxx": 'value'
const re = /^[\s]*"(Mobile\.[^"]+)":\s*"((?:[^"\\]|\\.)*)"/gm;
let m;
while ((m = re.exec(content)) !== null) {
map[m[1]] = m[2];
}
return map;
}
// ── 对比 ──────────────────────────────────────────────────
function diffKeys(exportMap, localMap) {
const added = {};
const modified = {};
const localOnly = {};
for (const [key, value] of Object.entries(exportMap)) {
if (!(key in localMap)) {
added[key] = value;
} else if (localMap[key] !== value) {
modified[key] = { local: localMap[key], remote: value };
}
}
// 本地有但平台没有的 key
for (const [key, value] of Object.entries(localMap)) {
if (!(key in exportMap)) {
localOnly[key] = value;
}
}
return { added, modified, localOnly };
}
// ── 分批 ──────────────────────────────────────────────────
function splitBatches(obj, batchSize) {
const entries = Object.entries(obj);
if (!batchSize || batchSize <= 0 || entries.length <= batchSize) {
return [obj];
}
const batches = [];
for (let i = 0; i < entries.length; i += batchSize) {
const batch = {};
for (const [k, v] of entries.slice(i, i + batchSize)) {
batch[k] = v;
}
batches.push(batch);
}
return batches;
}
// ── 主流程 ────────────────────────────────────────────────
function main() {
const opts = parseArgs();
const localeFileName = LANG_FILE_MAP[opts.lang];
if (!localeFileName) {
console.error(
`Error: unsupported lang "${opts.lang}". Use zh-CN | en-US | zh-TW.`,
);
process.exit(1);
}
const sourcePath = path.resolve(opts.source);
if (!fs.existsSync(sourcePath)) {
console.error(`Error: source file not found: ${sourcePath}`);
process.exit(1);
}
const localePath = path.resolve(
__dirname,
`../constants/i18n-locales/${localeFileName}`,
);
if (!fs.existsSync(localePath)) {
console.error(`Error: locale file not found: ${localePath}`);
process.exit(1);
}
console.log(`\n=== i18n Diff Report (${opts.lang}) ===`);
console.log(`Source: ${sourcePath}`);
console.log(`Local: ${localePath}\n`);
const exportMap = parseExport(sourcePath, opts.lang);
const localMap = parseLocaleFile(localePath);
const { added, modified, localOnly } = diffKeys(exportMap, localMap);
const addedCount = Object.keys(added).length;
const modifiedCount = Object.keys(modified).length;
const localOnlyCount = Object.keys(localOnly).length;
const unchangedCount =
Object.keys(exportMap).length - addedCount - modifiedCount;
console.log(`Export Mobile.* keys: ${Object.keys(exportMap).length}`);
console.log(`Local Mobile.* keys: ${Object.keys(localMap).length}`);
console.log(` Added (platform has, local missing): ${addedCount}`);
console.log(` Modified (value differs): ${modifiedCount}`);
console.log(` Local-only (local has, platform missing): ${localOnlyCount}`);
console.log(` Unchanged: ${unchangedCount}\n`);
if (addedCount > 0) {
console.log('--- Added Keys (sample) ---');
for (const [k, v] of Object.entries(added).slice(0, 5)) {
console.log(` + ${k}: ${v.slice(0, 60)}`);
}
if (addedCount > 5) console.log(` ... and ${addedCount - 5} more`);
console.log();
}
if (modifiedCount > 0) {
console.log('--- Modified Keys (sample) ---');
for (const [k, v] of Object.entries(modified).slice(0, 5)) {
console.log(` ~ ${k}:`);
console.log(` local: ${v.local.slice(0, 60)}`);
console.log(` remote: ${v.remote.slice(0, 60)}`);
}
if (modifiedCount > 5) console.log(` ... and ${modifiedCount - 5} more`);
console.log();
}
if (localOnlyCount > 0) {
console.log('--- Local-only Keys (sample) ---');
for (const [k, v] of Object.entries(localOnly).slice(0, 5)) {
console.log(` - ${k}: ${v.slice(0, 60)}`);
}
if (localOnlyCount > 5) console.log(` ... and ${localOnlyCount - 5} more`);
console.log();
}
// 写入差异 JSON — 输出到 .temp/i18n-platform/diff/{lang}/
const baseDir = path.resolve(__dirname, '../.temp/i18n-platform');
const outDir = path.join(baseDir, 'diff', opts.lang);
fs.mkdirSync(outDir, { recursive: true });
if (addedCount > 0) {
const addedBatches = splitBatches(added, opts.batchSize);
if (addedBatches.length === 1) {
const outFile = path.join(outDir, `i18n-diff-${opts.lang}-added.json`);
fs.writeFileSync(outFile, JSON.stringify(added, null, 2), 'utf8');
console.log(`Written: ${outFile} (${addedCount} keys)`);
} else {
addedBatches.forEach((batch, i) => {
const outFile = path.join(
outDir,
`i18n-diff-${opts.lang}-added-${i + 1}-of-${
addedBatches.length
}.json`,
);
fs.writeFileSync(outFile, JSON.stringify(batch, null, 2), 'utf8');
console.log(`Written: ${outFile} (${Object.keys(batch).length} keys)`);
});
}
}
if (modifiedCount > 0) {
const modifiedBatches = splitBatches(modified, opts.batchSize);
if (modifiedBatches.length === 1) {
const outFile = path.join(outDir, `i18n-diff-${opts.lang}-modified.json`);
fs.writeFileSync(outFile, JSON.stringify(modified, null, 2), 'utf8');
console.log(`Written: ${outFile} (${modifiedCount} keys)`);
} else {
modifiedBatches.forEach((batch, i) => {
const outFile = path.join(
outDir,
`i18n-diff-${opts.lang}-modified-${i + 1}-of-${
modifiedBatches.length
}.json`,
);
fs.writeFileSync(outFile, JSON.stringify(batch, null, 2), 'utf8');
console.log(`Written: ${outFile} (${Object.keys(batch).length} keys)`);
});
}
}
if (localOnlyCount > 0) {
const localOnlyBatches = splitBatches(localOnly, opts.batchSize);
if (localOnlyBatches.length === 1) {
const outFile = path.join(
outDir,
`i18n-diff-${opts.lang}-local-only.json`,
);
fs.writeFileSync(outFile, JSON.stringify(localOnly, null, 2), 'utf8');
console.log(`Written: ${outFile} (${localOnlyCount} keys)`);
} else {
localOnlyBatches.forEach((batch, i) => {
const outFile = path.join(
outDir,
`i18n-diff-${opts.lang}-local-only-${i + 1}-of-${
localOnlyBatches.length
}.json`,
);
fs.writeFileSync(outFile, JSON.stringify(batch, null, 2), 'utf8');
console.log(`Written: ${outFile} (${Object.keys(batch).length} keys)`);
});
}
}
if (addedCount === 0 && modifiedCount === 0 && localOnlyCount === 0) {
console.log('No differences found. Local and platform are in sync.');
}
}
main();