diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html index dcaed3d..e7c3271 100644 --- a/emrg/gui/renderer/index.html +++ b/emrg/gui/renderer/index.html @@ -21,7 +21,7 @@
-
🌱 已自我进化 0
+
🌱 已自我进化 0
边工作边学习,越用越懂你
@@ -146,7 +146,7 @@

设置

关于
-
EMRG v0.2.8 · 🌱 已自我进化 0 次
+
EMRG v0.2.8 · 🌱 已自我进化 0
EMRG 是一个会自我进化的 AI 智能体——每次改进都会自动汇报,你可以随时在这里看到它的成长。
diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js index 834badf..0fd7997 100644 --- a/emrg/gui/renderer/js/app.js +++ b/emrg/gui/renderer/js/app.js @@ -58,7 +58,7 @@ const App = (() => { return; } if (!init.project_dir_valid) { - Chat.addSystemMessage("工作目录不可用,请到设置里改一下。"); + Chat.addSystemMessage(_t("app.workdirInvalid")); Dialogs.showSettings(); return; } @@ -78,7 +78,7 @@ const App = (() => { // 回调会调用 setComposerDisabled(false),形成"需先发消息才能启用输入框"死锁) setComposerDisabled(false); } catch (e) { - Chat.addSystemMessage(`启动遇到了问题:${e.message}`); + Chat.addSystemMessage(_t("app.bootFailed", { msg: e.message })); } } @@ -97,7 +97,7 @@ const App = (() => { return; } if (!state.sessionId) { - Chat.addSystemMessage("请先创建一个对话。"); + Chat.addSystemMessage(_t("app.needSession")); return; } state.busy = true; @@ -127,27 +127,27 @@ const App = (() => { const cmd = parsed.cmd; const meta = Commands.COMMANDS[cmd]; if (!meta) { - Chat.addSystemMessage(`指令 ${cmd} 暂未开放。`); + Chat.addSystemMessage(_t("app.cmdUnknown", { cmd })); return; } try { switch (cmd) { case "/clear": if (!state.sessionId) { - Chat.addSystemMessage("请先创建一个对话。"); + Chat.addSystemMessage(_t("app.needSession")); return; } await window.emrg.clearSession({ sessionId: state.sessionId }); Chat.clear(); - Chat.addSystemMessage("已清空当前对话。"); + Chat.addSystemMessage(_t("app.cleared")); break; case "/compact": if (!state.sessionId) { - Chat.addSystemMessage("请先创建一个对话。"); + Chat.addSystemMessage(_t("app.needSession")); return; } await window.emrg.compactSession({ sessionId: state.sessionId }); - Chat.addSystemMessage("已压缩当前对话历史。"); + Chat.addSystemMessage(_t("app.compacted")); break; case "/version": showVersionInfo(); @@ -156,7 +156,7 @@ const App = (() => { showHelpDialog(); break; case "/image": - Chat.addSystemMessage("请直接粘贴图片到输入框(Ctrl+V / ⌘V)。"); + Chat.addSystemMessage(_t("app.imagePaste")); break; case "/sessions": case "/resume": @@ -170,7 +170,7 @@ const App = (() => { case "/rename": // P2:复用现有重命名对话框(右键菜单同款) if (!state.sessionId) { - Chat.addSystemMessage("请先创建一个对话。"); + Chat.addSystemMessage(_t("app.needSession")); return; } const cur = state.sessions.find((s) => s.session_id === state.sessionId); @@ -179,11 +179,11 @@ const App = (() => { case "/delete": // P2:复用现有删除确认(右键菜单同款) if (!state.sessionId) { - Chat.addSystemMessage("请先创建一个对话。"); + Chat.addSystemMessage(_t("app.needSession")); return; } Dialogs.showConfirm(EMRG_Copy.COPY.deleteConfirmTitle, EMRG_Copy.COPY.deleteConfirmBody, { - okText: "删除", + okText: _t("dlg.delete"), danger: true, onOk: () => deleteSession(state.sessionId), }); @@ -221,10 +221,10 @@ const App = (() => { } break; default: - Chat.addSystemMessage(`指令 ${cmd} 暂未开放。`); + Chat.addSystemMessage(_t("app.cmdUnknown", { cmd })); } } catch (e) { - Chat.addSystemMessage(`指令 ${cmd} 执行失败:${e.message}`); + Chat.addSystemMessage(_t("app.cmdFailed", { cmd, msg: e.message })); } } @@ -236,12 +236,12 @@ const App = (() => { await refreshSessions(); // 确保 state.sessions 最新 list.innerHTML = ""; if (state.sessions.length === 0) { - list.innerHTML = `
还没有对话,输入内容即可开始。
`; + list.innerHTML = `
${_t("app.helpNoSessions")}
`; } state.sessions.forEach((s) => { const row = el("button", { class: "help-row", type: "button", style: "width:100%;text-align:left;cursor:pointer;background:none;border:none;" }); - const name = el("span", { class: "help-cmd" }, s.title || "(未命名)"); - const hint = el("span", { class: "help-hint" }, s.session_id === state.sessionId ? "当前" : ""); + const name = el("span", { class: "help-cmd" }, s.title || _t("app.unnamed")); + const hint = el("span", { class: "help-hint" }, s.session_id === state.sessionId ? _t("app.current") : ""); row.appendChild(name); row.appendChild(hint); row.addEventListener("click", async () => { @@ -259,16 +259,16 @@ const App = (() => { const dialog = $("rewind-dialog"); if (!list || !dialog) return; if (!state.sessionId) { - Chat.addSystemMessage("请先创建一个对话。"); + Chat.addSystemMessage(_t("app.needSession")); return; } - list.innerHTML = `
加载中…
`; + list.innerHTML = `
${_t("dlg.loading")}
`; dialog.showModal(); try { const { messages } = await window.emrg.listHistory({ sessionId: state.sessionId }); list.innerHTML = ""; if (!messages || messages.length === 0) { - list.innerHTML = `
没有可回退的历史消息。
`; + list.innerHTML = `
${_t("app.noHistory")}
`; return; } // 倒序:最新消息点在最上 @@ -290,7 +290,7 @@ const App = (() => { list.appendChild(row); }); } catch (e) { - list.innerHTML = `
加载历史失败:${escapeHtml(e.message)}
`; + list.innerHTML = `
${_t("app.historyFailed", { msg: escapeHtml(e.message) })}
`; } } @@ -298,9 +298,9 @@ const App = (() => { try { const res = await window.emrg.rewindSession({ sessionId: state.sessionId, recordIndex }); Chat.clear(); - Chat.addSystemMessage(`已回退到消息点 #${recordIndex},移除了 ${res.removedCount ?? 0} 条记录。`); + Chat.addSystemMessage(_t("app.rewound", { index: recordIndex, n: res.removedCount ?? 0 })); } catch (e) { - Chat.addSystemMessage(`回退失败:${e.message}`); + Chat.addSystemMessage(_t("app.rewindFailed", { msg: e.message })); } } @@ -310,7 +310,7 @@ const App = (() => { const detail = $("memory-detail"); const dialog = $("memory-dialog"); if (!list || !dialog) return; - list.innerHTML = `
加载中…
`; + list.innerHTML = `
${_t("dlg.loading")}
`; if (detail) detail.classList.add("hidden"); dialog.showModal(); const scope = String(sub || "").toLowerCase() === "session" ? "session" : "project"; @@ -318,7 +318,7 @@ const App = (() => { const memories = await window.emrg.listMemories({ scope, sessionId: state.sessionId }); list.innerHTML = ""; if (!memories || memories.length === 0) { - list.innerHTML = `
还没有${scope === "session" ? "会话" : "项目"}记忆。
`; + list.innerHTML = `
${_t("app.noMemories", { scope: scope === "session" ? _t("app.sessionMem") : _t("app.projectMem") })}
`; return; } for (const m of memories) { @@ -327,7 +327,7 @@ const App = (() => { type: "button", style: "width:100%;text-align:left;cursor:pointer;background:none;border:none;", }); - const title = m.title || m.id || "(未命名)"; + const title = m.title || m.id || _t("app.unnamed"); const name = el("span", { class: "help-cmd" }, String(title).slice(0, 40)); const hint = el("span", { class: "help-hint" }, (m.summary || m.content || "").slice(0, 50)); row.appendChild(name); @@ -343,13 +343,13 @@ const App = (() => { Chat.addSystemMessage(body.slice(0, 500)); } } catch (err) { - Chat.addSystemMessage(`读取记忆失败:${err.message}`); + Chat.addSystemMessage(_t("app.readMemFailed", { msg: err.message })); } }); list.appendChild(row); } } catch (e) { - list.innerHTML = `
加载记忆失败:${escapeHtml(e.message)}
`; + list.innerHTML = `
${_t("app.memFailed", { msg: escapeHtml(e.message) })}
`; } } @@ -358,25 +358,25 @@ const App = (() => { const list = $("skills-list"); const dialog = $("skills-dialog"); if (!list || !dialog) return; - list.innerHTML = `
加载中…
`; + list.innerHTML = `
${_t("dlg.loading")}
`; dialog.showModal(); try { const skills = await window.emrg.listSkills(); list.innerHTML = ""; if (!skills || skills.length === 0) { - list.innerHTML = `
还没有加载技能。
`; + list.innerHTML = `
${_t("app.noSkills")}
`; return; } for (const s of skills) { const row = el("div", { class: "help-row" }); - const name = el("span", { class: "help-cmd" }, s.name || "(未命名)"); + const name = el("span", { class: "help-cmd" }, s.name || _t("app.unnamed")); const hint = el("span", { class: "help-hint" }, `${s.source || ""}${s.description ? " · " + s.description.slice(0, 50) : ""}`); row.appendChild(name); row.appendChild(hint); list.appendChild(row); } } catch (e) { - list.innerHTML = `
加载技能失败:${escapeHtml(e.message)}
`; + list.innerHTML = `
${_t("app.skillsFailed", { msg: escapeHtml(e.message) })}
`; } } @@ -389,7 +389,7 @@ const App = (() => { // 加载项目列表填充下拉 try { const projects = await window.emrg.listProjects(); - projSel.innerHTML = ``; + projSel.innerHTML = ``; for (const p of projects) { const name = typeof p === "string" ? p : (p.name || ""); if (name) projSel.appendChild(el("option", { value: name }, name)); @@ -403,14 +403,14 @@ const App = (() => { async function submitRant(message, project) { const text = String(message || "").trim(); if (!text) { - Chat.addSystemMessage("写点内容再提交吧。"); + Chat.addSystemMessage(_t("app.rantEmpty")); return; } try { const res = await window.emrg.sendRant({ message: text, project }); - Chat.addSystemMessage(`✓ 收到!EMRG 会据此进化${res.count ? `(已累计 ${res.count} 条反馈)` : ""}。`); + Chat.addSystemMessage(`${_t("app.rantReceived")}${res.count ? _t("app.rantCount", { n: res.count }) : ""}`); } catch (e) { - Chat.addSystemMessage(`提交失败了:${e.message}`); + Chat.addSystemMessage(_t("app.rantFailed", { msg: e.message })); } } @@ -419,13 +419,13 @@ const App = (() => { const list = $("tasks-list"); const dialog = $("tasks-dialog"); if (!list || !dialog) return; - list.innerHTML = `
加载中…
`; + list.innerHTML = `
${_t("dlg.loading")}
`; dialog.showModal(); try { const tasks = await window.emrg.listTasks(); list.innerHTML = ""; if (!tasks || tasks.length === 0) { - list.innerHTML = `
没有可触发的任务。
`; + list.innerHTML = `
${_t("app.noTasks")}
`; return; } for (const t of tasks) { @@ -434,8 +434,8 @@ const App = (() => { type: "button", style: "width:100%;text-align:left;cursor:pointer;background:none;border:none;", }); - const name = el("span", { class: "help-cmd" }, t.name || t.type || "(任务)" ); - const hint = el("span", { class: "help-hint" }, t.enabled === false ? "已停用" : `间隔 ${t.interval ?? "-"}s`); + const name = el("span", { class: "help-cmd" }, t.name || t.type || _t("app.unnamed")); + const hint = el("span", { class: "help-hint" }, t.enabled === false ? _t("app.taskDisabled") : _t("app.taskInterval", { n: t.interval ?? "-" })); row.appendChild(name); row.appendChild(hint); row.addEventListener("click", async () => { @@ -445,7 +445,7 @@ const App = (() => { list.appendChild(row); } } catch (e) { - list.innerHTML = `
加载任务失败:${escapeHtml(e.message)}
`; + list.innerHTML = `
${_t("app.tasksFailed", { msg: escapeHtml(e.message) })}
`; } } @@ -455,12 +455,12 @@ const App = (() => { try { const res = await window.emrg.triggerTask({ name: n }); if (res.error) { - Chat.addSystemMessage(`触发失败:${res.error}`); + Chat.addSystemMessage(_t("app.triggerFailed", { msg: res.error })); } else { - Chat.addSystemMessage(`已触发任务 ${n}。`); + Chat.addSystemMessage(_t("app.triggered", { n })); } } catch (e) { - Chat.addSystemMessage(`触发失败:${e.message}`); + Chat.addSystemMessage(_t("app.triggerFailed", { msg: e.message })); } } @@ -539,7 +539,7 @@ const App = (() => { Chat.clear(); updateEmptyState(); if (res.error === "session_not_found") { - Chat.addSystemMessage("这个对话已被删除,已帮你切到最近的对话。"); + Chat.addSystemMessage(_t("app.deletedSwitch")); if (res.next_session) { Sidebar.render(res.sessions || []); await switchSession(res.next_session, { silent: true }); @@ -550,13 +550,13 @@ const App = (() => { } // G13:v1 不加载历史(G12) if (!opts.silent) { - Chat.addSystemMessage("已切换对话。"); + Chat.addSystemMessage(_t("app.switched")); } updateEmptyState(); Sidebar.highlight(sid); setComposerDisabled(false); // 防御性:独立调用 switchSession 也确保输入框可用 } catch (e) { - Chat.addSystemMessage(`切换对话失败了:${e.message}`); + Chat.addSystemMessage(_t("app.switchFailed", { msg: e.message })); } } @@ -574,7 +574,7 @@ const App = (() => { Sidebar.highlight(state.sessionId); setComposerDisabled(false); // 防御性:独立调用 newSession 也确保输入框可用 } catch (e) { - Chat.addSystemMessage(`新建对话失败了:${e.message}`); + Chat.addSystemMessage(_t("app.newFailed", { msg: e.message })); } } @@ -595,7 +595,7 @@ const App = (() => { } await refreshSessions(); } catch (e) { - Chat.addSystemMessage(`删除失败了:${e.message}`); + Chat.addSystemMessage(_t("app.deleteFailed", { msg: e.message })); } } @@ -620,9 +620,9 @@ const App = (() => { }); menu.appendChild(b); }; - mk("✏️ 重命名", false, () => Dialogs.showRename(sid, title)); - mk("🗑 删除对话", true, () => Dialogs.showConfirm(EMRG_Copy.COPY.deleteConfirmTitle, EMRG_Copy.COPY.deleteConfirmBody, { - okText: "删除", + mk(_t("app.rename"), false, () => Dialogs.showRename(sid, title)); + mk(_t("app.deleteConv"), true, () => Dialogs.showConfirm(EMRG_Copy.COPY.deleteConfirmTitle, EMRG_Copy.COPY.deleteConfirmBody, { + okText: _t("dlg.delete"), danger: true, onOk: () => deleteSession(sid), })); @@ -688,7 +688,7 @@ const App = (() => { /** 版本信息(/version 命令 + 进化 toast "去看看" 共用) */ function showVersionInfo() { Chat.addSystemMessage( - `EMRG GUI v${state.version || "0.2.8"} · 实例 ${state.serverId || "未知"} · 模型 ${state.model || "未知"} · 已进化 ${state.evolutionCount ?? 0} 次` + _t("app.versionInfo", { ver: state.version || "0.2.8", id: state.serverId || _t("app.unknown"), model: state.model || _t("app.unknown"), n: state.evolutionCount ?? 0 }) ); } @@ -783,8 +783,8 @@ const App = (() => { const models = s.models?.length ? s.models : s.model ? [s.model] : []; const menu = el("div", { class: "model-menu" }); if (!models.length) { - const empty = el("div", { class: "model-menu-empty" }, "还没有配置模型"); - const go = el("a", {}, "去设置添加"); + const empty = el("div", { class: "model-menu-empty" }, _t("app.noModels")); + const go = el("a", {}, _t("app.goSettings")); go.addEventListener("click", () => { closeModelMenu(); Dialogs.showSettings(); @@ -810,7 +810,7 @@ const App = (() => { sw.classList.add("highlight"); setTimeout(() => sw.classList.remove("highlight"), 1200); } catch (err) { - Chat.addSystemMessage(`切换模型失败了:${err.message}`); + Chat.addSystemMessage(_t("app.modelSwitchFailed", { msg: err.message })); } }); menu.appendChild(item); @@ -844,7 +844,7 @@ const App = (() => { }; document.addEventListener("keydown", _modelMenuKeyHandler); } catch (err) { - Chat.addSystemMessage(`读取模型列表失败了:${err.message}`); + Chat.addSystemMessage(_t("app.modelListFailed", { msg: err.message })); } }); } @@ -879,7 +879,7 @@ const App = (() => { } // Ask 模式提示(仅当切到 ask 时轻提示一次,不打断) if (mode === "ask") { - Chat.addSystemMessage("Ask 模式:我只对话,不执行工具。输入内容问我就好。"); + Chat.addSystemMessage(_t("app.askModeNotice")); } } @@ -972,7 +972,7 @@ const App = (() => { row.classList.remove("running"); row.classList.add("failed"); const label = row.querySelector(".tool-label"); - if (label) label.textContent = "结果未知——连接中断"; + if (label) label.textContent = _t("app.unknownResult"); } } break; @@ -1003,7 +1003,7 @@ const App = (() => { state.ownStreamRequestId = null; setComposerDisabled(false); } else { - Chat.addSystemMessage(`出了点问题:${data.error || "未知错误"}`); + Chat.addSystemMessage(_t("app.error", { msg: data.error || _t("app.unknownError") })); } } @@ -1020,7 +1020,7 @@ const App = (() => { Chat.addSystemMessage(EMRG_Copy.COPY.reconnected); } else if (data.auth_failed) { updateConnectionDot("red"); - Chat.addSystemMessage("认证失败了,请检查设置里的 API Key。"); + Chat.addSystemMessage(_t("app.authFailed")); } else { updateConnectionDot("red"); } diff --git a/emrg/gui/renderer/js/chat.js b/emrg/gui/renderer/js/chat.js index ba76b44..af18b4e 100644 --- a/emrg/gui/renderer/js/chat.js +++ b/emrg/gui/renderer/js/chat.js @@ -20,12 +20,12 @@ const Chat = (() => { if (!pre) return; const code = pre.textContent || ""; const done = () => { - btn.textContent = "已复制 ✓"; - setTimeout(() => { btn.textContent = "复制"; }, 1500); + btn.textContent = EMRG_Copy._t("chat.copied"); + setTimeout(() => { btn.textContent = EMRG_Copy._t("chat.copyCode"); }, 1500); }; const fail = () => { - btn.textContent = "复制失败"; - setTimeout(() => { btn.textContent = "复制"; }, 1500); + btn.textContent = EMRG_Copy._t("chat.copyFailed"); + setTimeout(() => { btn.textContent = EMRG_Copy._t("chat.copyCode"); }, 1500); }; if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(code).then(done, fail); @@ -77,7 +77,7 @@ const Chat = (() => { function createAssistantNode(isOwn) { const node = el("div", { class: "msg assistant" }); if (!isOwn) { - node.appendChild(el("div", { class: "remote-label" }, "(来自其他客户端)")); + node.appendChild(el("div", { class: "remote-label" }, EMRG_Copy._t("chat.fromOtherClient"))); } const mark = el("span", { class: "msg-assistant-mark" }, "✦ "); const body = el("div", { class: "msg-body typing" }); @@ -132,7 +132,7 @@ const Chat = (() => { groupNodes.delete(rid); } if (data.timeout) { - addSystemMessage("⚠️ 响应超时了,我还没说完——可以再试一次。"); + addSystemMessage(EMRG_Copy._t("chat.timeoutWarn")); } } @@ -181,7 +181,7 @@ const Chat = (() => { } const elapsed = data.elapsed !== undefined ? `${data.elapsed.toFixed(1)}s` : ""; if (elapsed && ok) { - row.title = `耗时 ${elapsed}`; + row.title = EMRG_Copy._t("chat.elapsed", { s: elapsed }); } // G91/G131:content 截断 2000 字符 + 展开全文 const content = data.content || ""; @@ -190,7 +190,7 @@ const Chat = (() => { const out = el("div", { class: "tool-output hidden" }, truncated); row.appendChild(out); if (content.length > 2000) { - const btn = el("button", { class: "tool-expand-btn" }, "展开全文"); + const btn = el("button", { class: "tool-expand-btn" }, EMRG_Copy._t("chat.expand")); btn.addEventListener("click", () => { out.textContent = content; btn.remove(); diff --git a/emrg/gui/renderer/js/copywriting.js b/emrg/gui/renderer/js/copywriting.js index 0024c95..d9e33fb 100644 --- a/emrg/gui/renderer/js/copywriting.js +++ b/emrg/gui/renderer/js/copywriting.js @@ -55,6 +55,7 @@ const EMRG_Copy = (() => { toolPhrases, COPY, refresh, + _t, // rant 21:19 Stage 2:chat.js 等模块取词统一走这里(i18n 就绪时) get TOOL_FAIL_TEXT() { return failText; }, }; })(); diff --git a/emrg/gui/renderer/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js index 7a79a3f..f5a040f 100644 --- a/emrg/gui/renderer/js/dialogs.js +++ b/emrg/gui/renderer/js/dialogs.js @@ -69,7 +69,7 @@ const Dialogs = (() => { const def = extraModels.find((m) => m.name === defaultName); list.appendChild( renderModelItem( - defaultName || "未配置", + defaultName || _t("dlg.notConfigured"), true, Boolean(def?.vision), def?.model && def.model !== def.name ? def.model : "" @@ -85,7 +85,7 @@ const Dialogs = (() => { ); } if (extraModels.filter((m) => m.name !== defaultName).length === 0) { - const hint = el("div", { class: "model-list-empty" }, "暂无其他模型,点下方按钮添加"); + const hint = el("div", { class: "model-list-empty" }, _t("dlg.noOtherModels")); list.appendChild(hint); } } @@ -93,7 +93,7 @@ const Dialogs = (() => { function renderModelItem(name, isDefault, vision, modelId) { const row = el("div", { class: "model-item" + (isDefault ? " default" : "") }); // 单选圆点 → 设为默认 - const radio = el("button", { class: "model-radio" + (isDefault ? " checked" : ""), title: isDefault ? "当前默认模型" : "设为默认" }); + const radio = el("button", { class: "model-radio" + (isDefault ? " checked" : ""), title: isDefault ? _t("dlg.currentDefault") : _t("dlg.setDefault") }); if (isDefault) { radio.textContent = "●"; } else { @@ -105,20 +105,20 @@ const Dialogs = (() => { } row.appendChild(radio); const title = el("span", { class: "model-name" }, name); - if (isDefault) title.appendChild(el("span", { class: "model-badge" }, "默认")); + if (isDefault) title.appendChild(el("span", { class: "model-badge" }, _t("dlg.defaultBadge"))); row.appendChild(title); if (modelId) row.appendChild(el("span", { class: "model-id" }, modelId)); - if (vision) row.appendChild(el("span", { class: "model-vision" }, "🖼 支持图片")); + if (vision) row.appendChild(el("span", { class: "model-vision" }, _t("dlg.supportsImages"))); // 操作:编辑(非默认行 → 打开表单填本条);删除(默认不可删) const actions = el("span", { class: "model-actions" }); - const editBtn = el("button", { class: "model-action-btn", title: "编辑" }, "编辑"); + const editBtn = el("button", { class: "model-action-btn", title: _t("dlg.edit") }, _t("dlg.edit")); editBtn.addEventListener("click", () => openModelForm(name, isDefault)); actions.appendChild(editBtn); if (!isDefault) { - const delBtn = el("button", { class: "model-action-btn danger", title: "删除" }, "删除"); + const delBtn = el("button", { class: "model-action-btn danger", title: _t("dlg.delete") }, _t("dlg.delete")); delBtn.addEventListener("click", () => { - showConfirm("删除这个模型?", `「${name}」将从可用模型里移除。`, { - okText: "删除", + showConfirm(_t("dlg.deleteModelTitle"), _t("dlg.deleteModelBody", { name }), { + okText: _t("dlg.delete"), danger: true, onOk: () => { extraModels = extraModels.filter((m) => m.name !== name); @@ -156,7 +156,7 @@ const Dialogs = (() => { function saveModelForm() { const name = $("model-form-name").value.trim(); if (!name) { - showConfirm("还差一步", "模型名称是必填的哦。", { okText: "知道了", danger: false }); + showConfirm(_t("dlg.stepTitle"), _t("dlg.nameRequiredBody"), { okText: _t("dlg.gotIt"), danger: false }); return; } const modelId = $("model-form-id").value.trim() || name; @@ -165,7 +165,7 @@ const Dialogs = (() => { if (editingName === null) { // 添加新模型 if (name === defaultName || extraModels.some((m) => m.name === name)) { - showConfirm("重名了", "这个名称已经有模型了,换个名字吧。", { okText: "知道了", danger: false }); + showConfirm(_t("dlg.duplicateTitle"), _t("dlg.duplicateBody"), { okText: _t("dlg.gotIt"), danger: false }); return; } extraModels.push({ name, model: modelId, vision }); @@ -178,11 +178,11 @@ const Dialogs = (() => { } else { // 编辑非默认模型 if (name === defaultName) { - showConfirm("重名了", "这个名称是默认模型,请在默认行编辑。", { okText: "知道了", danger: false }); + showConfirm(_t("dlg.duplicateTitle"), _t("dlg.duplicateDefaultBody"), { okText: _t("dlg.gotIt"), danger: false }); return; } if (name !== editingName && extraModels.some((m) => m.name === name)) { - showConfirm("重名了", "这个名称已经有模型了,换个名字吧。", { okText: "知道了", danger: false }); + showConfirm(_t("dlg.duplicateTitle"), _t("dlg.duplicateBody"), { okText: _t("dlg.gotIt"), danger: false }); return; } extraModels = extraModels.map((m) => (m.name === editingName ? { name, model: modelId, vision } : m)); @@ -228,10 +228,10 @@ const Dialogs = (() => { const ver = $("about-version"); if (ver) ver.textContent = `v${App.state.version || "0.2.8"}`; const evo = $("about-evolutions"); - if (evo) evo.textContent = EMRG_Copy.COPY.growthCount(App.state.evolutionCount ?? 0); + if (evo) evo.textContent = `🌱 ${_t("copy.growthCountPrefix")} ${App.state.evolutionCount ?? 0} ${_t("copy.times")}`; } catch { /* 元素缺失(测试桩)时忽略 */ } } catch (e) { - Chat.addSystemMessage(`读取设置失败了:${e.message}`); + Chat.addSystemMessage(_t("settings.readFailed", { msg: e.message })); } $("settings-dialog").showModal(); } @@ -246,8 +246,8 @@ const Dialogs = (() => { models: extraModels, }; if (!config.apiKey) { - showConfirm("还差一步", "API Key 是连接模型服务必需的,填上才能继续哦。", { - okText: "知道了", + showConfirm(_t("dlg.stepTitle"), _t("app.authKeyRequired"), { + okText: _t("dlg.gotIt"), danger: false, }); return; @@ -255,14 +255,14 @@ const Dialogs = (() => { try { await window.emrg.saveSettings(config); if ($("settings-dialog").open) $("settings-dialog").close(); - Chat.addSystemMessage("设置已保存。"); + Chat.addSystemMessage(_t("dlg.saved")); App.state.apiKeyConfigured = true; App.state.projectDir = config.projectDir || App.state.projectDir; App.state.model = defaultName; App.updateModelSwitcher(); await App.boot(); } catch (e) { - Chat.addSystemMessage(`保存失败了:${e.message}`); + Chat.addSystemMessage(_t("settings.saveFailed", { msg: e.message })); } } @@ -271,7 +271,7 @@ const Dialogs = (() => { $("welcome-project-dir").value = ""; $("welcome-api-key").value = ""; $("welcome-base-url").value = ""; - $("welcome-model").innerHTML = ""; + $("welcome-model").innerHTML = ``; $("welcome-dialog").showModal(); window.emrg.getSettings().then((s) => { const sel = $("welcome-model"); @@ -293,8 +293,8 @@ const Dialogs = (() => { async function saveWelcome() { const key = $("welcome-api-key").value.trim(); if (!key) { - showConfirm("还差一步", "API Key 是连接模型服务必需的,填上才能继续哦。", { - okText: "知道了", + showConfirm(_t("dlg.stepTitle"), _t("app.authKeyRequired"), { + okText: _t("dlg.gotIt"), danger: false, }); return; @@ -308,12 +308,12 @@ const Dialogs = (() => { try { await window.emrg.saveSettings(config); if ($("welcome-dialog").open) $("welcome-dialog").close(); - Chat.addSystemMessage("设置完成,正在启动…"); + Chat.addSystemMessage(_t("dlg.starting")); App.state.apiKeyConfigured = true; App.state.projectDir = config.projectDir || App.state.projectDir; await App.boot(); } catch (e) { - Chat.addSystemMessage(`初始化失败了:${e.message}`); + Chat.addSystemMessage(_t("dlg.initFailed", { msg: e.message })); } } @@ -339,7 +339,7 @@ const Dialogs = (() => { $("rename-dialog").close(); await App.refreshSessions(); } catch (e) { - Chat.addSystemMessage(`重命名失败了:${e.message}`); + Chat.addSystemMessage(_t("dlg.renameFailed", { msg: e.message })); } } @@ -363,7 +363,7 @@ const Dialogs = (() => { $("confirm-title").textContent = title; $("confirm-message").textContent = message; const ok = $("confirm-ok"); - ok.textContent = opts.okText || "删除"; + ok.textContent = opts.okText || _t("dlg.delete"); ok.className = opts.danger === false ? "btn btn-primary" : "btn btn-danger"; confirmCb = opts.onOk || null; $("confirm-dialog").showModal(); diff --git a/emrg/gui/renderer/js/i18n.js b/emrg/gui/renderer/js/i18n.js index a1cedb7..aa82ba2 100644 --- a/emrg/gui/renderer/js/i18n.js +++ b/emrg/gui/renderer/js/i18n.js @@ -148,6 +148,8 @@ const I18N = (() => { "copy.aboutEvolutionEmpty": "EMRG 正在成长中", // WorkBuddy P3(#501):成长卡 + 进化 toast "copy.growthCount": "已自我进化 {n} 次", + "copy.growthCountPrefix": "已自我进化", + "copy.times": "次", "copy.growthNote": "边工作边学习,越用越懂你", "copy.evolutionToastTitle": "EMRG 刚刚完成一次自我进化!", "copy.evolutionToastMsg": "这是它的第 {n} 次自我改进,现在更好用了。", @@ -170,6 +172,99 @@ const I18N = (() => { "tool.fallback.doing": "正在处理…", "tool.fallback.done": "已完成", "tool.failText": "这一步没成功,我换个方法试试", + + // 聊天区(chat.js) + "chat.copyCode": "复制", + "chat.copied": "已复制 ✓", + "chat.copyFailed": "复制失败", + "chat.fromOtherClient": "(来自其他客户端)", + "chat.timeoutWarn": "⚠️ 响应超时了,我还没说完——可以再试一次。", + "chat.elapsed": "耗时 {s}", + "chat.expand": "展开全文", + // 时间分组(utils.js/sidebar.js) + "util.groupToday": "今天", + "util.groupYesterday": "昨天", + "util.groupEarlier": "更早", + // 结果面板(result-panel.js) + "panel.openFailed": "无法打开文件", + "panel.copied": "✓ 已复制", + // Markdown 代码块(markdown.js) + "md.copyCode": "复制代码", + // 对话框动态文案(dialogs.js) + "dlg.notConfigured": "未配置", + "dlg.noOtherModels": "暂无其他模型,点下方按钮添加", + "dlg.currentDefault": "当前默认模型", + "dlg.setDefault": "设为默认", + "dlg.defaultBadge": "默认", + "dlg.supportsImages": "🖼 支持图片", + "dlg.edit": "编辑", + "dlg.delete": "删除", + "dlg.deleteModelTitle": "删除这个模型?", + "dlg.deleteModelBody": "「{name}」将从可用模型里移除。", + "dlg.stepTitle": "还差一步", + "dlg.nameRequiredBody": "模型名称是必填的哦。", + "dlg.duplicateTitle": "重名了", + "dlg.duplicateBody": "这个名称已经有模型了,换个名字吧。", + "dlg.duplicateDefaultBody": "这个名称是默认模型,请在默认行编辑。", + "dlg.gotIt": "知道了", + "app.authKeyRequired": "API Key 是连接模型服务必需的,填上才能继续哦。", + "dlg.saved": "设置已保存。", + "dlg.loading": "加载中…", + "dlg.starting": "设置完成,正在启动…", + "dlg.initFailed": "初始化失败了:{msg}", + "dlg.renameFailed": "重命名失败了:{msg}", + // 应用动态文案(app.js) + "app.workdirInvalid": "工作目录不可用,请到设置里改一下。", + "app.bootFailed": "启动遇到了问题:{msg}", + "app.needSession": "请先创建一个对话。", + "app.cmdUnknown": "指令 {cmd} 暂未开放。", + "app.cleared": "已清空当前对话。", + "app.compacted": "已压缩当前对话历史。", + "app.imagePaste": "请直接粘贴图片到输入框(Ctrl+V / ⌘V)。", + "app.cmdFailed": "指令 {cmd} 执行失败:{msg}", + "app.helpNoSessions": "还没有对话,输入内容即可开始。", + "app.unnamed": "(未命名)", + "app.current": "当前", + "app.noHistory": "没有可回退的历史消息。", + "app.historyFailed": "加载历史失败:{msg}", + "app.rewound": "已回退到消息点 #{index},移除了 {n} 条记录。", + "app.rewindFailed": "回退失败:{msg}", + "app.noMemories": "还没有{scope}记忆。", + "app.sessionMem": "会话", + "app.projectMem": "项目", + "app.readMemFailed": "读取记忆失败:{msg}", + "app.memFailed": "加载记忆失败:{msg}", + "app.noSkills": "还没有加载技能。", + "app.skillsFailed": "加载技能失败:{msg}", + "app.globalAll": "(全局 — 所有项目)", + "app.rantEmpty": "写点内容再提交吧。", + "app.rantReceived": "✓ 收到!EMRG 会据此进化。", + "app.rantCount": "(已累计 {n} 条反馈)", + "app.rantFailed": "提交失败了:{msg}", + "app.noTasks": "没有可触发的任务。", + "app.taskDisabled": "已停用", + "app.taskInterval": "间隔 {n}s", + "app.tasksFailed": "加载任务失败:{msg}", + "app.triggerFailed": "触发失败:{msg}", + "app.triggered": "已触发任务 {n}。", + "app.deletedSwitch": "这个对话已被删除,已帮你切到最近的对话。", + "app.switched": "已切换对话。", + "app.switchFailed": "切换对话失败了:{msg}", + "app.newFailed": "新建对话失败了:{msg}", + "app.deleteFailed": "删除失败了:{msg}", + "app.rename": "✏️ 重命名", + "app.deleteConv": "🗑 删除对话", + "app.noModels": "还没有配置模型", + "app.goSettings": "去设置添加", + "app.modelSwitchFailed": "切换模型失败了:{msg}", + "app.modelListFailed": "读取模型列表失败了:{msg}", + "app.askModeNotice": "Ask 模式:我只对话,不执行工具。输入内容问我就好。", + "app.unknownResult": "结果未知——连接中断", + "app.error": "出了点问题:{msg}", + "app.unknownError": "未知错误", + "app.unknown": "未知", + "app.authFailed": "认证失败了,请检查设置里的 API Key。", + "app.versionInfo": "EMRG GUI v{ver} · 实例 {id} · 模型 {model} · 已进化 {n} 次", }, en: { @@ -305,6 +400,8 @@ const I18N = (() => { "copy.aboutEvolutionEmpty": "EMRG is still growing", // WorkBuddy P3 (#501): growth card + evolution toast "copy.growthCount": "Self-evolved {n} times", + "copy.growthCountPrefix": "Self-evolved", + "copy.times": "times", "copy.growthNote": "Learning as I work — getting better with use", "copy.evolutionToastTitle": "EMRG just completed a self-evolution!", "copy.evolutionToastMsg": "That's its {n}th self-improvement — it's even better now.", @@ -327,6 +424,99 @@ const I18N = (() => { "tool.fallback.doing": "Working on it…", "tool.fallback.done": "Done", "tool.failText": "That step didn't work — let me try another approach", + + // Chat area (chat.js) + "chat.copyCode": "Copy", + "chat.copied": "Copied ✓", + "chat.copyFailed": "Copy failed", + "chat.fromOtherClient": "(from another client)", + "chat.timeoutWarn": "⚠️ Response timed out — I wasn't done. Try again?", + "chat.elapsed": "took {s}", + "chat.expand": "Expand full text", + // Time groups (utils.js/sidebar.js) + "util.groupToday": "Today", + "util.groupYesterday": "Yesterday", + "util.groupEarlier": "Earlier", + // Result panel (result-panel.js) + "panel.openFailed": "Could not open file", + "panel.copied": "✓ Copied", + // Markdown code block (markdown.js) + "md.copyCode": "Copy code", + // Dialog dynamic copy (dialogs.js) + "dlg.notConfigured": "Not configured", + "dlg.noOtherModels": "No other models — add one below", + "dlg.currentDefault": "Current default model", + "dlg.setDefault": "Set as default", + "dlg.defaultBadge": "Default", + "dlg.supportsImages": "🖼 Supports images", + "dlg.edit": "Edit", + "dlg.delete": "Delete", + "dlg.deleteModelTitle": "Delete this model?", + "dlg.deleteModelBody": "\"{name}\" will be removed from available models.", + "dlg.stepTitle": "One more step", + "dlg.nameRequiredBody": "A model name is required.", + "dlg.duplicateTitle": "Name taken", + "dlg.duplicateBody": "A model with this name already exists — try another.", + "dlg.duplicateDefaultBody": "This is the default model — edit it in the default row.", + "dlg.gotIt": "Got it", + "app.authKeyRequired": "An API Key is required to connect to the model service — fill it in to continue.", + "dlg.saved": "Settings saved.", + "dlg.loading": "Loading…", + "dlg.starting": "Settings saved — starting…", + "dlg.initFailed": "Initialization failed: {msg}", + "dlg.renameFailed": "Rename failed: {msg}", + // App dynamic copy (app.js) + "app.workdirInvalid": "Working directory unavailable — update it in Settings.", + "app.bootFailed": "Startup failed: {msg}", + "app.needSession": "Start a conversation first.", + "app.cmdUnknown": "Command {cmd} is not available yet.", + "app.cleared": "Current conversation cleared.", + "app.compacted": "Conversation history compacted.", + "app.imagePaste": "Paste the image into the input (Ctrl+V / ⌘V).", + "app.cmdFailed": "Command {cmd} failed: {msg}", + "app.helpNoSessions": "No conversations yet — start typing.", + "app.unnamed": "(untitled)", + "app.current": "current", + "app.noHistory": "No history points to rewind to.", + "app.historyFailed": "Failed to load history: {msg}", + "app.rewound": "Rewound to message point #{index}, removed {n} records.", + "app.rewindFailed": "Rewind failed: {msg}", + "app.noMemories": "No {scope} memories yet.", + "app.sessionMem": "conversation", + "app.projectMem": "project", + "app.readMemFailed": "Failed to read memory: {msg}", + "app.memFailed": "Failed to load memories: {msg}", + "app.noSkills": "No skills loaded yet.", + "app.skillsFailed": "Failed to load skills: {msg}", + "app.globalAll": "(Global — all projects)", + "app.rantEmpty": "Write something first.", + "app.rantReceived": "✓ Got it! EMRG will evolve accordingly.", + "app.rantCount": "({n} feedback items so far)", + "app.rantFailed": "Submission failed: {msg}", + "app.noTasks": "No tasks to trigger.", + "app.taskDisabled": "Disabled", + "app.taskInterval": "every {n}s", + "app.tasksFailed": "Failed to load tasks: {msg}", + "app.triggerFailed": "Trigger failed: {msg}", + "app.triggered": "Task {n} triggered.", + "app.deletedSwitch": "This conversation was deleted — switched to the most recent one.", + "app.switched": "Conversation switched.", + "app.switchFailed": "Failed to switch: {msg}", + "app.newFailed": "Failed to create conversation: {msg}", + "app.deleteFailed": "Delete failed: {msg}", + "app.rename": "✏️ Rename", + "app.deleteConv": "🗑 Delete conversation", + "app.noModels": "No models configured", + "app.goSettings": "Add in Settings", + "app.modelSwitchFailed": "Failed to switch model: {msg}", + "app.modelListFailed": "Failed to load models: {msg}", + "app.askModeNotice": "Ask mode: I only chat — no tools. Just ask.", + "app.unknownResult": "Result unknown — connection lost", + "app.error": "Something went wrong: {msg}", + "app.unknownError": "Unknown error", + "app.unknown": "unknown", + "app.authFailed": "Authentication failed — check your API Key in Settings.", + "app.versionInfo": "EMRG GUI v{ver} · Instance {id} · Model {model} · Evolved {n} times", }, }; diff --git a/emrg/gui/renderer/js/markdown.js b/emrg/gui/renderer/js/markdown.js index d02db31..e1faf02 100644 --- a/emrg/gui/renderer/js/markdown.js +++ b/emrg/gui/renderer/js/markdown.js @@ -31,7 +31,7 @@ function codeRenderer(code, infostring, escaped) { ? `${highlighted}` : `${escaped ? code : escapeHtml(code)}`; // 复制按钮:事件委托在 chat-view(CSP 禁内联 handler);code 文本经 escapeHtml 防注入 - return `
${codeHtml}
`; + return `
${codeHtml}
`; } renderer.renderMarkdown = async function (mdText) { diff --git a/emrg/gui/renderer/js/result-panel.js b/emrg/gui/renderer/js/result-panel.js index f30cab5..b27b1b1 100644 --- a/emrg/gui/renderer/js/result-panel.js +++ b/emrg/gui/renderer/js/result-panel.js @@ -41,7 +41,7 @@ const ResultPanel = (() => { const list = listEl(); if (!list) return; if (list.children.length === 0) { - list.innerHTML = `
还没有产物,对话中生成的文件会出现在这里
`; + list.innerHTML = `
${window.EMRG_I18N ? window.EMRG_I18N.t("result.empty") : "还没有产物,对话中生成的文件会出现在这里"}
`; } } @@ -81,7 +81,7 @@ const ResultPanel = (() => { const res = await window.emrg.openFile({ filePath }); if (res && res.ok === false) { // 打开失败提示(文件可能已删除) - fileRow.title = res.error || "无法打开文件"; + fileRow.title = res.error || (window.EMRG_I18N ? window.EMRG_I18N.t("panel.openFailed") : "无法打开文件"); } }); card.appendChild(fileRow); @@ -93,13 +93,13 @@ const ResultPanel = (() => { const body = el("div", { class: "result-body hidden" }, preview); card.appendChild(body); // 复制按钮 - const copyBtn = el("button", { class: "result-copy", type: "button" }, "复制"); + const copyBtn = el("button", { class: "result-copy", type: "button" }, _t("chat.copyCode")); copyBtn.addEventListener("click", (e) => { e.stopPropagation(); try { navigator.clipboard.writeText(content).then(() => { - copyBtn.textContent = "✓ 已复制"; - setTimeout(() => { copyBtn.textContent = "复制"; }, 1500); + copyBtn.textContent = _t("panel.copied"); + setTimeout(() => { copyBtn.textContent = _t("chat.copyCode"); }, 1500); }); } catch { /* clipboard unavailable */ } }); diff --git a/emrg/gui/renderer/js/sidebar.js b/emrg/gui/renderer/js/sidebar.js index aac2d21..0591179 100644 --- a/emrg/gui/renderer/js/sidebar.js +++ b/emrg/gui/renderer/js/sidebar.js @@ -16,7 +16,11 @@ const Sidebar = (() => { nav.appendChild(el("div", { class: "conv-item placeholder" }, EMRG_Copy.COPY.noSessions)); return; } - const groups = { 今天: [], 昨天: [], 更早: [] }; + // rant 21:19:分组标签本地化(顺序保持 今天→昨天→更早 不变) + const groups = {}; + for (const lbl of [_t("util.groupToday"), _t("util.groupYesterday"), _t("util.groupEarlier")]) { + groups[lbl] = []; + } for (const s of sessions) { const g = groupLabel(s.updated_at || s.created_at); if (!groups[g]) groups[g] = []; diff --git a/emrg/gui/renderer/js/utils.js b/emrg/gui/renderer/js/utils.js index ff5ce15..42f93e7 100644 --- a/emrg/gui/renderer/js/utils.js +++ b/emrg/gui/renderer/js/utils.js @@ -44,15 +44,23 @@ function genRequestId() { /** 会话时间分组:今天 / 昨天 / 更早 */ function groupLabel(ts) { - if (!ts) return "更早"; + if (!ts) return _t("util.groupEarlier"); const d = new Date(ts); - if (Number.isNaN(d.getTime())) return "更早"; + if (Number.isNaN(d.getTime())) return _t("util.groupEarlier"); const now = new Date(); const startOfDay = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); const dayDiff = Math.round((startOfDay(now) - startOfDay(d)) / 86400000); - if (dayDiff <= 0) return "今天"; - if (dayDiff === 1) return "昨天"; - return "更早"; + if (dayDiff <= 0) return _t("util.groupToday"); + if (dayDiff === 1) return _t("util.groupYesterday"); + return _t("util.groupEarlier"); +} + +/** rant 21:19:i18n 取词(i18n.js 缺失时回退 key 本身) */ +function _t(key, params) { + try { + if (window.EMRG_I18N) return window.EMRG_I18N.t(key, params); + } catch { /* ignore */ } + return key; } /** 主题应用:system 时移除 data-theme(CSS 跟随 prefers-color-scheme) */ diff --git a/emrg/gui/test/app-commands.test.js b/emrg/gui/test/app-commands.test.js index 832cc0d..1a14c18 100644 --- a/emrg/gui/test/app-commands.test.js +++ b/emrg/gui/test/app-commands.test.js @@ -86,6 +86,7 @@ function makeSandbox(overrides = {}) { Set, Promise, requestAnimationFrame: (cb) => cb(), + navigator: { language: "zh-CN" }, // rant 21:19:沙箱固定 zh,断言保持确定性 requestIdleCallback: (cb) => cb(), crypto: { randomUUID: () => "mock-uuid" }, localStorage: { getItem: () => null, setItem() {} }, diff --git a/emrg/gui/test/i18n.test.js b/emrg/gui/test/i18n.test.js index cb5f864..3a00a0d 100644 --- a/emrg/gui/test/i18n.test.js +++ b/emrg/gui/test/i18n.test.js @@ -127,3 +127,37 @@ test("词典完整性:zh 与 en 键集合一致", () => { const n = zhKeys.split(",").length; assert.ok(n > 60, `词典应覆盖全部 Stage 1 文案(实际 ${n} 键)`); }); + +// ── Stage 2(rant 21:19):动态文案本地化 ──────────────── +test("Stage2:动态文案键(app/chat/dlg/panel)双语齐全", () => { + const { ctx } = makeSandbox({ navigator: { language: "en-US" } }); + // 抽查关键动态文案 + assert.strictEqual(evalIn(ctx, 'I18N.t("app.needSession")'), "Start a conversation first."); + assert.strictEqual(evalIn(ctx, 'I18N.t("app.versionInfo", { ver: "0.2.8", id: "srv", model: "m", n: 42 })'), + "EMRG GUI v0.2.8 · Instance srv · Model m · Evolved 42 times"); + assert.strictEqual(evalIn(ctx, 'I18N.t("chat.elapsed", { s: "1.2s" })'), "took 1.2s"); + assert.strictEqual(evalIn(ctx, 'I18N.t("dlg.deleteModelBody", { name: "gpt-4o" })'), + '"gpt-4o" will be removed from available models.'); + assert.strictEqual(evalIn(ctx, 'I18N.t("app.rewound", { index: 3, n: 5 })'), + "Rewound to message point #3, removed 5 records."); + const { ctx: zh } = makeSandbox({ navigator: { language: "zh-CN" } }); + assert.strictEqual(evalIn(zh, 'I18N.t("app.needSession")'), "请先创建一个对话。"); + assert.strictEqual(evalIn(zh, 'I18N.t("dlg.deleteModelBody", { name: "gpt-4o" })'), "「gpt-4o」将从可用模型里移除。"); +}); + +test("Stage2:时间分组标签本地化(util.group*)", () => { + const { ctx } = makeSandbox({ navigator: { language: "en-US" } }); + assert.strictEqual(evalIn(ctx, 'I18N.t("util.groupToday")'), "Today"); + assert.strictEqual(evalIn(ctx, 'I18N.t("util.groupYesterday")'), "Yesterday"); + assert.strictEqual(evalIn(ctx, 'I18N.t("util.groupEarlier")'), "Earlier"); + const { ctx: zh } = makeSandbox({ navigator: { language: "zh-CN" } }); + assert.strictEqual(evalIn(zh, 'I18N.t("util.groupToday")'), "今天"); +}); + +test("Stage2:成长卡/关于区静态文案键(#501 吸收)", () => { + const { ctx } = makeSandbox({ navigator: { language: "en-US" } }); + assert.strictEqual(evalIn(ctx, 'I18N.t("copy.growthCountPrefix")'), "Self-evolved"); + assert.strictEqual(evalIn(ctx, 'I18N.t("copy.times")'), "times"); + const { ctx: zh } = makeSandbox({ navigator: { language: "zh-CN" } }); + assert.strictEqual(evalIn(zh, 'I18N.t("copy.growthCount", { n: 7 })'), "已自我进化 7 次"); +}); diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index c540b54..7eee738 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -111,6 +111,7 @@ function makeSandbox(overrides = {}) { Set, Promise, requestAnimationFrame: (cb) => cb(), + navigator: { language: "zh-CN" }, // rant 21:19:沙箱固定 zh,断言保持确定性 requestIdleCallback: (cb) => cb(), crypto: { randomUUID: () => "mock-uuid" }, DOMPurify: { sanitize: (x) => x },