完成配置界面和参数的汉化
This commit is contained in:
parent
5f6dc2d89b
commit
fc8eae7426
@ -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: "已连接实例",
|
||||
|
||||
@ -27,6 +27,34 @@ function jsonValue(value: unknown): string {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveNodeLabels(
|
||||
path: Array<string | number>,
|
||||
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`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`,
|
||||
@ -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) {
|
||||
|
||||
@ -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<string, unknown>)[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`
|
||||
<section class="config-section-card" id="config-section-${key}">
|
||||
<div class="config-section-card__header">
|
||||
<span class="config-section-card__icon">${getSectionIcon(key)}</span>
|
||||
<div class="config-section-card__titles">
|
||||
<h3 class="config-section-card__title">${meta.label}</h3>
|
||||
${meta.description
|
||||
<h3 class="config-section-card__title">${label}</h3>
|
||||
${description
|
||||
? html`<p class="config-section-card__desc">${meta.description}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -92,9 +92,9 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">
|
||||
${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}
|
||||
</div>
|
||||
<div class="list-sub">${clampText(skill.description, 140)}</div>
|
||||
<div class="list-sub">${clampText((t(`skills.descriptions.${skill.skillKey}`) !== `skills.descriptions.${skill.skillKey}` ? t(`skills.descriptions.${skill.skillKey}`) : skill.description), 140)}</div>
|
||||
<div class="chip-row" style="margin-top: 6px;">
|
||||
<span class="chip">${skill.source}</span>
|
||||
<span class="chip ${skill.eligible ? "chip-ok" : "chip-warn"}">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user