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 ``;
+ 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 },