79 lines
2.1 KiB
JavaScript
79 lines
2.1 KiB
JavaScript
/**
|
|||
|
|
* Harvest EN→locale phrase map from already-translated message keys,
|
||
|
|
* then merge into phrase-map.json (does not overwrite non-empty existing locales).
|
||
|
|
*
|
||
|
|
* Run: node apps/web/scripts/harvest-phrase-map.mjs
|
||
|
|
*/
|
||
|
|
import fs from "node:fs";
|
||
|
|
import path from "node:path";
|
||
|
|
import { fileURLToPath } from "node:url";
|
||
|
|
|
||
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
|
const messagesDir = path.resolve(__dirname, "../src/lib/i18n/messages");
|
||
|
|
const mapPath = path.join(__dirname, "phrase-map.json");
|
||
|
|
const LOCALES = ["es", "fr", "de", "it", "pt", "nl", "pl", "ja"];
|
||
|
|
|
||
|
|
function parseMessageDict(source) {
|
||
|
|
const dict = {};
|
||
|
|
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
|
||
|
|
let m;
|
||
|
|
while ((m = re.exec(source))) {
|
||
|
|
const key = m[1];
|
||
|
|
const raw = m[2];
|
||
|
|
dict[key] = raw.startsWith("`")
|
||
|
|
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
|
||
|
|
: JSON.parse(raw);
|
||
|
|
}
|
||
|
|
return dict;
|
||
|
|
}
|
||
|
|
|
||
|
|
function load(code) {
|
||
|
|
return parseMessageDict(fs.readFileSync(path.join(messagesDir, `${code}.ts`), "utf8"));
|
||
|
|
}
|
||
|
|
|
||
|
|
const en = load("en");
|
||
|
|
const packs = Object.fromEntries(LOCALES.map((c) => [c, load(c)]));
|
||
|
|
const map = fs.existsSync(mapPath) ? JSON.parse(fs.readFileSync(mapPath, "utf8")) : {};
|
||
|
|
|
||
|
|
let harvestedPhrases = 0;
|
||
|
|
let filledSlots = 0;
|
||
|
|
let newPhrases = 0;
|
||
|
|
|
||
|
|
for (const [key, enVal] of Object.entries(en)) {
|
||
|
|
if (!enVal || typeof enVal !== "string") continue;
|
||
|
|
const byLoc = {};
|
||
|
|
let good = 0;
|
||
|
|
for (const code of LOCALES) {
|
||
|
|
const v = packs[code][key];
|
||
|
|
if (typeof v === "string" && v.trim() && v !== enVal) {
|
||
|
|
byLoc[code] = v;
|
||
|
|
good += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (good < 4) continue;
|
||
|
|
|
||
|
|
const existing = map[enVal] ?? {};
|
||
|
|
let changed = false;
|
||
|
|
const next = { ...existing };
|
||
|
|
for (const code of LOCALES) {
|
||
|
|
if ((!next[code] || !String(next[code]).trim()) && byLoc[code]) {
|
||
|
|
next[code] = byLoc[code];
|
||
|
|
filledSlots += 1;
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (changed) {
|
||
|
|
if (!map[enVal]) newPhrases += 1;
|
||
|
|
map[enVal] = next;
|
||
|
|
harvestedPhrases += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fs.writeFileSync(mapPath, JSON.stringify(map, null, "\t") + "\n", "utf8");
|
||
|
|
console.log({
|
||
|
|
phraseCount: Object.keys(map).length,
|
||
|
|
harvestedPhrases,
|
||
|
|
newPhrases,
|
||
|
|
filledSlots,
|
||
|
|
});
|