Files
diegosouzapw--omniroute/scripts/i18n/build-pending-from-missing.mjs
2026-07-13 13:39:12 +08:00

105 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
/**
* For every missing key in _audit.json, locate its English value by
* 1) finding the t("key") call site in the file
* 2) grabbing the line(s) immediately around it in the HEAD version of the file
*
* This rebuilds a complete _pending-keys.json after subagents have already
* rewritten .tsx files but en.json edits were lost.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { execSync } from "node:child_process";
const audit = JSON.parse(readFileSync("scripts/i18n/_audit.json", "utf8"));
function loadHead(file) {
try {
return execSync(`git show "HEAD:${file}"`, {
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
});
} catch {
return null;
}
}
const SKIP = new Set([
"templatePayloads.vision.system",
"templatePayloads.vision.userPrompt",
"templatePayloads.vision.imageUrl",
"templatePayloads.schemaCoercion.userPrompt",
"templatePayloads.schemaCoercion.toolDescription",
"templatePayloads.schemaCoercion.cityDescription",
]);
/** Find the line in NEW source where t("key") is used, then derive English value from HEAD */
function valueForKey(file, key) {
const cur = readFileSync(file, "utf8").split("\n");
const head = loadHead(file)?.split("\n") ?? [];
// String search avoids ReDoS — key is our own audit data but better safe
const needle1 = `t("${key}")`;
const needle2 = `t('${key}')`;
for (let i = 0; i < cur.length; i++) {
const line = cur[i];
if (!line.includes(needle1) && !line.includes(needle2)) continue;
// The original line in HEAD should be similar around the same area
// Find the most-recently corresponding HEAD line: scan backwards and forwards from i
const probe = [i, i - 1, i + 1, i - 2, i + 2, i - 3, i + 3];
for (const j of probe) {
if (j < 0 || j >= head.length) continue;
const line = head[j];
// Try to extract the English literal: between > <, or attribute value
const jsxMatch = line.match(/>([^<>{}\n]{2,})</);
if (jsxMatch && !jsxMatch[1].includes("{") && !jsxMatch[1].includes("=>")) {
const v = jsxMatch[1].trim();
if (v && /[A-Za-z]/.test(v) && v !== key) return v;
}
const attrMatch = line.match(
/\b(?:title|placeholder|aria-label|alt|label)\s*=\s*["']([^"']+)["']/
);
if (attrMatch) {
const v = attrMatch[1].trim();
if (v && /[A-Za-z]/.test(v) && v !== key) return v;
}
}
}
return null;
}
const inferredNamespaces = new Map();
function ensureNs(ns) {
if (!inferredNamespaces.has(ns)) inferredNamespaces.set(ns, {});
return inferredNamespaces.get(ns);
}
for (const entry of audit) {
if (!entry.missingKeys.length) continue;
const ns = entry.namespaces[0];
if (!ns) continue; // exampleTemplates etc. — skip
const target = ensureNs(ns);
for (const key of entry.missingKeys) {
if (SKIP.has(key)) continue;
if (target[key]) continue;
const value = valueForKey(entry.file, key);
if (value) {
target[key] = value;
} else {
// Could not infer — leave a TODO marker so we notice
target[key] = `__TODO__${key}`;
}
}
}
const out = Object.fromEntries(inferredNamespaces);
writeFileSync("scripts/i18n/_pending-keys.json", JSON.stringify(out, null, 2) + "\n");
let total = 0;
let todo = 0;
for (const [ns, keys] of Object.entries(out)) {
const cnt = Object.keys(keys).length;
total += cnt;
for (const v of Object.values(keys)) if (String(v).startsWith("__TODO__")) todo++;
console.log(`${ns}: ${cnt} keys`);
}
console.log(`\nTotal: ${total} keys (${todo} need manual resolution)`);