Files
hapi/web/src/lib/locales/zh-CN.ts
T
Junmo KimandGitHub 6e32b20524 feat(web): linkify custom URI schemes with a confirm prompt (#633)
* feat(web): add UriConfirmDialog component

Add a Radix Dialog-based confirmation modal for custom URI scheme
navigation. Follows the RenameSessionDialog pattern.

- UriConfirmDialog: shows URI, scheme label, Cancel/Open/Always-allow buttons
- i18n keys: dialog.uri.{title,description,open,alwaysAllow}

* feat(web): autolink non-https URI schemes in markdown

Add a remark plugin that converts raw `scheme://...` text nodes into
link nodes for non-http(s) schemes. GFM already handles http/https;
this plugin handles the remainder (obsidian://, vscode://, slack://, etc.).

- No scheme allowlist: every `scheme://` pattern is converted; the
  sanitize layer (urlTransform) and onClick layer (classifyScheme) handle
  blocking/confirmation downstream.
- Runs before remarkStripCjkAutolink so the CJK-strip plugin sees the
  new link nodes and can trim trailing CJK punctuation from them.
- Trailing punctuation (.,;!?) stripped from matched URIs.
- Unit tests: conversion, partial-match, escape, explicit link bypass,
  code-block bypass, trailing-punct trimming.

* feat(web): linkify custom URI schemes via markdown <a> handler

Wire up 4-layer URI security policy in the markdown renderer:

1. URL sanitize (deny-only): urlTransform strips javascript:/data:/vbscript:/file:
   using classifyScheme as single source of truth (handles percent-encoding,
   case-insensitive, whitespace-prefix bypass patterns).

2. onClick intercept: custom <A> component classifies each href —
   - IANA safe (https/http/irc/ircs/mailto/xmpp): navigate directly.
   - Deny (javascript/data/vbscript/file): preventDefault silently.
   - Custom (obsidian/vscode/slack/…): preventDefault + open UriConfirmDialog.

3. UriConfirmProvider: one dialog lifted to each markdown root (MarkdownText,
   Reasoning, MarkdownRenderer). Shared isAllowed state across all <a> tags in
   the subtree — "Always allow" click updates every link in one React commit.

4. Intra-tab cross-provider sync (P7e.1): module-level schemeListeners Set so
   sibling UriConfirmProviders (MarkdownText + Reasoning in AssistantMessage)
   receive allowed-scheme updates synchronously without waiting for the window
   storage event (which only fires in other tabs). Cross-tab sync continues via
   the existing window storage event listener.

5. "Always allow" persisted to localStorage (hapi-allowed-schemes). Custom
   schemes once allowed navigate directly on subsequent clicks, no dialog gate.
   href="#" in DOM for unallowed custom schemes prevents middle-click bypass.
   Deny-scheme href="" prevents any navigation even if localStorage tampered.

Security: classifyScheme decodes percent-encoding before scheme extraction,
blocking %6Aavascript:, jav%61script:, javascript%3A (single-encoded colon)
and double-encoded variants. DENY_SCHEMES checked after localStorage lookup so
tampered allowed-list cannot promote deny schemes.

Tests: classifyScheme 6-axis security bypass, denyOnlyTransform, localStorage
roundtrip, cross-tab storage event, <A> click handler cases.

* fix(web): block control-char-spliced deny schemes in classifyScheme

Browsers silently strip ASCII control characters (\t, \n, \r) and
whitespace from URL scheme names during navigation. A scheme like
`java\nscript:alert(1)` was navigated as `javascript:` while our
literal string comparison classified it as 'custom', allowing it
past the deny list and into window.open().

Introduce normalizedScheme() that:
- applies 2 rounds of decodeURIComponent so double-encoded schemes
  (javascript%253A → javascript%3A → javascript:) are fully unwrapped
  before comparison
- strips [\x00-\x1F\x7F\s] from the extracted scheme name, matching
  the browser's own normalization

classifyScheme() now delegates to normalizedScheme() so both the
denyOnlyTransform (urlTransform) path and the <A> onClick path benefit
from the same normalization.

Tests added for \n / \t / \r / space spliced into scheme, and verify
that double-encoded colon is now caught via scheme-match (not just
the no-colon fallback).

* fix(web): preserve relative markdown links from being blocked

Relative / no-scheme hrefs (/settings, ./foo, #section, ?q=1) were
silently preventDefault'd in <A>'s onClick handler. denyOnlyTransform
correctly passed them through (no colon → not a scheme URL), but the
click handler called classifyScheme(href) which returned 'deny' for
any input with no valid scheme separator — then the deny branch fired.

Add hasScheme(href): checks whether the first ':' appears before any
path/query/fragment boundary ('/', '?', '#'). When hasScheme is false
the href is treated as 'iana' so the browser or SPA router can navigate
normally with no dialog and no preventDefault.

Also wrap renderA() with <I18nProvider> so the UriConfirmDialog that
UriConfirmProvider may render does not throw outside its translation
context during tests.

Fixes a regression that broke all relative-path markdown links once the
custom-URI-scheme onClick handler was added.

* test(web): cover percent-encoded scheme control char + protocol-relative href

Round-5 internal hostile review noted two coverage gaps on the bot-fixup commits:

- `java%0Ascript:alert(1)` (percent-encoded newline in the scheme name) takes the
  same decode→strip code path as the literal `java\nscript:` case but was only
  tested literally. Add an explicit test so a future refactor that drops the
  decode-then-strip ordering would be caught.
- Protocol-relative URLs (`//host/path`) have no colon, so `hasScheme` returns
  false and `<A>` treats them as scheme-less — browsers then navigate them as
  the current origin's protocol. Existing relative-href tests covered absolute
  paths, hashes, queries, and colon-in-path, but not the protocol-relative
  variant. Add one assertion.

Also extend the `hasScheme` JSDoc to note that protocol-relative URLs are
intentionally treated as scheme-less.

* fix(web): preserve balanced parens/brackets in autolinked URIs

The trailing-punctuation strip used to drop every `)` / `]` from the end
of a matched URI, even when the URL body had an unmatched opener. So a
URI like `obsidian://open?file=Note(1)` was rendered with href
`obsidian://open?file=Note(1` plus a separate `)` text node, opening a
broken deep link.

Match the GFM autolink-literal behaviour: when the trailing character is
`)` or `]`, keep it iff the URL body has more opening counterparts than
closers (so the trailing closer balances an earlier opener and belongs
to the URL). Other trailing punctuation (`.,;!?:>'"`) and unmatched
closers still strip as before.

Add tests for the balanced cases (`Note(1)`, `Note[1]`, nested
`(a(b)c)`), the "balanced URL followed by a period" case, and a
regression test that an unmatched `).` after a URL is still stripped.
2026-05-18 10:40:32 +08:00

453 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export default {
// Loading states
'loading': '加载中…',
'authorizing': '认证中…',
'loading.session': '加载会话…',
'loading.git': '加载 Git 状态…',
'loading.file': '加载文件…',
'loading.files': '加载文件…',
'loading.messages': '加载消息…',
'loading.machines': '加载机器…',
// Login / Auth
'login.title': 'HAPI',
'login.subtitle': '随时随地,Vibe Coding',
'login.bind.title': '绑定 Telegram',
'login.submit': '登录',
'login.bind.submit': '绑定',
'login.submitting': '登录中…',
'login.bind.submitting': '绑定中…',
'login.placeholder': '访问令牌',
'login.error.enterToken': '请输入访问令牌',
'login.error.bindingUnavailable': '绑定服务不可用。',
'login.error.loginUnavailable': '登录服务不可用。',
'login.error.bindFailed': '绑定失败',
'login.error.authFailed': '认证失败',
'login.help': '需要帮助?',
'login.server.default': '(默认)',
'login.server.custom': '(自定义)',
'login.server.title': '服务器地址',
'login.server.description': '设置 HAPI 服务器地址以使用 API 和实时更新。',
'login.server.current': '当前:',
'login.server.origin': '服务器地址',
'login.server.placeholder': 'https://hapi.example.com',
'login.server.hint': '仅使用 http(s) 协议。任何路径将被忽略。',
'login.server.required': '必须先填写服务器地址才能登录。',
'login.server.useSameOrigin': '使用同源',
'login.server.save': '保存',
'login.footer': '用',
'login.footer.for': '设计,Vibe Coding',
'login.footer.copyright': '©',
// Sessions page
'sessions.count': '{n} 个会话,{m} 个项目',
'sessions.new': '新建会话',
'sessions.empty.title': '还没有会话',
'sessions.empty.hint': '在 workspace 下任意目录启动一个会话,或先浏览目录树看看。',
'sessions.empty.startSession': '启动会话',
'sessions.empty.browse': '浏览 workspace',
'sessions.search.placeholder': '搜索会话…',
'sessions.search.clear': '清除搜索',
'sessions.search.count': '{n} / {total} 个会话',
'sessions.search.noResults': '没有匹配的会话。',
'sessions.group.showMore': '再显示 {n} 个',
'sessions.group.showLess': '收起',
'sessions.group.new': '在此目录新建会话',
// Session list
'session.item.path': '路径',
'session.item.agent': '代理',
'session.item.model': '模型',
'session.item.worktree': '工作树',
'session.item.pending': '待处理',
'session.item.thinking': '思考中',
'session.time.justNow': '刚刚',
'session.time.minutesAgo': '{n} 分钟前',
'session.time.hoursAgo': '{n} 小时前',
'session.time.daysAgo': '{n} 天前',
// Session header
'session.title': '文件',
'session.more': '更多操作',
'session.outline.open': '会话大纲',
'session.outline.close': '关闭大纲',
'session.outline.title': '大纲',
'session.outline.loadOlder': '加载更早',
'session.outline.empty': '已加载消息中暂无大纲项',
'session.outline.kind.user': '用户',
// Session actions
'session.action.rename': '重命名',
'session.action.archive': '归档',
'session.action.delete': '删除',
'session.action.copy': '复制',
// Dialogs
'dialog.uri.title': '打开此链接?',
'dialog.uri.description': '此链接使用自定义 URI 协议。请确认您信任此链接后再打开。',
'dialog.uri.open': '打开',
'dialog.uri.alwaysAllow': '始终允许 {scheme}:',
'dialog.rename.title': '重命名会话',
'dialog.rename.placeholder': '会话名称',
'dialog.rename.save': '保存',
'dialog.rename.saving': '保存中…',
'dialog.rename.error': '重命名失败,请重试。',
'dialog.archive.title': '归档会话',
'dialog.archive.description': '确定要归档 "{name}" 吗?这将断开活动会话。',
'dialog.archive.confirm': '归档',
'dialog.archive.confirming': '归档中…',
'dialog.delete.title': '删除会话',
'dialog.delete.description': '确定要删除 "{name}" 吗?此操作无法撤销。',
'dialog.delete.confirm': '删除',
'dialog.delete.confirming': '删除中…',
'dialog.error.default': '操作失败,请重试。',
// Common buttons
'button.cancel': '取消',
'button.save': '保存',
'button.confirm': '确认',
'button.close': '关闭',
'button.dismiss': '忽略',
'button.copy': '复制',
'button.paste': '粘贴',
// New session form
'newSession.title': '创建会话',
'newSession.machine': '机器',
'newSession.directory': '目录',
'newSession.placeholder': '/path/to/project',
'newSession.browse': '浏览',
'newSession.recent': '最近路径',
'newSession.type': '会话类型',
'newSession.type.simple': '简单',
'newSession.type.simple.desc': '直接使用选定的目录',
'newSession.type.worktree': '工作树',
'newSession.type.worktree.desc': '在仓库旁创建新工作树',
'newSession.type.worktree.placeholder': 'feature-x (默认 1228-xxxx)',
'newSession.agent': '代理',
'newSession.model': '模型',
'newSession.effort': '思考强度',
'newSession.model.optional': '可选',
'newSession.model.loadFailed': '加载 Codex 模型失败',
'newSession.opencodeModel.loading': '正在发现 OpenCode 模型…',
'newSession.opencodeModel.loadFailed': '加载 OpenCode 模型失败',
'newSession.opencodeModel.retry': '重试',
'newSession.opencodeModel.empty': '未在此目录发现 OpenCode 模型',
'newSession.opencodeModel.default': '默认',
'newSession.reasoningEffort': '推理强度',
'newSession.yolo': 'YOLO 模式',
'newSession.yolo.title': '跳过审批和沙箱',
'newSession.yolo.desc': '启动时使用危险的代理标志。',
'newSession.create': '创建',
'newSession.creating': '创建中…',
// Spawn session (old component)
'spawn.title': '创建会话',
'spawn.sessionType': '会话类型',
'spawn.useAsIs': '直接使用选定的目录',
'spawn.createWorktree': '在仓库旁创建新工作树',
'spawn.worktreePlaceholder': 'feature-x (默认 1228-xxxx)',
'spawn.cancel': '取消',
'spawn.create': '创建会话',
'spawn.creating': '创建中…',
'session.directoryMissingSimple': '目录不存在,创建会话时将自动创建。',
'session.directoryMissingSimpleConfirm': '目录不存在。再次点击按钮将自动新建该目录。',
'session.directoryMissingWorktree': 'worktree 需要已存在的仓库目录。',
'session.codexModelsLoadFailed': '加载 Codex 模型失败',
'session.createAndCreateDirectory': '创建并新建目录',
// Machine
'machine.unknown': '未知平台',
// Chat
'chat.placeholder': '输入消息…',
'chat.send': '发送',
'chat.abort': '中止',
'chat.settings': '设置',
'chat.terminal': '终端',
'chat.switchRemote': '切换到远程模式',
// Codex review
'codexReview.title': 'Codex review',
'codexReview.findings': '{count} 条发现',
'codexReview.confidence': '置信度 {value}',
'codexReview.location.missing': '无位置',
// Terminal
'terminal.commandName': '命令',
'terminal.commandMessage': '命令消息',
'terminal.commandArgs': '命令参数',
'terminal.stdout': '标准输出',
'terminal.stderr': '标准错误',
'terminal.unsupportedWindows': 'Windows 主机暂不支持远程终端。',
'terminal.paste.fallbackTitle': '粘贴输入',
'terminal.paste.fallbackDescription': '无法读取剪贴板,请在下方粘贴文本。',
'terminal.paste.placeholder': '在此粘贴终端输入…',
// Code block
'code.copy': '复制',
'code.truncated': '预览已截断 — 打开详情查看完整输出',
// Diff view
'diff.title': '差异',
'diff.view': '查看',
// Files page
'files.page.title': '文件',
'files.page.refresh': '刷新',
'files.page.searchPlaceholder': '搜索文件',
'files.projectRoot': '项目根目录',
'files.branch.detached': '游离 HEAD',
'files.branch.summary': '{staged} 个已暂存,{unstaged} 个未暂存',
'files.tab.changes': '变更',
'files.tab.directories': '目录',
'files.search.empty': '没有匹配搜索的文件。',
'files.search.error.failed': '搜索文件失败。',
'files.search.error.failedWithDetail': '搜索文件失败:{error}',
'files.changes.section.staged': '已暂存变更({n}',
'files.changes.section.unstaged': '未暂存变更({n}',
'files.changes.empty.unavailable': 'Git 状态不可用。请使用“目录”浏览全部文件,或直接搜索。',
'files.changes.empty.none': '未检测到变更。请使用“目录”浏览全部文件,或直接搜索。',
'files.changes.error.gitStatusUnavailable': 'Git 状态不可用。',
'files.changes.error.gitStatusUnavailableWithDetail': 'Git 状态不可用:{error}',
'files.changes.error.unstagedDiffUnavailableWithDetail': '未暂存 Diff 不可用:{error}',
'files.changes.error.stagedDiffUnavailableWithDetail': '已暂存 Diff 不可用:{error}',
'files.directories.empty': '空目录。',
'files.directories.error.listFailed': '加载目录失败。',
'files.directories.error.listFailedWithDetail': '加载目录失败:{error}',
// File page
'file.page.fallbackName': '文件',
'file.page.unknownPath': '未知路径',
'file.page.copyPath': '复制路径',
'file.page.copyContent': '复制文件内容',
'file.page.tab.diff': 'Diff',
'file.page.tab.file': '文件',
'file.page.missingPath': '未提供文件路径。',
'file.page.binary': '该文件看起来是二进制文件,无法显示。',
'file.page.imagePreviewAlt': '{name} 图片预览',
'file.page.empty': '文件为空。',
'file.page.noChanges': '没有可显示的变更。',
'file.error.readFailed': '读取文件失败。',
'file.error.readFailedWithDetail': '读取文件失败:{error}',
'file.error.diffUnavailable': 'Diff 不可用。',
'file.error.diffUnavailableWithDetail': 'Diff 不可用:{error}',
// Tool card
'tool.askQuestion': '其他',
'tool.edit': '编辑',
'tool.multiedit': '多文件编辑',
'tool.write': '写入',
'tool.todo': '任务',
'tool.confirm': '确认',
'tool.deny': '拒绝',
'tool.abort': '中止',
'tool.allow': '允许',
'tool.allowAll': '允许所有编辑',
'tool.denyAll': '拒绝所有编辑',
'tool.approved': '已批准',
'tool.rejected': '已拒绝',
'tool.cancel': '取消',
'tool.exitPlan': '退出计划模式',
'tool.patch': '补丁',
'tool.input': '输入',
'tool.trace': '追踪',
'tool.trace.callsSuffix': '次调用',
'tool.result': '结果',
'tool.semanticTitle.readFile': '读取文件',
'tool.semanticTitle.runShell': '运行命令',
'tool.semanticTitle.search': '搜索',
'tool.semanticTitle.openUrl': '打开链接',
'tool.semanticTitle.query': '查询',
'tool.questionsAnswers': '问答',
'tool.submit': '提交',
'tool.submitting': '提交中…',
'tool.prev': '← 上一个',
'tool.next': '下一个 →',
'tool.yes': '是',
'tool.yesForSession': '本会话允许',
'tool.allowForSession': '本会话允许',
'tool.abortLabel': '中止',
'tool.other': '其他',
'tool.otherDescription': '输入您自己的答案',
'tool.question': '问题',
'tool.selectOption': '请至少选择一个选项或输入答案。',
'tool.waitingForApproval': '等待审批…',
'tool.canceled': '已取消',
'tool.approvedForSession': '本会话已批准',
'tool.aborted': '已中止',
'tool.approvedAllowAllEdits': '已批准:允许所有编辑',
'tool.requestFailed': '请求失败',
'tool.askUserQuestion.fallback': 'AskUserQuestion 格式不正确。请输入您的答案:',
'tool.askUserQuestion.placeholder': '输入您的答案…',
'tool.askUserQuestion.otherPlaceholder': '或输入您自己的答案…',
'tool.requestUserInput.textPlaceholder': '输入您的答案…',
'tool.requestUserInput.noteLabel': '补充说明(可选)',
'tool.requestUserInput.notePlaceholder': '添加备注…',
'toolGroup.title': '工具活动',
'toolGroup.primary.fileTargets': '{target} 等 +{n}',
'toolGroup.primary.commandTargets': '{target} 等 +{n}',
'toolGroup.primary.searchTargets': '{target} 等 +{n}',
'toolGroup.primary.urlTargets': '{target} 等 +{n}',
'toolGroup.primary.otherTargets': '{target} 等 +{n}',
'toolGroup.summary.read': '读取 {n}',
'toolGroup.summary.mutation': '编辑 {n}',
'toolGroup.summary.command': '执行 {n}',
'toolGroup.summary.search': '搜索 {n}',
'toolGroup.summary.web': '访问 {n}',
'toolGroup.summary.other': '工具 {n}',
'toolGroup.friendly.inspectFiles': '检查项目文件',
'toolGroup.friendly.searchContent': '搜索项目内容',
'toolGroup.friendly.runCommands': '执行项目命令',
'toolGroup.friendly.editFiles': '修改项目文件',
'toolGroup.friendly.openWeb': '浏览网页内容',
'toolGroup.friendly.genericCommand': '执行命令',
'toolGroup.friendly.genericTool': '使用工具',
'toolGroup.badge.running': '运行中 {n}',
'toolGroup.badge.pending': '等待中 {n}',
'toolGroup.badge.error': '错误 {n}',
'toolGroup.badge.fileTargets': '{n} 文件',
'toolGroup.toolCount': '{n} 次操作',
'toolGroup.loadingOlderHistory': '正在补加载更早的工具活动…',
'toolGroup.historyUnavailable': '更早的工具活动已不可用。',
'toolGroup.rowStatus.running': '运行中',
'toolGroup.rowStatus.pending': '等待中',
'toolGroup.rowStatus.error': '错误',
// Composer buttons
'composer.settings': '设置',
'composer.terminal': '终端',
'composer.abort': '中止',
'composer.switchRemote': '切换到远程模式',
'composer.attach': '添加文件',
'composer.send': '发送',
'composer.stop': '停止',
'composer.voice': '语音助手',
'composer.scheduleSend': '定时发送',
'composer.scheduleRelativeTab': '相对时间',
'composer.scheduleSpecificTab': '指定时间',
'composer.scheduleSpecificHint': '最多 7 天。需 Hub 运行;CLI 下次连接时会接收消息。',
'composer.scheduleErrorPast': '发送时间必须在未来。',
'composer.scheduleErrorTooFar': '最多只能定时 7 天。',
'queuedMessages.scheduledFor': '定时发送: {time}',
'queuedMessages.editAlreadyInvoked': '消息已发送,无法编辑',
'composer.codexSlashUnsupported.title': '无法执行 Codex 命令',
'composer.codexSlashUnsupported.body': 'HAPI 远程模式暂不支持 {command} 这类 Codex 内建 slash command,请改用自然语言,或在本地 Codex TUI 中执行。',
// Voice assistant
'voice.connecting': '连接中...',
'voice.active': '活跃',
'voice.muted': '已静音',
'voice.error': '错误',
'voice.mute': '静音',
'voice.unmute': '取消静音',
'voice.end': '结束',
'voice.error.micPermission': '麦克风权限被拒绝',
'voice.error.network': '网络错误',
'voice.error.notInitialized': '语音会话未初始化',
'voice.error.startFailed': '启动语音会话失败',
'voice.error.notAllowed': '语音功能不可用',
'voice.error.connection': '连接错误',
'voice.dismiss': '关闭',
// Banners
'offline.title': '离线',
'offline.message': '您当前处于离线状态。某些功能可能受限。',
'syncing.title': '同步中…',
'syncing.message': '正在同步您的数据。',
'reconnecting.message': '正在重新连接...',
'reconnecting.reason.error': '流连接错误',
'reconnecting.reason.closed': '流连接已关闭',
'reconnecting.reason.heartbeatTimeout': '心跳超时',
'reconnecting.reason.visibilityRecovery': '后台恢复中',
// Send blocked
'send.blocked.title': '无法发送消息',
'send.blocked.noConnection': '未连接到服务器',
'resume.failed.title': '恢复会话失败',
'toast.ready.title': '等待输入',
'toast.ready.body': '{agent} 正在 {session} 等待你的输入',
'toast.permission.title': '权限请求',
'toast.task.completed': '任务完成',
'toast.task.failed': '任务失败',
// Install prompt
'install.title': '安装 HAPI',
'install.description': '将 HAPI 添加到主屏幕以便快速访问。',
'install.button': '安装',
// Language switcher
'language.title': '语言',
'language.english': 'English',
'language.chinese': '简体中文',
// Settings page
'settings.title': '设置',
'settings.language.title': '语言',
'settings.language.label': '语言',
'settings.display.title': '显示',
'settings.display.appearance': '外观',
'settings.display.appearance.system': '跟随系统',
'settings.display.appearance.dark': '深色',
'settings.display.appearance.light': '浅色',
'settings.display.fontSize': '字体大小',
'settings.display.terminalFontSize': '终端字体大小',
'settings.display.sessionPreviewLimit': '会话折叠阈值',
'settings.display.sessionPreviewLimit.decrease': '减少折叠前显示的会话数',
'settings.display.sessionPreviewLimit.increase': '增加折叠前显示的会话数',
'settings.chat.title': '聊天',
'settings.chat.enterBehavior': '回车键行为',
'settings.chat.enterBehavior.send': '发送消息',
'settings.chat.enterBehavior.newline': '插入换行',
'settings.chat.terminalToolDisplay': '终端工具卡片',
'settings.chat.terminalToolDisplay.compact': '简洁(仅命令)',
'settings.chat.terminalToolDisplay.detailed': '详细(显示输出预览)',
'settings.chat.groupedToolBackground': '聚合 Tool Use 背景',
'settings.chat.userMessageBackground': '用户消息背景',
'settings.chat.surfaceColor.default': '默认颜色',
'settings.chat.surfaceColor.softBlue': '柔和蓝',
'settings.chat.surfaceColor.softGreen': '柔和绿',
'settings.chat.surfaceColor.softYellow': '柔和黄',
'settings.chat.surfaceColor.custom': '自定义颜色',
'settings.voice.title': '语音助手',
'settings.voice.language': '语音语言',
'settings.voice.autoDetect': '自动检测',
'settings.about.title': '关于',
'settings.about.website': '官方网站',
'settings.about.appVersion': '应用版本',
'settings.about.protocolVersion': '协议版本',
// Browse / Workspace
'browse.title': '浏览',
'browse.goUp': '返回上层',
'browse.empty': '未找到子目录',
'browse.refresh': '刷新',
'browse.startSession': '启动会话',
'browse.nav': '浏览',
'browse.noRootTitle': '未启用 workspace 浏览',
'browse.noRootHint': '浏览功能是可选的。带一个或多个 --workspace-root 参数重启 runner,即可启用文件树浏览和受限的会话启动。',
'browse.noRootFooter': '你仍然可以在「新建会话」页面直接创建会话。',
'browse.noMachinesConnected': '没有已连接的 CLI。在某台机器上运行 `hapi runner start --workspace-root /path/a --workspace-root /path/b` 来开始。',
// Misc
'misc.noMachines': '无可用机器',
'misc.machine': '机器',
'misc.collaborationMode': '协作模式',
'misc.permissionMode': '权限模式',
'misc.model': '模型',
'misc.reasoningEffort': '推理强度',
'misc.effort': '思考强度',
'misc.loading': '加载中…',
'misc.loadOlder': '加载更早的',
'misc.newMessage': '{n} 条新消息',
'misc.loadingMessages': '加载消息中…',
'misc.typeMessage': "输入 'continue' 继续...",
'misc.typeAMessage': '输入消息...',
'misc.offline': '离线',
'misc.permissionRequired': '需要权限',
'misc.percentLeft': '剩余 {percent}%',
'misc.online': '在线',
} as const