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.
This commit is contained in:
spearzificial 2026-10-20 15:03:56 +05:30
parent ab0179545a
commit e1de66e75a
6 changed files with 253 additions and 13 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,15 @@
// Builds src/dictionary.comments.json from the translated comments.
//
// Two shapes feed this and both become whole delimited comments, which is what
// the rewriter matches:
// - Comment BODIES (text between `/*` and `*/`) from the stylesheet pass,
// re-wrapped in their delimiters.
// - Whole comments FROM the code-comment pass, already delimited.
// 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}
//
// Inputs: ~/.cache/c2e/cmt-authored.json {wholeComment: translatedComment}
// ~/.cache/c2e/css-authored.json {body: translatedBody}
// Output: src/dictionary.comments.json {wholeComment: translatedComment}
import { readFileSync, writeFileSync, existsSync } from "node:fs";
@ -19,14 +21,26 @@ 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) {
function load(name, fallback) {
const path = `${C2E}/${name}`;
return existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
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("css-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 };

View file

@ -170,12 +170,14 @@
"/* ── ② 按钮 .ofm_btn ─────────────────────────── */": "/* ── ② Buttons .ofm_btn ─────────────────────────── */",
"/* 数字列表针走格感:tabular-nums + pl5-tick 机械顿挫(steps) */": "/* Numeric list needle stepping: tabular-nums + pl5-tick mechanical detents (steps) */",
"/** 上次同步账号库的时刻(0 = 还没读过;初始化那次 refresh 之前会先读一次)。 */": "/** Timestamp of the last account store sync (0 = not read yet; it is read once before the init refresh). */",
"/** 使用用户当地的日历日期,不能用 UTC 的 toISOString() 划分“今天”。 */": "/** Use the user's local calendar date; a UTC toISOString() must not decide what \"today\" is. */",
"/** 最近一次自动换号(后端只在**自动**切号时写;手动切不记)。null = 还没有过。 */": "/** The last automatic account rotation (the backend writes it only on an **automatic** switch; manual switches are not recorded). null = never yet. */",
"/* ── ⑧【师傅批改①】拉丝纹理增强:金属感要真的能摸到 ─────────────── */": "/* ── 8 [Master review 1] Brushed texture upgrade: the metal feel has to be actually touchable ─────────────── */",
"/* 挂到 KPI 大数字与统计卡数字:hover 时数字以机械顿挫走一格,模拟仪表指针跳动 */": "/* Attach to the KPI big number and the stat card numbers: on hover the digits step one notch with a mechanical jolt, mimicking a gauge needle */",
"// because \"累计\" that resets at midnight is a lie.": "// because \"cumulative\" that resets at midnight is a lie.",
"/* chip 选中态:JS 注入彩色背景,这里压回宿主语言——主色薄涂+主色边,去掉杂色 */": "/* chip selected state: JS injects a colored background, so push it back to the host language here -- light primary tint + primary border, no stray colors */",
"/* 滑块用 --ease-settle 带回弹;按下时滑块变宽=被\"捏扁\",松开回弹归位 */": "/* The slider uses --ease-settle with bounce; on press it widens = gets \"squashed\", on release it springs back into place */",
"/** 同源窗口共用 Web Lock;只有公告成功打开后才记账。存储禁用时跳过自动广告。 */": "/** Same-origin windows share a Web Lock; bookkeeping happens only once the announcement opened. Auto-ads are skipped when storage is disabled. */",
"// 8 秒超时兜底:后端路由未注册/挂起时 fetch 不再永久挂起——此前全部 api 调用": "// 8s timeout fallback: when a backend route is unregistered/hung, fetch no longer hangs forever -- previously every api call",
"/* ── ⑧【师傅批改③】表格竖条语言统一:所有表格行 hover 都带主色竖条 ── */": "/* ── 8 [Master review 3] Unify the table rail language: every table row hover carries the primary-color rail ── */",
"/* 夜模式 3% → 4.5%,昼模式 1.5% → 2.8%,纹理间隔收紧让拉丝更密 */": "/* Dark mode 3% -> 4.5%, light mode 1.5% -> 2.8%; tighten the texture spacing so the brushed grain is denser */",
@ -185,6 +187,7 @@
"// 问题」,动作是「一眼能看到的星按钮」,数字是宿主 /picks 取回的真实 Star /": "// problem\", the action is \"a Star button you cannot miss\", the numbers are the real Star the host fetches via /picks",
"/** 用最近一次载荷重画(折叠这类纯前端状态用它,不走重建签名、也不打 HTTP)。 */": "/** Redraw from the last payload (used for purely client-side state like collapse; no rebuild signature, no HTTP request). */",
"// 实时积分:每个账号的剩余余额(credits.balances),卡片汇总 + 账号行": "// Live credits: each account's remaining balance (credits.balances), card total + account row",
"// 宿主在 document 上用 Esc 关闭设置页;公告打开时先消费按键,保留设置页。": "// The host closes the settings page on document-level Esc; while the announcement is open, consume the key first so the settings page survives.",
"// 超时兜底:3 秒拿不到 ack(后端挂起/超时/异常)→ 当作已 ack 主动放行——": "// timeout fallback: no ack in 3 seconds (backend hung/timed out/threw) -> treat as acked and let it through --",
"/* 整行染成纯错误色在深色底上会刺眼且发暗;混一点正文色压回来,色条仍然说明性质。 */": "/* Painting a whole row in the pure error color is harsh and dark on a dark background; mix in a bit of the body text color to pull it back, and the bar still states the nature. */",
"/* 聚焦环:仪器刻度环——细环+外晕,用 --ease-instrument 淡入 */": "/* Focus ring: an instrument dial ring -- thin ring + outer glow, faded in with --ease-instrument */",
@ -199,6 +202,7 @@
"/* 说明文字:限宽 64ch —— 满宽的一行小字读起来很累,且会拖长整张卡。 */": "/* Helper text: capped at 64ch -- a full-width line of small text is tiring to read and stretches the whole card. */",
"// GitHub 授权状态由这一层持有:模型卡的锁标记与 EAC 面板必须看到同一个": "// GitHub authorization state is held by this layer: the model card's lock marker and the EAC panel must see the same",
"// 只有拿到过明确判定(网关答过 /eac/status)且未授权时才上锁:状态未知": "// Lock only when there is an explicit verdict (the gateway answered /eac/status) and it is unauthorized: unknown state",
"// 清除收起动画的 forwards 状态,否则同一界面跨日再打开会透明并卡住操作。": "// Clear the collapse animation's forwards state, or reopening the same view on a later day leaves it transparent and blocks interaction.",
"// 默认档(balanced)而不是 deep:测速要回答的是“这个模型日常多快”,": "// default tier (balanced) rather than deep: a speed test answers \"how fast is this model day to day\",",
"/** 最近一次 /usage 的响应(切换\"按账号/按模型\"时不必重新拉)。 */": "/** The most recent /usage response (no need to refetch when switching \"by account / by model\"). */",
"/** 渲染进程是否支持 File System Access 的「另存为」。 */": "/** Whether the renderer supports File System Access \"Save As\". */",
@ -217,10 +221,12 @@
"/* 控件需要占满整行时(滑块 + 数值)用这个变体:文字在上、控件在下。 */": "/* Use this variant when a control must span the full row (slider + value): text on top, control below. */",
"/** 24 根小柱:每小时调用量。高度按最大值归一,失败多的时段用红色。 */": "/** 24 small bars: calls per hour. Height normalised by the maximum; hours with many failures are red. */",
"/** 与现有 RPC 的能力对应:余额、每日领取及独立签到状态分别判断。 */": "/** Maps to the capabilities of the existing RPCs: balance, daily claim, and standalone check-in state are judged separately. */",
"/** 两端共用 React 组件;插件通过构建脚本内联,独立端直接导入。 */": "/** Both sides share the React components; the plugin inlines them at build time while the standalone side imports them directly. */",
"// 这一回合的 token 去向。聊天窗属于宿主内核,插件无法在对话流里注入,": "// where this round's token went. The chat window belongs to the host kernel, the plugin cannot inject into the conversation stream,",
"// 重新打开——永久关闭让公告/更新/升级推送在宿主重启或休眠后整体静默失效,": "// reopen it -- a permanent close leaves announcement/update/upgrade pushes silently dead across host restarts or sleep,",
"// 闸门在服务器:网关拒绝没有用户令牌的对话,而这个面板是取得令牌的唯一入口。": "// The gate is server-side: the gateway rejects conversations with no user token, and this panel is the only way in to get one.",
"/* 状态点呼吸灯保留,但改成 pl5- 前缀、机械顿挫更明显的两拍节奏 */": "/* Keep the breathing status dot, but move it under the pl5- prefix and give it a two-beat rhythm with a sharper mechanical step */",
"/** 赞助内容与每日公告交互:浏览器专用,不读取账号或调用赞助商接口。 */": "/** Sponsored content and daily announcement interaction: browser-only; reads no account and calls no sponsor endpoint. */",
"// 「最近一回合」跟着一个轻量轮询走(#26):看板其余部分仍是页面加载时的": "// \"Latest round\" rides a lightweight poll (#26): the rest of the board is still the one from page load",
"// 免费车道转发端口的接入配方。它和上面这块网关是两条独立监听:这里的地址与": "// The connection recipe for the free lane's forwarding port. It and the gateway above are two independent listeners: the address here and",
"// 复制粘贴:后端把授权页交给系统浏览器,前端轮询网关领取令牌。令牌本身从不": "// Copy-paste: the backend hands the authorization page to the system browser, the frontend polls the gateway for the token. The token itself never",
@ -236,6 +242,7 @@
"/** 把任意值吸附到最近的档位(配置里可能是旧值或手改过的值)。 */": "/** Snap any value to the nearest tier (the config may hold an old value or a hand-edited one). */",
"// 配方(未启用时 /clients 回的是占位端口与占位 Key)。": "// recipe (when not enabled /clients returns placeholder ports and a placeholder Key).",
"/* tab:激活项在滑块底衬之上再补一道品牌下划线,选中更明确 */": "/* tab: the active item adds a brand underline on top of the slider track, so the selection reads more clearly */",
"// 插件挂在宿主设置弹窗中;菜单所在的这一层不是需要避让的其他弹窗。": "// The plugin mounts inside the host settings dialog; this menu layer is not another dialog to dodge.",
"// 的宿主、非常规 base)就退回根路径原值——行为不比修复前差。": "// host, unusual base) fall back to the root-path original -- no worse than before the fix.",
"// 会被清零,退避就永远是第一档而不是 30s 翻倍到 5 分钟。": "// gets zeroed, so the backoff would forever sit at the first tier instead of doubling from 30s to 5 minutes.",
"// 保持可 grep;各仓库的地址与「第二个动作」在这里各写一份。": "// keep it greppable; each repo's address and \"second action\" are written once here.",
@ -245,6 +252,7 @@
"// 逐条展示;随账号列表与领积分动作刷新,另有一个慢轮询兜底。": "// shown row by row; refreshes with the account list and the claim-credits action, with a slow poll as backup.",
"/** 渲染宿主返回的节流设置(含可选档位与清理策略)。 */": "/** Render the throttle settings returned by the host (including selectable tiers and the cleanup policy). */",
"/** 短时间:今天只显示时分,其它显示月/日 时:分。 */": "/** Short time: for today show only hour:minute, otherwise show month/day hour:minute. */",
"// 写入失败时不弹窗,避免刷新、重挂载或隐私模式下反复打扰。": "// No dialog when the write fails: it would nag on every refresh, remount or private-mode session.",
"/** 倒计时:还剩 2 小时 13 分 / 已解除。 */": "/** Countdown: 2 hours 13 minutes left / lifted. */",
"// 不该显示一把凭空的锁,网关不可达时本地记录仍算已授权。": "// must not show a lock out of nowhere; when the gateway is unreachable the local record still counts as authorized.",
"/* 用户开了\"减少动态效果\"就别再让元素滑来滑去。 */": "/* If the user turned on \"reduce motion\", stop sliding elements around. */",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long