From fc8eae7426909526ddb1073d2521b6a6d96df749 Mon Sep 17 00:00:00 2001 From: "Xu, Jingrong" Date: Sat, 31 Jan 2026 00:41:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E9=85=8D=E7=BD=AE=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E5=92=8C=E5=8F=82=E6=95=B0=E7=9A=84=E6=B1=89=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/ui/locales/zh-CN.ts | 241 ++++++++++++++++++++++++++ ui/src/ui/views/config-form.node.ts | 52 ++++-- ui/src/ui/views/config-form.render.ts | 25 ++- ui/src/ui/views/config.ts | 19 ++ ui/src/ui/views/skills.ts | 4 +- 5 files changed, 322 insertions(+), 19 deletions(-) diff --git a/ui/src/ui/locales/zh-CN.ts b/ui/src/ui/locales/zh-CN.ts index d9acfcfbf..57bcac5d6 100644 --- a/ui/src/ui/locales/zh-CN.ts +++ b/ui/src/ui/locales/zh-CN.ts @@ -75,6 +75,235 @@ export const zhCN = { authFailed: "身份验证失败。请重新复制包含令牌的 URL 或更新令牌,然后点击连接。", insecureContext: "当前页面为 HTTP,浏览器已禁用设备身份。请使用 HTTPS 或在网关主机上访问 localhost。", }, + nodes: { + approvalsTitle: "节点审批", + approvalsSubtitle: "配置对新节点的默认连接策略", + nodesTitle: "节点", + nodesSubtitle: "管理已连接的计算节点", + devicesTitle: "设备", + devicesSubtitle: "管理已配对的设备", + target: "目标", + targetHint: "允许连接的目标节点类型", + hostLabel: "主机", + gateway: "网关", + node: "节点", + selectNode: "选择节点...", + noApprovalsNodes: "未找到可审批的节点", + scope: "范围", + defaults: "默认", + security: "安全", + securityDefaultHint: "默认安全策略", + securityAgentHint: "当前设置: {security}", + modeLabel: "模式", + useDefault: "使用默认值 ({security})", + modelLabel: "模型", + securityOptions: { + deny: "拒绝", + allowlist: "白名单", + full: "完全访问", + }, + ask: "询问", + askDefaultHint: "连接时的询问策略", + askOptions: { + off: "关闭", + onMiss: "仅缺失时", + always: "总是", + }, + askFallback: "后备询问", + askFallbackHint: "后备询问策略", + fallbackLabel: "后备", + autoAllowSkills: "自动允许技能", + autoAllowSkillsHint: "自动允许已知技能运行", + bindingTitle: "绑定", + bindingSubtitle: "将代理绑定到特定节点", + save: "保存", + saving: "保存中...", + bindingRawWarn: "高级模式:请小心编辑 JSON", + loadConfigToEdit: "加载配置以编辑绑定", + loadConfig: "加载配置", + loadApprovalsToEdit: "加载审批配置以编辑", + loadApprovals: "加载审批配置", + defaultBinding: "默认绑定", + defaultBindingHint: "未指定绑定的代理将使用此节点", + nodeLabel: "节点", + anyNode: "任意节点 (自动调度)", + noRunNodes: "未发现可作为运行目标的节点", + noNodesFound: "未发现已连接的节点", + pending: "待批准", + paired: "已配对", + noPairedDevices: "暂无已配对设备", + approve: "批准", + reject: "拒绝", + tokens: "访问令牌", + tokensNone: "无令牌", + revoked: "已撤销", + active: "活跃", + rotate: "轮换", + revoke: "撤销", + }, + debug: { + snapshotsTitle: "快照", + snapshotsSubtitle: "当前系统状态快照", + status: "状态", + health: "健康状况", + lastHeartbeat: "最近心跳", + securityAudit: "安全审计: {label}", + criticalIssues: "{count} 个严重问题", + warningIssues: "{count} 个警告", + infoIssues: "{count} 个提示", + noCriticalIssues: "无严重问题", + refreshing: "刷新中...", + manualRpcTitle: "手动 RPC", + manualRpcSubtitle: "手动调用远程过程", + methodLabel: "方法", + paramsLabel: "参数 (JSON)", + call: "调用", + modelsTitle: "模型", + modelsSubtitle: "已加载的模型", + eventLogTitle: "事件日志", + eventLogSubtitle: "最近的系统事件", + noEvents: "暂无事件。", + }, + logs: { + logsTitle: "日志", + logsSubtitle: "实时系统日志流", + export: "导出", + exportFiltered: "导出筛选结果", + exportVisible: "导出可见日志", + noEntries: "暂无日志条目。", + filterLabel: "过滤", + searchPlaceholder: "搜索日志...", + autoFollow: "自动滚动", + fileLabel: "日志文件", + truncatedWarn: "日志已截断,仅显示最近的部分。", + trace: "Trace", + debug: "Debug", + info: "Info", + warn: "Warn", + error: "Error", + fatal: "Fatal", + }, + config: { + subtitle: "全局配置编辑器", + searchPlaceholder: "搜索配置项...", + allSettings: "所有设置", + sections: { + env: "环境", + update: "更新", + agents: "代理", + auth: "认证", + channels: "渠道", + messages: "消息", + commands: "命令", + hooks: "钩子", + skills: "技能", + tools: "工具", + gateway: "网关", + wizard: "向导", + meta: "元数据", + diagnostics: "诊断", + logging: "日志", + browser: "浏览器", + ui: "界面", + models: "模型", + nodeHost: "节点主机", + bindings: "绑定", + broadcast: "广播", + audio: "音频", + media: "媒体", + approvals: "审批", + session: "会话", + cron: "定时任务", + web: "Web 服务", + discovery: "发现", + canvasHost: "画布主机", + talk: "语音", + plugins: "插件", + }, + formMode: "表单模式", + rawMode: "原始模式", + reload: "重新加载", + save: "保存", + apply: "应用", + update: "更新", + noChanges: "没有更改", + schema: { + logging: { + label: "日志", + description: "日志级别和输出配置", + consoleLevel: { label: "控制台级别", description: "控制台日志的输出级别" }, + consoleStyle: { label: "控制台样式", description: "控制台日志的格式 (plain/json)" }, + file: { label: "文件日志", description: "日志文件路径" }, + level: { label: "日志级别", description: "全局日志级别 (trace/debug/info/warn/error)" }, + redactPatterns: { label: "脱敏模式", description: "用于在日志中隐藏敏感信息的正则表达式" }, + redactSensitive: { label: "自动脱敏", description: "自动隐藏已知的敏感键值 (如 password, token)" }, + }, + diagnostics: { + label: "诊断", + description: "OpenTelemetry 与调试选项", + enabled: { label: "启用诊断", description: "开启系统诊断功能" }, + flags: { label: "诊断标志", description: "启用特定模块的详细日志 (如 *telegram*)" }, + cacheTrace: { + label: "缓存跟踪", + description: "配置缓存操作的跟踪详细程度", + enabled: { label: "启用缓存跟踪", description: "记录嵌入式代理运行的缓存跟踪快照" }, + filePath: { label: "缓存跟踪文件路径", description: "缓存跟踪日志的 JSONL 输出路径" }, + includeMessages: { label: "包含消息", description: "在跟踪输出中包含完整的消息负载" }, + includePrompt: { label: "包含提示词", description: "在跟踪中包含提示词文本" }, + includeSystem: { label: "包含系统提示", description: "在跟踪中包含系统提示词" }, + }, + otel: { + label: "OpenTelemetry", + description: "OTLP 导出配置", + enabled: { label: "启用 OpenTelemetry", description: "发送遥测数据到 OTLP 端点" }, + endpoint: { label: "OTLP 端点", description: "OTLP 收集器 URL" }, + flushInterval: { label: "刷新间隔 (ms)", description: "数据上报的频率" }, + headers: { label: "请求头", description: "附加的 HTTP 请求头" }, + logsEnabled: { label: "启用日志", description: "发送日志数据" }, + metricsEnabled: { label: "启用指标", description: "发送指标数据" }, + tracesEnabled: { label: "启用链路追踪", description: "发送链路追踪数据" }, + protocol: { label: "协议", description: "传输协议 (http/grpc)" }, + }, + }, + update: { + label: "更新", + description: "配置更新行为", + channel: { label: "更新渠道", description: "选择更新发布渠道 (main/beta/dev)" }, + }, + meta: { + label: "元数据", + description: "系统运行信息", + lastRunAt: { label: "上次运行时间", description: "" }, + lastRunCommand: { label: "上次运行命令", description: "" }, + lastRunCommit: { label: "上次运行提交", description: "" }, + lastRunMode: { label: "上次运行模式", description: "" }, + lastRunVersion: { label: "上次运行版本", description: "" }, + }, + auth: { + label: "认证", + description: "身份验证设置", + allowTailscale: { label: "允许 Tailscale", description: "允许 Tailscale 访问" }, + mode: { label: "模式", description: "认证模式 (token/password)" }, + password: { label: "网关密码", description: "Tailscale funnel 需要此密码" }, + token: { label: "网关令牌", description: "访问网关所需的令牌" }, + gatewayPassword: { label: "网关密码", description: "Tailscale funnel 需要此密码" }, + gatewayToken: { label: "网关令牌", description: "访问网关所需的令牌" }, + }, + nodeHost: { + label: "节点主机", + description: "网关与节点通信配置", + allowTobacco: { label: "允许烟草内容", description: "是否允许涉及烟草的内容" }, + password: { label: "网关密码", description: "连接网关所需的密码" }, + token: { label: "网关令牌", description: "连接网关所需的令牌" }, + mode: { label: "认证模式", description: "选择 token 或 password 认证" }, + }, + env: { + label: "环境变量", + description: "传递给网关进程的环境变量", + allSubsections: { label: "所有变量", description: "所有配置的环境变量列表" }, + }, + }, + }, channels: { title: "渠道", subtitle: "管理消息渠道连接", @@ -309,6 +538,18 @@ export const zhCN = { reasonAllowlist: "被白名单拦截", enable: "启用", disable: "禁用", + names: { + "1password": "1Password CLI", + "apple-notes": "备忘录 (Apple Notes)", + "apple-reminders": "提醒事项 (Apple Reminders)", + "github": "GitHub", + "openai-whisper": "OpenAI Whisper", + }, + descriptions: { + "1password": "设置并使用 1Password CLI (op)。用于安装 CLI、启用桌面集成、登录账户或通过 op 访问机密。", + "apple-notes": "在 macOS 上通过 `memo` CLI 管理 Apple Notes (创建、查看、编辑、删除、搜索、移动和导出笔记)。", + "apple-reminders": "在 macOS 上通过 `remindctl` CLI 管理 Apple Reminders (列表、添加、编辑、完成、删除)。", + }, }, instances: { title: "已连接实例", diff --git a/ui/src/ui/views/config-form.node.ts b/ui/src/ui/views/config-form.node.ts index 3416e98cb..5983d4b17 100644 --- a/ui/src/ui/views/config-form.node.ts +++ b/ui/src/ui/views/config-form.node.ts @@ -27,6 +27,34 @@ function jsonValue(value: unknown): string { } } +function resolveNodeLabels( + path: Array, + schema: JsonSchema, + hints: ConfigUiHints +): { label: string; help?: string } { + const hint = hintForPath(path, hints); + // Try translation + // Filter out numeric indices to handle array items if necessary, + // but for settings keys (which are object properties), we usually want the specific path. + // However, many array items might share the same schema. + // For now, we focus on the settings panel which is mostly object paths. + const strPath = path.join("."); + const schemaPath = `config.schema.${strPath}`; + const labelKey = `${schemaPath}.label`; + const descKey = `${schemaPath}.description`; + + const translatedLabel = t(labelKey); + const translatedDesc = t(descKey); + + const fallbackLabel = hint?.label ?? schema.title ?? humanize(String(path.at(-1))); + const fallbackHelp = hint?.help ?? schema.description; + + return { + label: translatedLabel && translatedLabel !== labelKey ? translatedLabel : fallbackLabel, + help: translatedDesc && translatedDesc !== descKey ? translatedDesc : fallbackHelp, + }; +} + // SVG Icons as template literals const icons = { chevronDown: html``, @@ -49,9 +77,12 @@ export function renderNode(params: { const { schema, value, path, hints, unsupported, disabled, onPatch } = params; const showLabel = params.showLabel ?? true; const type = schemaType(schema); - const hint = hintForPath(path, hints); - const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1))); - const help = hint?.help ?? schema.description; + const hint = hintForPath(path, hints); // Keep for sensitive check below? + // Actually resolveNodeLabels does `hintForPath` internally but we might need `hint` variable for other things like `sensitive`. + // `renderNode` doesn't use `hint` for anything else except label/help. + // `pathKey` is used below. + + const { label, help } = resolveNodeLabels(path, schema, hints); const key = pathKey(path); if (unsupported.has(key)) { @@ -229,8 +260,7 @@ function renderTextInput(params: { const { schema, value, path, hints, disabled, onPatch, inputType } = params; const showLabel = params.showLabel ?? true; const hint = hintForPath(path, hints); - const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1))); - const help = hint?.help ?? schema.description; + const { label, help } = resolveNodeLabels(path, schema, hints); const isSensitive = hint?.sensitive ?? isSensitivePath(path); const placeholder = hint?.placeholder ?? @@ -297,8 +327,7 @@ function renderNumberInput(params: { const { schema, value, path, hints, disabled, onPatch } = params; const showLabel = params.showLabel ?? true; const hint = hintForPath(path, hints); - const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1))); - const help = hint?.help ?? schema.description; + const { label, help } = resolveNodeLabels(path, schema, hints); const displayValue = value ?? schema.default ?? ""; const numValue = typeof displayValue === "number" ? displayValue : 0; @@ -348,8 +377,7 @@ function renderSelect(params: { const { schema, value, path, hints, disabled, options, onPatch } = params; const showLabel = params.showLabel ?? true; const hint = hintForPath(path, hints); - const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1))); - const help = hint?.help ?? schema.description; + const { label, help } = resolveNodeLabels(path, schema, hints); const resolvedValue = value ?? schema.default; const currentIndex = options.findIndex( (opt) => opt === resolvedValue || String(opt) === String(resolvedValue), @@ -391,8 +419,7 @@ function renderObject(params: { const { schema, value, path, hints, unsupported, disabled, onPatch } = params; const showLabel = params.showLabel ?? true; const hint = hintForPath(path, hints); - const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1))); - const help = hint?.help ?? schema.description; + const { label, help } = resolveNodeLabels(path, schema, hints); const fallback = value ?? schema.default; const obj = fallback && typeof fallback === "object" && !Array.isArray(fallback) @@ -490,8 +517,7 @@ function renderArray(params: { const { schema, value, path, hints, unsupported, disabled, onPatch } = params; const showLabel = params.showLabel ?? true; const hint = hintForPath(path, hints); - const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1))); - const help = hint?.help ?? schema.description; + const { label, help } = resolveNodeLabels(path, schema, hints); const itemsSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items; if (!itemsSchema) { diff --git a/ui/src/ui/views/config-form.render.ts b/ui/src/ui/views/config-form.render.ts index 7e3f1444a..fe93d6035 100644 --- a/ui/src/ui/views/config-form.render.ts +++ b/ui/src/ui/views/config-form.render.ts @@ -185,8 +185,16 @@ export function renderConfigForm(props: ConfigFormProps) { ? (() => { const { sectionKey, subsectionKey, schema: node } = subsectionContext; const hint = hintForPath([sectionKey, subsectionKey], props.uiHints); - const label = hint?.label ?? node.title ?? humanize(subsectionKey); - const description = hint?.help ?? node.description ?? ""; + + // Try translation first + const schemaPath = `config.schema.${sectionKey}.${subsectionKey}`; + const labelKey = `${schemaPath}.label`; + const descKey = `${schemaPath}.description`; + const translatedLabel = t(labelKey); + const translatedDesc = t(descKey); + + const label = translatedLabel !== labelKey ? translatedLabel : (hint?.label ?? node.title ?? humanize(subsectionKey)); + const description = translatedDesc !== descKey ? translatedDesc : (hint?.help ?? node.description ?? ""); const sectionValue = (value as Record)[sectionKey]; const scopedValue = sectionValue && typeof sectionValue === "object" @@ -221,14 +229,23 @@ export function renderConfigForm(props: ConfigFormProps) { })() : filteredEntries.map(([key, node]) => { const meta = getSectionMeta(key); + // Try translation for top-level schema items if not in meta + const schemaPath = `config.schema.${key}`; + const labelKey = `${schemaPath}.label`; + const descKey = `${schemaPath}.description`; + const translatedLabel = t(labelKey); + const translatedDesc = t(descKey); + + const label = translatedLabel !== labelKey ? translatedLabel : meta.label; + const description = translatedDesc !== descKey ? translatedDesc : meta.description; return html`
${getSectionIcon(key)}
-

${meta.label}

- ${meta.description +

${label}

+ ${description ? html`

${meta.description}

` : nothing}
diff --git a/ui/src/ui/views/config.ts b/ui/src/ui/views/config.ts index 5a8e79b7d..363cb215c 100644 --- a/ui/src/ui/views/config.ts +++ b/ui/src/ui/views/config.ts @@ -88,6 +88,25 @@ const SECTIONS: Array<{ key: string; label: string }> = [ { key: "tools", label: t("config.sections.tools") }, { key: "gateway", label: t("config.sections.gateway") }, { key: "wizard", label: t("config.sections.wizard") }, + { key: "meta", label: t("config.sections.meta") }, + { key: "diagnostics", label: t("config.sections.diagnostics") }, + { key: "logging", label: t("config.sections.logging") }, + { key: "browser", label: t("config.sections.browser") }, + { key: "ui", label: t("config.sections.ui") }, + { key: "models", label: t("config.sections.models") }, + { key: "nodeHost", label: t("config.sections.nodeHost") }, + { key: "bindings", label: t("config.sections.bindings") }, + { key: "broadcast", label: t("config.sections.broadcast") }, + { key: "audio", label: t("config.sections.audio") }, + { key: "media", label: t("config.sections.media") }, + { key: "approvals", label: t("config.sections.approvals") }, + { key: "session", label: t("config.sections.session") }, + { key: "cron", label: t("config.sections.cron") }, + { key: "web", label: t("config.sections.web") }, + { key: "discovery", label: t("config.sections.discovery") }, + { key: "canvasHost", label: t("config.sections.canvasHost") }, + { key: "talk", label: t("config.sections.talk") }, + { key: "plugins", label: t("config.sections.plugins") }, ]; type SubsectionEntry = { diff --git a/ui/src/ui/views/skills.ts b/ui/src/ui/views/skills.ts index 513dcc8d2..2d70ac01a 100644 --- a/ui/src/ui/views/skills.ts +++ b/ui/src/ui/views/skills.ts @@ -92,9 +92,9 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
- ${skill.emoji ? `${skill.emoji} ` : ""}${skill.name} + ${skill.emoji ? `${skill.emoji} ` : ""}${t(`skills.names.${skill.skillKey}`) !== `skills.names.${skill.skillKey}` ? t(`skills.names.${skill.skillKey}`) : skill.name}
-
${clampText(skill.description, 140)}
+
${clampText((t(`skills.descriptions.${skill.skillKey}`) !== `skills.descriptions.${skill.skillKey}` ? t(`skills.descriptions.${skill.skillKey}`) : skill.description), 140)}
${skill.source}