diff --git a/desktop/src/App.test.tsx b/desktop/src/App.test.tsx index 65e54581..c6a61c57 100644 --- a/desktop/src/App.test.tsx +++ b/desktop/src/App.test.tsx @@ -30,6 +30,7 @@ import type { import { App } from "./App"; import { __resetComposerBehaviorForTests } from "./app/composerBehavior"; import { __resetEscapeLayersForTests } from "./app/escapeLayer"; +import { __setLocaleForTests } from "./app/i18n"; import type { AnyRpcNotification, DesktopRuntime, @@ -1505,6 +1506,7 @@ describe("desktop command center", () => { localStorage.clear(); __resetComposerBehaviorForTests(); __resetEscapeLayersForTests(); + __setLocaleForTests("en"); }); afterEach(() => { cleanup(); diff --git a/desktop/src/app/i18n.ts b/desktop/src/app/i18n.ts index fa9519b9..fbabf080 100644 --- a/desktop/src/app/i18n.ts +++ b/desktop/src/app/i18n.ts @@ -59,6 +59,29 @@ const ZH_CN: Record = { "settings.composer.queue": "排队为下一个 Turn", "settings.composer.note": "仅在忙碌时生效;Cmd/Ctrl+Enter 执行另一种行为。空闲时 Enter 始终发送。本设置立即生效,仅保存在本机。", + // Updates card + "settings.updates.eyebrow": "签名发布通道", + "settings.updates.title": "应用更新", + "settings.updates.check": "检查更新", + "settings.updates.checking": "正在检查…", + "settings.updates.install": "安装", + "settings.updates.preparing": "准备中…", + "settings.updates.downloading": "下载中…", + "settings.updates.installing": "安装中…", + "settings.updates.upToDate": "当前已是最新版本。", + "settings.updates.available": + "DeepCode {{version}} 可用。安装前会验证软件包签名。", + "settings.updates.idle": + "仅在手动请求时检查更新。开发版本可能未配置发布通道。", + "settings.updates.checkingNote": "正在检查签名发布通道。", + // Diagnostics card + "settings.diagnostics.eyebrow": "故障排除", + "settings.diagnostics.title": "诊断", + "settings.diagnostics.export": "导出报告", + "settings.diagnostics.exporting": "导出中…", + "settings.diagnostics.runChecks": "运行检查", + "settings.diagnostics.savedTo": "已保存脱敏诊断报告至 {{path}}", + "settings.diagnostics.noProject": "未选择项目", // Sidebar "sidebar.threads": "会话", "sidebar.automations": "自动化", @@ -67,12 +90,106 @@ const ZH_CN: Record = { "sidebar.mcp": "MCP", "sidebar.settings": "设置", "sidebar.projects": "项目", + "sidebar.newThread": "新建会话", + "sidebar.searchSessions": "搜索会话", + "sidebar.openFolder": "打开本地文件夹", + "sidebar.openFolderHint": "CLI 中的会话将在此显示。", + "sidebar.noResults": "无匹配会话", + "sidebar.noResultsHint": "可按标题、项目或工作空间路径搜索。", + "sidebar.noSessions": "暂无会话", + "sidebar.showMore": "显示更多 {{count}} 个", + "sidebar.showLess": "收起", + "sidebar.previousSessions": "历史会话", + "sidebar.folderUnavailable": "原始文件夹不可用", + "sidebar.localAgent": "本地智能体", + "sidebar.agentReady": "本地智能体就绪", + "sidebar.sharedHistory": "共享会话历史", // Composer high-traffic strings "composer.hint.send": "↵ 发送", "composer.hint.steerQueue": "↵ 引导 · ⌘↵ 排队", "composer.hint.queueSteer": "↵ 排队 · ⌘↵ 引导", "composer.hint.newline": "⇧↵ 换行", "composer.queueNext": "排队下一个", + // Thread header + "thread.startThread": "开始本地编码会话", + "thread.folderUnavailable": "文件夹不可用", + "thread.noLocalFolder": "无本地文件夹", + "thread.previousSessions": "历史会话", + "thread.trustFolder": "信任文件夹", + "thread.trusted": "已信任", + "thread.trustedTooltip": "允许在此文件夹中执行", + "thread.folderUnavailableTooltip": "原始会话文件夹不再可用", + "thread.paper": "论文", + "thread.paperTooltip": "创建 Paper2Code 会话", + "thread.fork": "分叉", + "thread.forkTooltip": "分叉到独立工作树", + "thread.review": "审查", + "thread.closeReview": "关闭审查面板", + "thread.openReview": "打开审查面板", + // Approval card + "approval.label": "需要审批", + "approval.decision": "决定: {{status}}", + "approval.allowContinue": "允许 {{tool}} 继续?", + "approval.reviewOperation": "在智能体继续之前,请审查此操作。", + "approval.sensitiveOperation": "敏感操作", + "approval.reviewArguments": "查看参数", + "approval.allowOnce": "允许一次", + "approval.allowSession": "允许本次会话", + "approval.deny": "拒绝", + // Runtime notice + "runtime.offline": "本地应用服务器不可用。", + "runtime.restart": "重启服务", + // Goal rail + "goal.setGoal": "设定目标", + "goal.setGoalHint": "在普通 Turn 之间保持一个持久的目标", + "goal.sessionGoal": "会话目标", + "goal.turn": "Turn", + "goal.turns": "Turns", + "goal.tokens": "tokens", + "goal.continue": "继续", + "goal.editGoal": "编辑目标", + "goal.pause": "暂停", + "goal.pauseTooltip": "暂停自动继续;不会中断当前 Turn", + "goal.editReopen": "编辑并重新开启", + "goal.resume": "恢复", + "goal.edit": "编辑", + "goal.newGoal": "新建目标", + "goal.objectiveHint": + "目标编辑保持同一 Goal 身份,并到达活跃的 Turn。", + "goal.outcome": "目标结果", + "goal.tokenBudget": "Token 预算", + "goal.tokenBudgetOptional": "可选", + "goal.noLimit": "无限制", + "goal.skills": "技能", + "goal.cancel": "取消", + "goal.saveResume": "保存并恢复", + "goal.saveGoal": "保存目标", + "goal.startGoal": "开始目标", + "goal.clearGoal": "清除目标", + "goal.clearConfirm": "确定清除此会话目标?", + "goal.completionOutcome": "完成结果", + "goal.blockedOutcome": "阻塞结果", + "goal.relatedActivity": "相关活动", + "goal.noSkills": "未选择目标相关技能。", + "goal.activeTime": "{{duration}} 活跃时间", + "goal.noUsage": "尚无已完成的 Goal Turn 用量", + "goal.tokenBudgetLabel": "{{budget}} token 预算", + "goal.noTokenBudget": "无 token 预算", + "goal.decidingTurn": "决定 Turn", + "goal.goalDescription": + "目标 {{id}} 附属于此会话。普通后续对话引导工作方向,而不重写目标。", + "goal.closeEditor": "关闭目标编辑器", + "goal.describeOutcome": "描述 DeepCode 应达成的完整目标。", + // Inspector + "inspector.label": "检查器", + "inspector.views": "检查器视图", + "inspector.closeReview": "关闭审查面板", + "inspector.tab.changes": "变更", + "inspector.tab.files": "文件", + "inspector.tab.artifacts": "产物", + "inspector.tab.tests": "测试", + "inspector.tab.terminal": "终端", + "inspector.tab.details": "详情", }; function readLocale(): Locale { diff --git a/desktop/src/components/RuntimeNotice.tsx b/desktop/src/components/RuntimeNotice.tsx index 00befd16..d9578a19 100644 --- a/desktop/src/components/RuntimeNotice.tsx +++ b/desktop/src/components/RuntimeNotice.tsx @@ -1,4 +1,5 @@ import { AlertTriangle, RotateCw, X } from "lucide-react"; +import { useTranslation } from "react-i18next"; import type { BridgeError, SidecarStatus } from "../rpc/contracts"; import styles from "./RuntimeNotice.module.css"; @@ -18,9 +19,10 @@ export function RuntimeNotice({ onRestart, onDismissError, }: RuntimeNoticeProps) { + const { t } = useTranslation(); if (!error && runtime.phase !== "crashed" && runtime.phase !== "stopped") return null; const serviceOffline = runtime.phase === "crashed" || runtime.phase === "stopped"; - const message = error?.message ?? runtime.message ?? "The local App Server is unavailable."; + const message = error?.message ?? runtime.message ?? t("runtime.offline", "The local App Server is unavailable."); return (
diff --git a/desktop/src/features/execution/ApprovalCard.tsx b/desktop/src/features/execution/ApprovalCard.tsx index 5db21f6d..916dfc47 100644 --- a/desktop/src/features/execution/ApprovalCard.tsx +++ b/desktop/src/features/execution/ApprovalCard.tsx @@ -5,6 +5,7 @@ import { TerminalSquare, Wrench, } from "lucide-react"; +import { useTranslation } from "react-i18next"; import type { Approval, ApprovalDecision } from "../../generated/app-server"; import styles from "./ApprovalCard.module.css"; @@ -15,17 +16,17 @@ interface ApprovalCardProps { onRespond(approvalId: string, decision: ApprovalDecision): void; } -function requestDetail(approval: Approval): string { +function requestDetail(approval: Approval, t: (key: string, defaultValue: string, options?: Record) => string): string { const tool = approval.request.toolName; const reason = approval.request.reason; if (typeof reason === "string" && reason) return reason; - if (typeof tool === "string" && tool) return `Allow ${tool} to continue?`; - return "Review this operation before the agent continues."; + if (typeof tool === "string" && tool) return t("approval.allowContinue", "Allow {{tool}} to continue?", { tool }); + return t("approval.reviewOperation", "Review this operation before the agent continues."); } -function requestTool(approval: Approval): string { +function requestTool(approval: Approval, t: (key: string, defaultValue: string) => string): string { const tool = approval.request.toolName; - return typeof tool === "string" && tool.trim() ? tool : "Sensitive operation"; + return typeof tool === "string" && tool.trim() ? tool : t("approval.sensitiveOperation", "Sensitive operation"); } function requestArguments(approval: Approval): string | null { @@ -61,27 +62,28 @@ function CategoryIcon({ category }: { category: Approval["category"] }) { } export function ApprovalCard({ approval, busy, onRespond }: ApprovalCardProps) { + const { t } = useTranslation(); if (approval.status !== "pending") { return (

- Decision: {approval.status.replaceAll("_", " ")} + {t("approval.decision", "Decision: {{status}}", { status: approval.status.replaceAll("_", " ") })}

); } const argumentsPreview = requestArguments(approval); return ( -
+
- {requestTool(approval)} + {requestTool(approval, t)} {approval.category.replaceAll("_", " ")}
-

{requestDetail(approval)}

+

{requestDetail(approval, t)}

{argumentsPreview ? (
- Review arguments + {t("approval.reviewArguments", "Review arguments")}
{argumentsPreview}
) : null} @@ -91,14 +93,14 @@ export function ApprovalCard({ approval, busy, onRespond }: ApprovalCardProps) { onClick={() => onRespond(approval.id, "approved_once")} disabled={busy} > - Allow once + {t("approval.allowOnce", "Allow once")}
diff --git a/desktop/src/features/goal/GoalRail.tsx b/desktop/src/features/goal/GoalRail.tsx index 6d9fee43..69f60d48 100644 --- a/desktop/src/features/goal/GoalRail.tsx +++ b/desktop/src/features/goal/GoalRail.tsx @@ -10,6 +10,7 @@ import { X, } from "lucide-react"; import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import type { Goal, @@ -58,6 +59,7 @@ export function GoalRail({ const [objective, setObjective] = useState(""); const [tokenBudget, setTokenBudget] = useState(""); const [skillIds, setSkillIds] = useState([]); + const { t } = useTranslation(); const selectableSkills = useMemo( () => skills.filter((skill) => skill.selectable && skill.enabled), [skills], @@ -110,8 +112,8 @@ export function GoalRail({ disabled={!enabled || busy} > - Set a Goal - Keep a durable outcome across ordinary Turns + {t("goal.setGoal", "Set a Goal")} + {t("goal.setGoalHint", "Keep a durable outcome across ordinary Turns")} ); } @@ -120,7 +122,7 @@ export function GoalRail({
{goal ? ( <> @@ -139,11 +141,11 @@ export function GoalRail({ {presentation?.label} · {presentation?.linkedTurnCount ?? 0}{" "} - {presentation?.linkedTurnCount === 1 ? "Turn" : "Turns"} + {presentation?.linkedTurnCount === 1 ? t("goal.turn", "Turn") : t("goal.turns", "Turns")} {goal.tokensUsed > 0 ? ( <> · - {compactNumber(goal.tokensUsed)} tokens + {compactNumber(goal.tokensUsed)} {t("goal.tokens", "tokens")} ) : null} @@ -169,7 +171,7 @@ export function GoalRail({ disabled={!enabled || busy} > - Continue + {t("goal.continue", "Continue")} ) : null} ) : goal.status === "complete" ? ( @@ -195,7 +197,7 @@ export function GoalRail({ onClick={() => openEditor(true)} disabled={!enabled || busy} > - Edit & reopen + {t("goal.editReopen", "Edit & reopen")} ) : ( <> @@ -205,14 +207,14 @@ export function GoalRail({ disabled={!enabled || busy} > - Resume + {t("goal.resume", "Resume")} )} @@ -220,25 +222,23 @@ export function GoalRail({ {expanded ? (

- Goal {shortId(goal.id)} stays attached to this - Session. Normal follow-ups steer the work without rewriting the - objective. + {t("goal.goalDescription", "Goal {{id}} stays attached to this Session. Normal follow-ups steer the work without rewriting the objective.", { id: shortId(goal.id) })}

{goal.timeUsedSeconds > 0 - ? `${formatDuration(goal.timeUsedSeconds)} active time` - : "No completed Goal Turn usage yet"} + ? t("goal.activeTime", "{{duration}} active time", { duration: formatDuration(goal.timeUsedSeconds) }) + : t("goal.noUsage", "No completed Goal Turn usage yet")} {goal.tokenBudget - ? ` · ${compactNumber(goal.tokenBudget)} token budget` - : " · no token budget"} + ? ` · ${t("goal.tokenBudgetLabel", "{{budget}} token budget", { budget: compactNumber(goal.tokenBudget) })}` + : ` · ${t("goal.noTokenBudget", "no token budget")}`}

{outcome ? ( -
+
{outcome.status === "complete" - ? "Completion outcome" - : "Blocked outcome"} + ? t("goal.completionOutcome", "Completion outcome") + : t("goal.blockedOutcome", "Blocked outcome")} {outcome.source} @@ -249,12 +249,12 @@ export function GoalRail({

{outcome.reason}

{outcome.decidedByTurnId ? ( - Deciding Turn {shortId(outcome.decidedByTurnId)} + {t("goal.decidingTurn", "Deciding Turn")} {shortId(outcome.decidedByTurnId)} ) : null} {outcome.evidenceRefs.length ? (
- Related activity + {t("goal.relatedActivity", "Related activity")} {outcome.evidenceRefs.map((evidence) => (
) : null} @@ -313,46 +313,45 @@ export function GoalRail({ >
- {goal ? "Edit Goal" : "New Goal"} + {goal ? t("goal.editGoal", "Edit Goal") : t("goal.newGoal", "New Goal")} - Objective edits keep the same Goal identity and reach the active - Turn. + {t("goal.objectiveHint", "Objective edits keep the same Goal identity and reach the active Turn.")}