chinese2english/scripts/merge-comments.mjs
spearzificial e1de66e75a fix: keep translated comments across a rebuild, cover two updated plugins
Checking the live instance a day later showed 2,023 Chinese characters back
in the served bundles, from two causes.

The comment merge rebuilt its dictionary from the scratch files in ~/.cache
only, so the committed translations were never an input. Once a later pass
added comments, a rebuild discarded them: the count stayed at 274 while
eight new translations vanished. src/dictionary.comments.json is now loaded
first and the scratch files overlay it, so a rebuild can only grow the
dictionary.

Two plugins had also shipped new Chinese since the last sweep:
dsh-builtin-browser (a new plugin, 834 characters of settings copy) and
dsh-our-free-model 2.5.4 (1,082 characters, mostly sponsor copy and
comments). Their strings were harvested from the live-served bytes and
translated: 70 UI strings into dictionary.residual.json and 8 comments into
the comment dictionary.

Verified on a fresh instance, escapes decoded: total Chinese across the 79
served bundles 2,023 -> 107, and in the browser both visible text and full
page HTML report 0 CJK. The 107 remaining characters are the functional
regular expressions that match Chinese input, as before.
2026-10-20 15:03:56 +05:30

76 lines
3.4 KiB
JavaScript

// Builds src/dictionary.comments.json from the translated comments.
//
// The committed dictionary is the source of truth: it is loaded first, then any
// newer translation passes overlay it. Earlier this read only the scratch files
// in ~/.cache, so once those were cleared a rebuild silently produced a smaller
// dictionary and translated comments reverted to Chinese.
//
// Overlays, when present:
// ~/.cache/c2e/cmt-authored.json {wholeComment: translatedComment}
// ~/.cache/c2e/css-authored.json {body: translatedBody} (delimiters added)
// ~/.cache/c2e/c2-authored.json {wholeComment: translatedComment}
//
// Output: src/dictionary.comments.json {wholeComment: translatedComment}
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const C2E = "/home/oiupoyt/.cache/c2e";
const OUT = "/home/oiupoyt/my stuff yea/projects/chinese2english/src/dictionary.comments.json";
const HAN = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/;
const BOX = /[\u2500-\u257f\u2580-\u259f]/g;
const boxOf = (t) => (t.match(BOX) ?? []).sort().join("");
const newlines = (t) => (t.match(/\n/g) ?? []).length;
function load(name, fallback) {
const path = `${C2E}/${name}`;
if (!existsSync(path)) return fallback ?? {};
return JSON.parse(readFileSync(path, "utf8"));
}
const pairs = new Map();
// Existing committed translations survive a rebuild.
if (existsSync(OUT)) {
for (const [zh, en] of Object.entries(JSON.parse(readFileSync(OUT, "utf8")))) pairs.set(zh, en);
}
for (const [zh, en] of Object.entries(load("cmt-authored.json"))) pairs.set(zh, en);
for (const [zh, en] of Object.entries(load("c2-authored.json"))) pairs.set(zh, en);
for (const [zh, en] of Object.entries(load("css-authored.json"))) {
// A stylesheet body is stored without delimiters; the emitted key carries them.
if (zh.includes("/*")) pairs.set(zh, en);
else pairs.set(`/*${zh}*/`, `/*${en}*/`);
}
const clean = {};
const reasons = { cjk: 0, latin: 0, identity: 0, box: 0, lines: 0, syntax: 0 };
for (const [zh, en] of pairs) {
if (typeof en !== "string" || !en.trim()) continue;
if (HAN.test(en)) { reasons.cjk++; continue; }
if (!/[A-Za-z]/.test(en)) { reasons.latin++; continue; }
if (en === zh) { reasons.identity++; continue; }
// Replacing a comment must keep it a well-formed comment, or the emitted
// bundle stops parsing.
if (zh.includes("/*") !== en.includes("/*")) { reasons.syntax++; continue; }
if (zh.includes("*/") !== en.includes("*/")) { reasons.syntax++; continue; }
if (zh.startsWith("//") !== en.startsWith("//")) { reasons.syntax++; continue; }
// Box-drawing runs are the structure of block comments, where they align the
// `*` column. In a single-line `//` banner the run is pure decoration and its
// length shifts freely with the translated text.
const isLineComment = zh.startsWith("//");
if (!isLineComment && boxOf(zh) !== boxOf(en)) { reasons.box++; continue; }
if (newlines(zh) !== newlines(en)) { reasons.lines++; continue; }
clean[zh] = en;
}
const sorted = Object.fromEntries(
Object.entries(clean).sort((a, b) => b[0].length - a[0].length || a[0].localeCompare(b[0])),
);
writeFileSync(OUT, JSON.stringify(sorted, null, 1) + "\n");
const dropped = Object.values(reasons).reduce((a, b) => a + b, 0);
console.log(
`comments: ${Object.keys(sorted).length} entries (dropped ${dropped}` +
Object.entries(reasons).filter(([, v]) => v).map(([k, v]) => `, ${k} ${v}`).join("") +
")",
);