mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 15:22:21 +00:00
i18n: generate zh-Hant from zh-CN
zh-Hant is the zh-CN text in Traditional characters, but contributors still translated it by hand from `--missing zh-Hant`, and a model asked for Traditional Chinese writes Taiwan vocabulary (影片, 伺服器, 載入). `npm run i18n -- --zh-hant` now rewrites zh-Hant.json from zh-CN.json with OpenCC s2tw (opencc-js 1.4.2, cn -> tw, characters only), then the fixed list of characters it picks wrongly. The check that `npm test` runs fails when zh-Hant differs from that conversion, naming the key, and `--missing zh-Hant` points to the command instead of printing a template. The conversion reproduces every one of the 3346 entries on main. 25 keys were out of zh-CN order and move; no value changes. The converter loads on first use, so a copy of the script without node_modules (stale-index.test.mjs) still extracts messages.
This commit is contained in:
1 parent
42540ad85e
commit
5d28d614e0
9 files changed
+230
-38
No files matched your search
+121
-8
@@ -3,17 +3,21 @@
|
||||
// npm run i18n check every catalog and find unmarked UI text
|
||||
// npm run i18n -- --prune also drop entries the code no longer uses
|
||||
// npm run i18n -- --missing ru print what ru lacks, as JSON to translate
|
||||
// npm run i18n -- --zh-hant rewrite zh-Hant from zh-CN (see toZhHant)
|
||||
//
|
||||
// A translation left as the English fails unless src/i18n/same-as-english.json
|
||||
// lists it.
|
||||
// lists it. zh-Hant is not translated: it is generated from zh-CN, and the
|
||||
// check fails when it differs.
|
||||
//
|
||||
// Messages are the English literals passed to t(), tn() and msg() in src/,
|
||||
// plus the markup opted in with data-i18n* in index.html and the Vue views.
|
||||
// It also lists UI text that is not marked at all (see findUnmarked).
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const LOCALES_DIR = path.join(ROOT, 'src/i18n/locales');
|
||||
@@ -618,16 +622,123 @@ export function missingTemplate(locale, catalog, messages) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// zh-Hant is zh-CN in Traditional characters (docs/translations/zh-Hant.md):
|
||||
// the same wording, only the script differs. OpenCC s2tw (opencc-js, cn → tw)
|
||||
// converts characters only; s2twp would also swap in Taiwan vocabulary
|
||||
// (影片, 伺服器, 載入), which the catalog does not use.
|
||||
const ZH_HANT = 'zh-Hant';
|
||||
const ZH_HANT_SOURCE = 'zh-CN';
|
||||
const ZH_HANT_COMMAND = 'npm run i18n -- --zh-hant';
|
||||
|
||||
// Characters s2tw picks wrongly in this text, replaced after the conversion.
|
||||
const ZH_HANT_CORRECTIONS = [
|
||||
['年曆史', '年歷史'],
|
||||
['下遊', '下游'],
|
||||
['並道', '併道'],
|
||||
['發佈', '發布'],
|
||||
['公佈', '公布'],
|
||||
['控制檯', '控制台'],
|
||||
// 周 as a week: the time-lapse speed in weeks per second.
|
||||
[' 周/秒', ' 週/秒'],
|
||||
];
|
||||
|
||||
let s2tw;
|
||||
|
||||
/** zh-CN text in Traditional characters, as zh-Hant shows it. */
|
||||
export function toZhHant(text) {
|
||||
// Loaded on first use: extracting messages needs no converter, and a copy
|
||||
// of this script without node_modules (stale-index.test.mjs) still runs.
|
||||
s2tw ??= createRequire(import.meta.url)('opencc-js/cn2t').Converter({
|
||||
from: 'cn',
|
||||
to: 'tw',
|
||||
});
|
||||
return ZH_HANT_CORRECTIONS.reduce(
|
||||
(out, [wrong, right]) => out.replaceAll(wrong, right),
|
||||
s2tw(text),
|
||||
);
|
||||
}
|
||||
|
||||
/** The zh-Hant catalog for a zh-CN catalog: the same keys in the same order. */
|
||||
export function zhHantCatalog(zhCN) {
|
||||
const convert = (entry) => {
|
||||
if (typeof entry === 'string') return toZhHant(entry);
|
||||
if (entry && typeof entry === 'object')
|
||||
return Object.fromEntries(
|
||||
Object.entries(entry).map(([category, form]) => [
|
||||
category,
|
||||
convert(form),
|
||||
]),
|
||||
);
|
||||
return entry;
|
||||
};
|
||||
return Object.fromEntries(
|
||||
Object.entries(zhCN).map(([text, entry]) => [text, convert(entry)]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a zh-Hant catalog is not the conversion of the zh-CN one.
|
||||
* @returns {string[]} One error per entry, naming its key.
|
||||
*/
|
||||
export function checkZhHant(zhCN, zhHant) {
|
||||
const expected = zhHantCatalog(zhCN);
|
||||
const fix = `zh-Hant is generated: edit ${ZH_HANT_SOURCE}, then run ${ZH_HANT_COMMAND}`;
|
||||
const errors = [];
|
||||
for (const [text, entry] of Object.entries(expected)) {
|
||||
if (!Object.hasOwn(zhHant, text))
|
||||
errors.push(`"${text}" is in ${ZH_HANT_SOURCE} but not here. ${fix}`);
|
||||
else if (!isDeepStrictEqual(zhHant[text], entry))
|
||||
errors.push(
|
||||
`"${text}" is ${JSON.stringify(zhHant[text])}, not ${ZH_HANT_SOURCE} converted (${JSON.stringify(entry)}). ${fix}`,
|
||||
);
|
||||
}
|
||||
for (const text of Object.keys(zhHant)) {
|
||||
if (!Object.hasOwn(expected, text))
|
||||
errors.push(`"${text}" is not in ${ZH_HANT_SOURCE}. ${fix}`);
|
||||
}
|
||||
if (!errors.length) {
|
||||
const order = Object.keys(expected);
|
||||
const keys = Object.keys(zhHant);
|
||||
const at = order.findIndex((text, i) => keys[i] !== text);
|
||||
if (at >= 0)
|
||||
errors.push(
|
||||
`entries leave the ${ZH_HANT_SOURCE} order at "${order[at]}": run ${ZH_HANT_COMMAND}`,
|
||||
);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
const serialize = (catalog) => `${JSON.stringify(catalog, null, 2)}\n`;
|
||||
|
||||
/** Rewrite zh-Hant.json as the conversion of zh-CN.json. */
|
||||
export function writeZhHant() {
|
||||
const catalog = zhHantCatalog(readCatalog(ZH_HANT_SOURCE));
|
||||
writeFileSync(catalogPath(ZH_HANT), serialize(catalog));
|
||||
return Object.keys(catalog).length;
|
||||
}
|
||||
|
||||
function main(args) {
|
||||
const messages = extractMessages();
|
||||
const missingFor = args.includes('--missing')
|
||||
? args[args.indexOf('--missing') + 1]
|
||||
: null;
|
||||
if (missingFor?.toLowerCase() === ZH_HANT.toLowerCase()) {
|
||||
console.error(
|
||||
`${ZH_HANT} is generated, not translated: add the entries to ${ZH_HANT_SOURCE} (npm run i18n -- --missing ${ZH_HANT_SOURCE}), then run ${ZH_HANT_COMMAND}`,
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (missingFor) {
|
||||
const out = missingTemplate(missingFor, readCatalog(missingFor), messages);
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
return 0;
|
||||
}
|
||||
if (args.includes('--zh-hant')) {
|
||||
const count = writeZhHant();
|
||||
console.log(
|
||||
`${ZH_HANT}: wrote ${count} entries converted from ${ZH_HANT_SOURCE}`,
|
||||
);
|
||||
}
|
||||
let failed = false;
|
||||
for (const locale of shippedLocales()) {
|
||||
let catalog = readCatalog(locale);
|
||||
@@ -636,10 +747,7 @@ function main(args) {
|
||||
catalog = Object.fromEntries(
|
||||
Object.entries(catalog).filter(([text]) => messages.has(text)),
|
||||
);
|
||||
writeFileSync(
|
||||
catalogPath(locale),
|
||||
`${JSON.stringify(catalog, null, 2)}\n`,
|
||||
);
|
||||
writeFileSync(catalogPath(locale), serialize(catalog));
|
||||
console.log(`${locale}: pruned ${stale.length} unused entries`);
|
||||
} else if (stale.length) {
|
||||
failed = true;
|
||||
@@ -648,11 +756,16 @@ function main(args) {
|
||||
);
|
||||
for (const text of stale) console.log(` ${JSON.stringify(text)}`);
|
||||
}
|
||||
// shippedLocales() is sorted, so --prune has already pruned zh-CN.
|
||||
if (locale === ZH_HANT)
|
||||
errors.push(...checkZhHant(readCatalog(ZH_HANT_SOURCE), catalog));
|
||||
if (missing.length) {
|
||||
failed = true;
|
||||
console.log(
|
||||
`${locale}: ${missing.length} missing (npm run i18n -- --missing ${locale}):`,
|
||||
);
|
||||
const fill =
|
||||
locale === ZH_HANT
|
||||
? `add them to ${ZH_HANT_SOURCE}, then ${ZH_HANT_COMMAND}`
|
||||
: `npm run i18n -- --missing ${locale}`;
|
||||
console.log(`${locale}: ${missing.length} missing (${fill}):`);
|
||||
for (const text of missing)
|
||||
console.log(` ${JSON.stringify(text)} ${messages.get(text).where}`);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user