fix: translate the API copy that actually renders

Bundle translation worked, but the UI still showed Chinese. The visible text
does not come from plugin bundles at all: plugin descriptors, model and
provider names, and settings descriptions reach the browser as JSON over /api.
Measured in the Plugins view, all 76 distinct Chinese characters came from
/api/pluginManager/listBundles, which the bundle seam cannot see.

Add a second seam on the typert gateway. It resolves its own dispatchRpc per
request, so overriding that instance method intercepts every /api call without
claiming the interceptor slot the gateway already owns.

Rewriting JSON is deliberately narrow. An allowlist covers the six endpoints
that return server-authored copy; everything else, notably /api/session/list,
carries the operator's own prompts and transcripts and is left alone.

Three shapes needed handling, each found by testing against the live server:

- Locale maps ({ zh, en }) resolve to the English sibling, because the author's
  own English beats a dictionary pass.
- Bilingual descriptors joined by " · " keep their English half, which covers
  every such descriptor without a dictionary entry.
- Keys are not always trustworthy: dshmarket publishes its bilingual string
  under `en` and a short English one under `zh`, and one plugin repeats the same
  string under both. A locale map is therefore detected by shape, and an English
  sibling is only used when it is itself free of Chinese.

Also harvest author-written { zh, en } pairs from plugin locale maps (331
entries) as the highest-precedence dictionary source, and extend the curated
supplement with the vocabulary that unlocks whole families at once: "免费" alone
covers about twenty model names.

Measured on a live server: 107 -> 0 visible Chinese characters across the home,
Plugins, and Automation tasks views, 0 residual Chinese across all six
allowlisted endpoints, no page errors, and both bundle combos still compile.
This commit is contained in:
spearzificial 2026-10-15 23:44:17 +05:30
parent e3962c1591
commit 68a7fc608d
8 changed files with 1842 additions and 415 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

333
locale-pairs.json Normal file
View file

@ -0,0 +1,333 @@
{
"聊天区的动效:跟随、自动开合与卷帘门、新文字淡入、文件变更行、发送时的气泡动效、过程组标题上的计数,以及说明模型正在做什么的那一行。开启时播放本插件的这一套,关闭后聊天区交回宿主——宿主自己的说法也一并回来。「动画」设为「减弱」时一律停在原处。": "The conversation area's motion: the follow, the automatic folding with its rolling door, the text fade, the file change rows, the send flight, the counted figures on each process group's header and the line saying what the model is doing. On plays this plugin's set; off hands the conversation back to the host, its own words included. With Animation set to Reduced it all stands still.",
"此页面经远程地址连接 DSH,DSH 只保存从 localhost 等本机地址打开的页面所做的改动。这里显示的是 DSH 当前的设置,请在运行 DSH 的电脑上修改。": "This page reaches DSH through a remote address, and DSH saves settings only from a page opened at a local address such as localhost. The values shown are the ones DSH holds; change them on the computer running DSH.",
"右侧栏停靠的面板画成悬浮卡片:四周留 8px、16px 圆角、1px 描边与投影;关闭后铺满整列、贴住四边,分栏时每栏铺满各自那一栏。": "Draw the right sidebar's docked panels as floating cards: 8px inside the column, a 16px radius, a hairline and an elevation. Off fills the column and hugs its edges; a split gives each pane its own column.",
"调整左侧栏的紧凑程度。「宽松」是现在的样子;「紧凑」缩短上侧各行到工作区之间的间隙、压低对话行的高度与行间距、收紧账号区的内边距。": "Adjust how tight the sidebar is. Relaxed is how it looks now; Compact shortens the gap between the sidebar's top rows and the workspace, takes the conversation rows' height and spacing down, and trims the account area's padding.",
"面向智能体与代码任务,198B/11B 稀疏 MoE,图像与视频理解,256K 上下文,最高 400 tokens/s。": "For agentic and coding tasks: 198B/11B sparse MoE with image and video understanding, 256K context, up to 400 tokens/s.",
"替换标志旁的品牌名称,宽度按图片比例随高度换算,优先于品牌名称。PNG、SVG 或 WebP,不超过 2 MB。": "Replaces the brand name beside the mark; its width follows the image's proportions at the row's height, and it takes precedence over the brand name. PNG, SVG or WebP, up to 2 MB.",
"Apache 2.0 开放权重多模态模型,细粒度 MoE,675B 总参 / 41B 激活,256K 上下文。": "Apache-2.0 open-weight multimodal MoE with 675B total / 41B active parameters and 256K context.",
"替换侧栏顶部、折叠栏、新会话页与账号行兜底头像上的品牌标志。PNG、SVG 或 WebP,不超过 2 MB。": "Replaces the brand mark at the top of the sidebar, on the collapsed rail, on the new-conversation page and as the account row's fallback picture. PNG, SVG or WebP, up to 2 MB.",
"由于活动异常,我们已于 <strong>{time}</strong> 暂停你的账户。你的对话与数据都还在。": "We put your account on hold on <strong>{time}</strong> because of unusual activity. Your chats and data are safe.",
"生成速度 1,664 tokens/s,超过 Mercury 2 的 5 倍,MMLU-Pro 75.9%。": "Generates 1,664 tokens/s, over 5x Mercury 2, at 75.9% MMLU-Pro.",
"「原色」保留图片自己的颜色;「文字色」把图片当作形状,按所在位置的文字颜色上色,亮色暗色下都看得清。": "Original keeps the images' own colours; Text colour uses each image as a shape filled with the text colour where it stands, so it reads in both light and dark.",
"给模型菜单的每一行标出此刻的峰价、谷价或限时活动,以及距离切换还有多久。关闭后各行只显示模型名。": "Marks every row of the model menu with the peak, off-peak or promotional rate in force right now, and how long it lasts. Off leaves the rows bare.",
"按钮、选中标记、链接与焦点环使用的颜色;未指定时沿用所选品牌的主色。配色选「跟随宿主」时不生效。": "The colour of buttons, selected marks, links and focus rings. Unset, the chosen brand's own accent stays. It has no effect while Colours is set to Follow the host.",
"面向代码生成,H100 上超 1,000 tokens/s,速度最高为同类模型的 10 倍。": "Built for code, over 1,000 tokens/s on H100, up to 10x faster than speed-optimized LLMs.",
"结合实时网络信息与引用的搜索 API,从轻量 Sonar 到多步深研 Sonar Pro。": "Search APIs answering with fresh web data and citations, from light Sonar to deep-research Sonar Pro.",
"桌面端把会话标题与顶栏那一行的控件抬进窗口的标题栏,放不下的一侧留在原来的行里。": "On the desktop the session title and the header's controls rise into the window's caption row, and a side without room stays in the header's own row.",
"Micro/Lite/Pro/Premier 四档,支持文本、图像与视频输入。": "Micro, Lite, Pro and Premier tiers taking text, image and video.",
"面向编程与办公,2.4 万亿参数 MoE,1M 上下文、思考模式与内置工具。": "A 2.4T-parameter MoE for coding and office work, with 1M context, thinking mode and built-in tools.",
"分 Pro 与 Turbo 两档的新一代生产力智能体,以通用任务与代码见长。": "Pro and Turbo tiers of a next-generation productivity agent, strong at general agentic and coding tasks.",
"经 50+ 内部业务场景验证,每日千亿级 token 用量打磨,支持多模态。": "Validated across 50+ internal scenarios and hundreds of billions of daily tokens, with multimodal support.",
"面向工具调用、RAG 与多语言,Command A 为 111B/256K。": "For tool use, RAG and multilingual work; Command A is 111B with 256K context.",
"「随系统」跟随系统的动画设置;「减弱」把动画停在代表帧;「总是」始终播放。": "Follow the system keeps the system's animation setting in charge; Reduced holds animations on their still frame; Always plays them. The background-work ring turns in every setting.",
"面向复杂专业工作、金融与科研,1.1 mini 为 35B、可本地部署。": "For complex professional work, finance and science; the 35B 1.1 mini runs locally.",
"MIT 许可,314B 总参 / 13.2B 激活,256K 上下文。": "MIT-licensed, 314B total / 13.2B active, 256K context.",
"从云端到边缘的前沿模型,开放权重与商用并行,可 API 调用或自托管。": "Frontier models from cloud to edge, open-weight and commercial, via API or self-hosting.",
"面向边缘与本地部署,Phi-4 以 14B 专注推理、数学与代码。": "For edge and local use; Phi-4 (14B) targets reasoning, math and code.",
"Gemini 图像生成与编辑模型,支持自然语言对话式创建与修改。": "Gemini's image generation and editing model, with conversational creation and refinement.",
"我们会把你的对话、项目和设置打包供你下载。这可能需要一些时间。": "We’ll package up your conversations, projects, and settings for download. This might take some time to complete.",
"首个集前沿编程、1M 上下文与原生多模态于一身的开放权重模型。": "The first open-weight model combining frontier coding, 1M context and native multimodality.",
"主打智能体推理、工具调用、编程与多模态输入,面向长上下文任务。": "Agentic reasoning, tool use, coding and multimodal input for long-context work.",
"为 Codex 智能体编程优化,可并行处理多项软件工程任务。": "Optimized for agentic coding in Codex, handling several software tasks in parallel.",
"替换侧栏顶部的品牌文字;留空时显示所选品牌自己的文字标志。": "Replaces the brand text at the top of the sidebar. Leave empty to keep the chosen brand's own wordmark.",
"账户保留彩蛋页(点账户弹层顶部的用户名行进入)使用的语言。": "The language of the account-hold easter egg page (open it from the account row at the top of the sidebar footer popover). It does not follow the interface language.",
"开放权重与云端 API 并行,覆盖文本、图像、音频与视频。": "Open weights and cloud APIs across text, image, audio and video.",
"作答前深入思考,从 o1 到 o3 与 o4-mini。": "Trained to think more deeply before responding, from o1 through o3 and o4-mini.",
"面向编程与专业知识工作,原生图文视频输入,1M 上下文。": "For programming and professional knowledge work, with native text, image and video input and a 1M-token context.",
"全球最智能的模型之一,支持原生工具使用以及实时搜索功能。": "One of the most intelligent models in the world, with native tool use and real-time search.",
"你的Token用量相当于{count}本{book}。": "You've used ~{count}× the tokens in {book}.",
"速度与成本优先,面向规模化智能体任务与高频低延迟场景。": "Speed and cost first, for high-volume low-latency work and agentic tasks at scale.",
"源自 Gemini 研究,从云端服务器到手机都能运行。": "Built from the same research as Gemini, running from cloud servers to phones.",
"原生支持文本、图像与视频输入,含思考模式与工具调用。": "Native text, image and video input, with thinking mode and tool calling.",
"覆盖编程、智能体与知识工作,API 支持多模态输入。": "Coding, agentic tasks and knowledge work, with multimodal API input.",
"ERROR : 设置存储服务异常,改动不会被保存。": "The settings store is unavailable, so changes will not be saved.",
"最强开放权重编程模型之一,拓展智能上界的可及边界。": "For coding and long-horizon autonomy: 1M context, 128K output, and post-training alone makes it the most capable open-weight coding model.",
"整合大模型、搜索与自动化成一个企业级安全工作空间。": "LLMs, search and automation combined into one secure enterprise workspace.",
"新一代智能,专为解决最艰深的知识工作和编程难题。": "Next generation of intelligence for the hardest knowledge work and coding problems.",
"内置思考、可配置推理强度,面向编程与智能体任务。": "Thinking built in with configurable reasoning effort, for coding and agentic tasks.",
"覆盖对话、编程与智能体,支持思考模式与函数调用。": "Covers chat, coding and agents, with thinking mode and function calling.",
"总参大、激活参数少,单位成本低于同规模稠密模型。": "Many total parameters but few active, so it costs less than a dense model of the same size.",
"覆盖文本推理、多模态与语音,经官方开放平台提供。": "Reasoning, multimodal understanding and voice, served through the official open platform.",
"同一文件的 {count} 次改动合并为这一行": "{count} changes to the same file are merged into this row",
"允许插件修改主题配色,关闭后只保留布局和控件。": "Claude uses the skin's own colours; Follow the host leaves the colours to DSH and to other theme plugins (a wallpaper plugin, say), and the skin keeps only its layout and controls.",
"更快更省的多模态推理,兼顾长上下文与日常编程。": "Fast, low-cost multimodal inference with long context and everyday coding.",
"面向编程与知识工作的 2.8T 开放权重模型。": "An open-weight 2.8T model for coding and knowledge work.",
"开放权重 MoE,面向补全、生成与仓库级修改。": "Open-weight MoE for completion, generation and repo-level edits.",
"小而快、原生多模态推理,驱动 Meta AI。": "Small and fast by design, natively multimodal, powering Meta AI.",
"{countdown} 后转为 {next}": "in {countdown} → {next}",
"只接受 PNG、SVG 或 WebP 图片。": "Only PNG, SVG or WebP images are accepted.",
"如果你认为这次暂停是误判,可以申请账户复核。": "If you think this hold is an error, you can request an account review.",
"团队成员会把你的申请与账户活动放在一起复核。": "A team member will review your request and account activity together.",
"面向复杂编程与 AI 智能体的混合推理模型。": "A hybrid reasoning model built for serious coding and AI agents.",
"响应快、成本低,工具调用与长上下文支持完整。": "Fast and cheap, with full tool calling and long-context support.",
"面向补全、FIM 与生成,128K 上下文。": "Built for completion, FIM and generation, with 128K context.",
"该选项由插件 dsh-chat-ux 管理": "Managed by dsh-chat-ux",
"你可以永久删除账户与数据。此操作不可撤销。": "You can permanently delete your account and data. This can’t be undone.",
"面向速度敏感的高吞吐场景,响应快、成本低。": "For speed-sensitive, high-volume workloads at low cost.",
"产业级知识增强,支持中文理解与多模态生成。": "Industry-grade knowledge enhancement, with Chinese understanding and multimodal generation.",
"把侧栏工作区分成「进行中」和「已归档」。": "Split the sidebar's workspace section into In progress and Archived; archived conversations can be restored or deleted. Off restores the host's workspace list.",
"Anthropic Claude 模型。": "Anthropic Claude model.",
"GLM 5.3 flash的高吞吐版本。": "A high-throughput variant running at 200 tokens/s.",
"面向长程编程的开放权重智能体模型高速版。": "A high-speed variant at about 180 tokens/s, 5–6x the regular tier, for 2x the price.",
"面向实时智能体与高吞吐的混合推理模型。": "Hybrid reasoning for real-time agents and high-volume work.",
"OpenAI 开发的开放权重推理模型。": "Open-weight reasoning models, Apache 2.0, in 120B and 20B sizes, customizable and self-hostable.",
"扩散架构大语言模型,主打超低延迟生成。": "Diffusion-architecture LLMs built for ultra-low-latency generation.",
"面向网络安全与生物学研究的可靠模型。": "Built for cybersecurity and biology research, with access limited to vetted organizations.",
"面向复杂任务、深度推理与多模态理解。": "For complex tasks, deep reasoning and multimodal understanding.",
"GLM-5系列的首个原生多模态模型。": "Natively multimodal, with 320B/18B MoE, 1M context and MIT licensing at $0.045 per task.",
"2.4 万亿参数的原生全模态大模型。": "A native omni-modal model with 2.4 trillion parameters.",
"响应快、成本低,适合简单或高频任务。": "Fast and cheap, for simple or high-frequency tasks.",
"适合复杂或质量优先的任务,成本更高。": "For complex, quality-first work at higher cost.",
"插件提供的品牌标识和配色色系风格。": "The brand mark in the sidebar and on the home page, and its colour family: warm for Claude, blue for DeepSeek. While Colours is set to Follow the host, only the mark changes.",
"将侧栏底部的设置入口收进账户弹层。": "Fold the sidebar footer's settings entry into the account popover. Off restores the host's footer.",
"本插件提供的哪些弹层悬停时便展开。": "Which popovers open on hover. \"Account only\" keeps it to the sidebar account popover; \"All\" adds the permission, model, session-stats and home-page pickers. Off leaves every popover click-to-open.",
"面向编程与专业工作的长上下文模型。": "For coding and professional work, with a 1.05M-token context and native computer use.",
"面向长程编程的开放权重智能体模型。": "An open-weight agentic coding model for long-horizon work, 1T/32B with 256K context.",
"能力与成本均衡,完整工具调用支持。": "Balanced cost and capability with full tool calling.",
"可自由微调、蒸馏并部署到任何环境。": "Fine-tune, distill and deploy it anywhere.",
"会话仍被本应用占用,重启后再删除": "The conversation is still held open by this app; restart it, then delete again",
"用户名会显示在欢迎语和账号行里。": "The name shown in the new-conversation greeting and the account row. Leave empty to use the signed-in account name, then the HDSL launcher name, then the local system user.",
"使用插件提供的搜索框功能和逻辑。": "A search box in the sidebar that finds sessions, projects, plugins, Skills and shortcuts. Off restores the host's brand row.",
"原生理解文本、图像、视频与音频。": "Native text, image, video and audio understanding.",
"选择模型,当前 {model}": "Select model, currently {model}",
"在 GitHub 上打开本插件": "Open the plugin on GitHub",
"非对称模型结构,小成本大智能。": "Asymmetric architecture: more intelligence, less cost.",
"中文理解与智能体场景表现均衡。": "Balanced for Chinese language and agentic scenarios.",
"执行 {count} 次思考": "{count} thought",
"输出 {count} 段文字": "{count} output",
"调用 {count} 次工具": "{count} tool call",
"在一级列表展示勾选的供应商。": "Picked providers follow the official service in the picker's first level, one rule between providers. A provider removed from the catalog stays in the list marked \"Removed\"; uncheck it to clear it.",
"站在输入框上沿的像素小伙伴。": "The pixel companion on the input area's top edge, animated by what the agent is doing. Follow the brand shows the pixel crab under Claude and Deepy the whale under DeepSeek.",
"使用插件提供的权限菜单样式。": "Replace the composer's permission menu with a segmented control. Off restores the host's permission menu.",
"选择对话统计信息的展示位置。": "Choose where the conversation statistics show.",
"支持文本与图像输入、多语言。": "Text and image input, multilingual.",
"删除失败:{detail}": "Delete failed: {detail}",
"再显示 {count} 个": "Show {count} more",
"重绘对话框区域的模型菜单。": "Replace the composer's model menu with the two-level Claude-style menu. Off restores the host's model menu.",
"保存失败:{detail}": "Saving failed: {detail}",
"《哈克贝利·费恩历险记》": "Adventures of Huckleberry Finn",
"记录 {count} 条": "{count} record",
"{count} 个供应商": "{count} providers",
"重绘对话区界面的标签条。": "Redraw the Chat / Trajectory tab strip at the top of the conversation, lifted onto the title's line when it fits. Off restores the host's tab strip.",
"告诉我们当时发生了什么。": "Tell us more about what happened.",
"复核大约需要 10 天。": "Reviews take about 10 days.",
"上午 {hour} 点": "{hour} AM",
"下午 {hour} 点": "{hour} PM",
"《哈利·波特与魔法石》": "Harry Potter and the Philosopher's Stone",
"《查拉图斯特拉如是说》": "Thus Spoke Zarathustra",
"{count} 个会话": "{count} sessions",
"性能比肩顶级闭源模型。": "Performance rivaling the world's top closed-source models.",
"{count} 分钟": "{count} min",
"{count} 小时": "{count} h",
"《道林·格雷的画像》": "The Picture of Dorian Gray",
"《三体Ⅱ·黑暗森林》": "The Dark Forest",
"《三体Ⅲ·死神永生》": "Death's End",
"思考×{count}": "Thinking×{count}",
"输出×{count}": "Output×{count}",
"工具×{count}": "Tools×{count}",
"记录×{count}": "Records×{count}",
"图片超过 2 MB。": "The image is larger than 2 MB.",
"使用插件提供的字体。": "Claude uses the Anthropic faces (or the lookalike Inter and Noto Serif when they are missing) and JetBrains Mono for code; Follow the host keeps the fonts DSH or another plugin sets.",
"移动光标时平滑移动。": "The composer's caret is drawn by the plugin and glides between positions instead of jumping. Explicit moves glides only on an arrow key or a click, and lands instantly while typing.",
"我们会把结果邮件给你": "We’ll email you the outcome",
"更强、更快、更普惠。": "Smarter, faster, more affordable.",
"可本地或低成本部署。": "Cheap to serve, or run locally.",
"此刻 {rate}": "Now {rate}",
"{count} 天": "{count} d",
"《了不起的盖茨比》": "The Great Gatsby",
"《哈利·波特》全集": "the whole Harry Potter series",
"《爱丽丝漫游奇境》": "Alice's Adventures in Wonderland",
"《福尔摩斯冒险史》": "The Adventures of Sherlock Holmes",
"《安娜·卡列尼娜》": "Anna Karenina",
"《卡拉马佐夫兄弟》": "The Brothers Karamazov",
"正在加载模型目录…": "Loading providers…",
"重绘对话导航区域。": "Rest the pointer on the turn marks at the conversation's right edge and they open into a list of every turn, one line each with the message that started it; the wheel scrolls the list and a click jumps to that turn. Alt+↑ and Alt+↓ jump to the previous or the next turn, and the turn you land on is marked with a short line. Off restores the host's turn marks.",
"接下来会发生什么:": "What happens next:",
"我们会复核你的账户": "We’ll review your account",
"没有可用的模型。": "No models available.",
"没有已归档的会话": "No archived conversations",
"正在准备工具调用": "Preparing a tool call…",
"《追忆似水年华》": "In Search of Lost Time",
"《鲁滨逊漂流记》": "Robinson Crusoe",
"Token 总量": "Total tokens",
"吉祥物出现位置。": "The new-conversation page alone, or the new-conversation page and the conversation.",
"对话统计信息位置": "Conversation statistics position",
"你的账户已被暂停": "Your account is on hold",
"正在加载模型…": "Loading models…",
"没有匹配的结果": "No results",
"《傲慢与偏见》": "Pride and Prejudice",
"《战争与和平》": "War and Peace",
"《弗兰肯斯坦》": "Frankenstein",
"《平凡的世界》": "Ordinary World",
"《野性的呼唤》": "The Call of the Wild",
"《螺丝在拧紧》": "The Turn of the Screw",
"《安徒生童话》": "Andersen's Fairy Tales",
"《善恶的彼岸》": "Beyond Good and Evil",
"《格列佛游记》": "Gulliver's Travels",
"《包法利夫人》": "Madame Bovary",
"《基督山伯爵》": "The Count of Monte Cristo",
"折叠侧栏设置区": "Collapse the sidebar settings area",
"重绘对话框样式": "Which input area the skin restyles: the new-conversation page, the conversation, or both. Off restores the host's composer.",
"新会话页的排版": "The new-conversation page layout. Classic is the centered hero with the input card; Studio moves the greeting to the top left, pins the composer to the bottom and shows usage in between.",
"重绘模型选择器": "Redraw the model picker",
"选择搜索框样式": "Choose the search box style",
"工作区分栏视图": "In progress / Archived view",
"聊天区动画效果": "Chat-area animations",
"正在调用工具": "Running tools…",
"《动物农场》": "Animal Farm",
"《一九八四》": "Nineteen Eighty-Four",
"《霍比特人》": "The Hobbit",
"《圣诞颂歌》": "A Christmas Carol",
"《三国演义》": "Romance of the Three Kingdoms",
"《黄色壁纸》": "The Yellow Wallpaper",
"《化身博士》": "The Strange Case of Dr Jekyll and Mr Hyde",
"《时间机器》": "The Time Machine",
"《绿野仙踪》": "The Wonderful Wizard of Oz",
"《黑暗之心》": "Heart of Darkness",
"《伊索寓言》": "Aesop's Fables",
"《彼得·潘》": "Peter Pan",
"《世界大战》": "The War of the Worlds",
"《格林童话》": "Grimms' Fairy Tales",
"《瓦尔登湖》": "Walden",
"《远大前程》": "Great Expectations",
"《堂吉诃德》": "Don Quixote",
"《悲惨世界》": "Les Misérables",
"暂无模型数据": "No model data yet",
"悬停展开弹层": "Open popovers on hover",
"恢复品牌主色": "Use the brand's accent",
"封号彩蛋语言": "Account-hold easter egg language",
"重绘权限控件": "Redraw the permission control",
"面板悬浮卡片": "Float docked panels",
"对话统计信息": "Conversation statistics",
"标题进入顶栏": "Header in the title bar",
"提交复核申请": "Request a review",
"你可以做的事": "What you can do",
"导出你的数据": "Export your data",
"删除你的账户": "Delete your account",
"正在加载…": "Loading…",
"键盘快捷键": "Keyboard shortcuts",
"正在搜索…": "Searching…",
"正在写回答": "Writing…",
"《道德经》": "Tao Te Ching",
"《德古拉》": "Dracula",
"《简·爱》": "Jane Eyre",
"《西游记》": "Journey to the West",
"《红楼梦》": "Dream of the Red Chamber",
"《水浒传》": "Water Margin",
"《变形记》": "Metamorphosis",
"《浮士德》": "Faust",
"《君主论》": "The Prince",
"《隐形人》": "The Invisible Man",
"《沉思录》": "Meditations",
"《奥德赛》": "The Odyssey",
"《双城记》": "A Tale of Two Cities",
"《理想国》": "The Republic",
"《罪与罚》": "Crime and Punishment",
"《利维坦》": "Leviathan",
"最常用模型": "Favorite model",
"重绘对话框": "Composer restyle",
"快捷供应商": "Quick providers",
"首页与对话": "Home and conversation",
"搜索框样式": "Search box style",
"左侧栏布局": "Sidebar layout",
"收进上下文": "Inside the context popover",
"重绘标签条": "Chat / Trajectory tabs",
"选择模型": "Select model",
"推理等级": "Reasoning effort",
"更多模型": "More models",
"删除会话": "Delete conversation",
"取消归档": "Unarchive conversation",
"切换分类": "Filters",
"正在思考": "Thinking…",
"30 天": "30d",
"高峰时段": "Peak hour",
"《白鲸》": "Moby-Dick",
"《魔戒》": "The Lord of the Rings",
"《活着》": "To Live",
"《三体》": "The Three-Body Problem",
"《白牙》": "White Fang",
"《神曲》": "The Divine Comedy",
"活跃天数": "Active days",
"暂未响应": "No response yet",
"主题风格": "Theme style",
"仅账号区": "Account only",
"首页版面": "Home layout",
"峰谷电表": "Peak rate meter",
"自动获取": "Auto-detect account or host user",
"品牌名称": "Brand name",
"所选品牌": "The chosen brand",
"标志图片": "Brand mark",
"文字标志": "Wordmark",
"图片颜色": "Image colours",
"跟随品牌": "Brand's own",
"动画效果": "Animation",
"出现位置": "Where it appears",
"侧栏搜索": "Sidebar search",
"覆盖品牌": "Cover the brand",
"对话导航": "Conversation navigator",
"独立成行": "On its own line",
"光标动画": "Composer caret motion",
"退出登录": "Sign out",
"申请复核": "Request a review",
"活动价": "Promotion",
"进行中": "Active",
"已归档": "Archived",
"快捷键": "Shortcuts",
"新会话": "New session",
"7 天": "7d",
"消息数": "Messages",
"会话数": "Sessions",
"总用时": "Total time",
"工作台": "Studio",
"未选择": "None",
"已移除": "Removed",
"用户名": "Username",
"文字色": "Text colour",
"随系统": "Follow the system",
"仅首页": "Home only",
"仅对话": "Conversation only",
"输入区": "Composer",
"吉祥物": "Mascot",
"更快": "Faster",
"更强": "Smarter",
"峰价": "Peak rate",
"谷价": "Off-peak rate",
"刚刚": "Just now",
"搜索": "Search",
"关闭": "Close",
"打开": "Open",
"全部": "All",
"会话": "Sessions",
"项目": "Projects",
"插件": "Plugins",
"最近": "Recents",
"操作": "Actions",
"设置": "Settings",
"概览": "Overview",
"模型": "Models",
"收起": "Show less",
"用户": "User",
"权限": "Permission",
"经典": "Classic",
"上传": "Upload",
"更换": "Replace",
"移除": "Remove",
"原色": "Original",
"主色": "Accent colour",
"减弱": "Reduced",
"总是": "Always",
"通用": "General",
"外观": "Appearance",
"侧栏": "Sidebar",
"对话": "Conversation",
"配色": "Colours",
"开启": "Claude",
"字体": "Typefaces",
"自动": "Follow the brand",
"首页": "Home",
"图标": "Icon",
"独立": "Standalone",
"宽松": "Relaxed",
"紧凑": "Compact"
}

View file

@ -32,6 +32,15 @@ if (existsSync(extraPath)) {
for (const [zh, en] of Object.entries(data)) pairs.set(zh, en); for (const [zh, en] of Object.entries(data)) pairs.set(zh, en);
} }
// Author-authored `{ zh, en }` pairs lifted from plugin locale maps. This is the
// highest-quality source: a human wrote both sides, so it beats every
// machine-harvested pair and is merged last.
const localePairsPath = join(root, "locale-pairs.json");
if (existsSync(localePairsPath)) {
const data = JSON.parse(readFileSync(localePairsPath, "utf8"));
for (const [zh, en] of Object.entries(data)) pairs.set(zh, en);
}
// A handful of upstream entries are Malay, not English. Emitting them would // A handful of upstream entries are Malay, not English. Emitting them would
// inject foreign words into an English UI, so they are replaced by name. // inject foreign words into an English UI, so they are replaced by name.
const NON_ENGLISH = new Map([ const NON_ENGLISH = new Map([

View file

@ -1,402 +1,467 @@
{ {
"输出": "Output", "Our Free Model — 在 dsh 里装上这个插件即可,无需登录、注册或填 API Key,就能使用包括 DeepSeek V4.1 Flash、Kimi K3 在内的前沿模型,桌面端另有 EAC 协付渠道;2.0 起接入 CodeArts、CodeBuddy、WorkBuddy、LobsterAI、Qoder、TRAE、Cline、Loomy、Raccoon、MiniMax、ZCode、Gemini 十三个账号渠道(原有匿名免费通道保留)(登录、账号池、积分领取、本地 OpenAI 网关与局域网转发、数据看板与运行日志),六页毛玻璃界面统一呈现。": "Our Free Model — install this plugin in dsh and use frontier models including DeepSeek V4.1 Flash and Kimi K3 with no login, registration, or API key. The desktop app adds the EAC co-payment channel; from 2.0 it connects thirteen account channels (CodeArts, CodeBuddy, WorkBuddy, LobsterAI, Qoder, TRAE, Cline, Loomy, Raccoon, MiniMax, ZCode, and Gemini) while keeping the original anonymous free channel (login, account pool, credit claiming, a local OpenAI gateway and LAN forwarding, a dashboard, and a runtime log), all presented across six frosted-glass pages.",
"运行中": "Running", "DSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件:余额/今日已用/峰谷定价、自定义泡泡点击序列(文本/余额/今日/峰谷/图片/随机语句与并列加权选择)、逐行样式与字体、悬浮快捷编辑、音效与每轮消耗、自定义角色/动图/音效、吸附与翻转自定义": "A DeepSeek balance whale widget in the bottom-right of the DSH Web interface: balance, today's usage, and peak/off-peak pricing; a customisable bubble click sequence (text, balance, today, peak/off-peak, image, random lines with weighted parallel choice); per-row styling and fonts; hover quick-edit; sound effects and per-turn usage; custom characters, animations, and audio; snapping and flip customisation.",
"输入": "Input", "web_fetch 由谁抓取:providers 用填了 Key 的引擎(都没有或都失败时退回 dsh 自带的本地抓取);dsh 直接用 dsh 自带的本地抓取(免费,不经第三方,但不渲染 JS)。": "Who performs web_fetch: providers uses the engines with a key configured (falling back to dsh's own local fetching when none is set or all fail); dsh uses dsh's own local fetching directly (free, no third party, but it does not render JavaScript).",
"失败": "Failed", "Our Free Model — 在 dsh 里装上这个插件即可,无需登录、注册或填 API Key,就能使用包括 DeepSeek V4.1 Flash、Kimi K3 在内的前沿模型,桌": "Our Free Model — install this plugin in dsh and use frontier models including DeepSeek V4.1 Flash and Kimi K3 with no login, registration, or API key, on the desktop",
"移除": "Remove", "为 DeepSeek Harness 网页界面复刻 Claude Code Desktop 的主题插件——象牙白与暖黑双画布、陶烬橙强调色、Claude 风格模型选择器、权限分段与账户抽屉。": "A theme plugin that recreates Claude Code Desktop for the DeepSeek Harness web interface: ivory and warm-black canvases, ember-orange accents, a Claude-style model picker, permission segments, and an account drawer.",
"恢复默认": "Restore Defaults", "在 DeepSeek Harness 内直连免密免费模型:清单跟随上游更新、地区可用性自动探测、思考强度档位真实生效,并附 Token 用量看板与 OpenAI 兼容的本地转发端口。": "Connect directly to key-free free models inside DeepSeek Harness: the catalog follows upstream updates, regional availability is probed automatically, reasoning effort takes real effect, and it includes a token usage dashboard plus an OpenAI-compatible local forwarding port.",
"缓存命中": "Cache Hit", "是否允许模型调用 web_fetch(URL 内容抓取)。web_fetch 工具由 dsh 统一挂载,关闭后调用会返回明确的错误提示,而不是从工具表移除;切换即时生效,无需重启。": "Whether the model may call web_fetch (URL content fetching). The web_fetch tool is mounted by dsh itself; turning it off returns a clear error instead of removing it from the tool list. The change takes effect immediately, with no restart.",
"已删除": "Deleted", "搜索策略:fallback 逐个尝试(最省额度);race 同时调用前 N 个引擎、取最快的;merge 同时调用前 N 个引擎并合并去重(召回最好,消耗 N 倍额度)。": "Search strategy: fallback tries each engine in turn (cheapest on quota); race calls the first N engines at once and takes the fastest; merge calls the first N engines at once and combines and deduplicates the results (best recall, at N times the quota).",
"推理": "Reasoning", "定义 Provider 的调用顺序。排在前面的服务会优先执行,如果请求失败(或额度用尽),会自动按照该顺序 fallback 到下一个可用服务。": "Define the order in which providers are called. Services earlier in the list run first; if a request fails or the quota runs out, it falls back to the next available service in that order.",
"文档渲染中...": "Rendering document...", "所有带 Key 的抓取都失败时,用不带 Key 的 Jina Reader 兜底(有速率限制)。没配抓取引擎时它就是唯一的抓取通道。": "When every keyed fetch fails, fall back to the keyless Jina Reader (rate limited). With no fetch engine configured it is the only fetch channel.",
"进行中": "In progress",
"时间": "Time",
"缓存写入": "Cache Write",
"账号": "Account",
"成本": "Cost",
"自动换行": "Word Wrap",
"图片": "Image",
"已关闭": "Closed",
"结果": "Result",
"思考": "Thinking",
"秒": "sec",
"未知模型": "Unknown model",
"用量统计": "Usage Statistics",
"打开": "Open",
"文件": "File",
"未运行": "Not running",
"系统提示词": "System Prompt",
"对话": "Conversation",
"小时": "hour",
"刚刚": "Just now",
"计划": "Plan",
"已覆盖": "Overridden",
"本部署的设置为只读。": "This deployment's settings are read-only.",
"该插件当前未加载,暂时无法配置。": "This plugin is not loaded, so it cannot be configured yet.",
"本部署没有接受这些值,已保留供你修改。": "This deployment did not accept these values; they were preserved for you to edit.",
"工具": "Tool",
"浏览器窗口登录": "Browser window login",
"(未知)": "(unknown)",
"{count} 个技能": "{count} skills",
"总处理": "Total processed",
"未知工作区": "Unknown workspace",
"关闭成本设置": "Close cost settings",
"AI 图像工坊": "AI Image Studio",
"终端": "Terminal",
"去充值": "Add credit",
"跳过": "Skip",
"复制失败": "Copy failed",
"退出登录": "Sign out",
"无": "None",
"脚注": "Footnote",
"请先选择会话": "Select a session first",
"加载更多": "Load more",
"代码": "Code",
"没有匹配的模型。": "No matching models.",
"插件": "Plugin",
"加载中": "Loading",
"正在检查…": "Checking…",
"无输出": "No output",
"收起输出": "Collapse output",
"退出码 {code}": "Exit code {code}",
"信号 {signal}": "Signal {signal}",
"未正常退出": "Exited abnormally",
"插话发送": "Send interjection",
"周一": "Monday",
"周二": "Tuesday",
"周三": "Wednesday",
"周四": "Thursday",
"周五": "Friday",
"周六": "Saturday",
"周日": "Sunday",
"消息": "Message",
"{count} 个后台任务": "{count} background tasks",
"已中断": "Interrupted",
"已停止": "Stopped",
"查看": "View",
"工具调用": "Tool call",
"网页搜索": "Web search",
"… 其余 {count} 行": "… {count} more lines",
"拒绝": "Deny",
"自动化任务": "Automated tasks",
"{seconds}秒": "{seconds}s",
"{minutes}分{seconds}秒": "{minutes}m {seconds}s",
"工具已更新": "Tools updated",
"指令": "Instructions",
"计划待审": "Plan awaiting review",
"子智能体": "Subagent",
"目标": "Goal",
"反馈": "Feedback",
"压缩": "Compact",
"权限": "Permissions",
"请填数字;留空表示使用默认值。": "Enter a number; leave blank to use the default.",
"当前不可用": "Currently unavailable",
"调用": "Call",
"账号库": "Account pool",
"切换中……": "Switching…",
"登录态探活": "Login health check",
"近 7 天": "Last 7 days",
"近 30 天": "Last 30 days",
"输出 Token": "Output tokens",
"(已写入配置,重启后仍生效)": "(Written to config; persists after restart)",
"分钟": "minute",
"时区": "Time zone",
"每周": "Weekly",
"检查中…": "Checking…",
"测试中…": "Testing…",
"全部模型": "All models",
"速度": "Speed",
"测试": "Test",
"输入 / 1M": "Input / 1M",
"输出 / 1M": "Output / 1M",
"缓存读 / 1M": "Cache read / 1M",
"缓存写 / 1M": "Cache write / 1M",
"(已删除)": "(deleted)",
"缓存命中率": "Cache hit rate",
"未启用": "Disabled",
" 天": " days",
"回合": "Turn",
"首字": "First token",
"已保存": "Saved",
"保存失败": "Save failed",
"下一步": "Next",
"继续": "Continue",
"上一步": "Back",
"登录": "Sign in",
"加载失败": "Load failed",
"提交": "Submit",
"已截断": "Truncated",
"… 已截断,共 {total} 字符": "… truncated, {total} characters total",
"清空搜索": "Clear search",
"打开侧边栏": "Open sidebar",
"收起侧边栏": "Collapse sidebar",
"已达两格上限": "Two-pane limit reached",
"栏宽不足,拖宽侧边栏后再分栏": "Not enough width; widen the sidebar before splitting",
"全屏": "Full screen",
"自动刷新": "Auto refresh",
"开启自动刷新": "Enable auto refresh",
"关闭自动刷新": "Disable auto refresh",
"纯文本": "Plain text",
"该格式文件暂时无法预览": "This file format cannot be previewed yet",
"读取失败:{message}": "Read failed: {message}",
"正在读取…": "Reading…",
"检查更新失败,请稍后重试。": "Update check failed; try again later.",
"DeepSeek 账号": "DeepSeek account",
"删除 {provider}": "Remove {provider}",
"提供商": "Provider",
"显示名称": "Display name",
"上下文窗口": "Context window",
"使用提供商默认值": "Use provider default",
"正在读取插件…": "Reading plugins…",
"添加插件": "Add plugin",
"异常": "Error",
"包元信息错误:{error}": "Package metadata error: {error}",
"等待依赖": "Waiting for dependencies",
"查看 {name}": "View {name}",
"插件列表": "Plugin list",
"未找到相关插件": "No matching plugins",
"展开其余 {n} 行输出": "Show {n} more output lines",
"… 其余 {n} 行": "… {n} more lines",
"完成": "Done",
"启动失败": "Start failed",
"发送消息": "Send message",
"排队发送": "Queued to send",
"待处理": "Pending",
"新增": "Added",
"单次": "Once",
"重复": "Repeat",
"搜索会话": "Search sessions",
"空闲": "Idle",
"提供方": "Provider",
"内容": "Content",
"层级": "Level",
"任务 ID": "Task ID",
"/{command} 不接受附件,请先移除附件": "/{command} does not accept attachments; remove them first",
"等待回答": "Awaiting answer",
"已继续工作,仍可回答": "Work resumed; still answerable",
"已结束": "Ended",
"运行命令": "Run command",
"运行 Cordis 插件": "Run Cordis plugin",
"停止 Cordis 插件": "Stop Cordis plugin",
"移除 Cordis 插件": "Remove Cordis plugin",
"取消编辑": "Cancel edit",
"当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。": "This session is already in use, possibly by another running DSH instance (such as another dsh web or the desktop app). Quit the other DSH instances and try again.", "当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。": "This session is already in use, possibly by another running DSH instance (such as another dsh web or the desktop app). Quit the other DSH instances and try again.",
"等待审批": "Awaiting approval", "同一模型,thinking 开启:先推理再作答(推理流作为思考块回传)。适合数学/多步调试/规划,更慢也更耗额度": "Same model with thinking on: reasons before answering, with the reasoning stream returned as a thinking block. Suits math, multi-step debugging, and planning; slower and heavier on quota.",
"图片预览": "Image preview", "相同的搜索或抓取在这么多分钟内直接返回缓存,不再消耗额度。0 为关闭。缓存只在内存里,重启即清空。": "Return cached results for identical searches or fetches within this many minutes, without spending quota. 0 disables it. The cache lives in memory only and clears on restart.",
"第 {turn} 轮": "Turn {turn}", "同一模型,thinking 关闭:直接作答、最快、最省免费额度。适合工具调用/改写/检索类任务": "Same model with thinking off: answers directly, fastest, and cheapest on free quota. Suits tool calls, rewriting, and retrieval tasks.",
"简洁": "Concise", "Tavily 搜索深度:advanced 结果更相关,但每次消耗 2 credits。": "Tavily search depth: advanced returns more relevant results but costs 2 credits per call.",
"详细": "Detailed", "保存自动化任务,到点后在原会话中执行,并为智能体提供管理任务的工具。": "Save automated tasks, run them in the original session when due, and give agents a tool to manage them.",
"标准": "Standard", "每条结果的摘要长度(字符)。越长上下文越多,也越费模型 token。": "Summary length per result, in characters. Longer means more context and more model tokens.",
"已添加工具:{name}": "Tool added: {name}", "结果地区(ISO 3166 两位代码)。auto 使用各引擎默认值。": "Result region (a two-letter ISO 3166 code). auto uses each engine's default.",
"已移除工具:{name}": "Tool removed: {name}", "在本机使用 SenseVoice 转写录音,首次使用需安装依赖。": "Transcribe recordings locally with SenseVoice; install its dependencies on first use.",
"新增:{names}": "Added: {names}", "提供自动审查权限模式,由模型在每次工具调用前判断是否授权。": "Provide an automatic approval permission mode where the model decides whether to authorize each tool call.",
"新增 {count} 个": "Added {count}", "每一步把当前时间、浏览器时区和距上一条消息的时长告诉模型。": "Tell the model the current time, the browser time zone, and the time since the last message on every step.",
"Codex 风格侧栏、工作区会话树、全局搜索和轮次导航。": "A Codex-style sidebar, workspace session tree, global search, and turn navigation.",
"优先的域名,每行一个:结果中来自这些域名的条目排到前面。": "Preferred domains, one per line: results from these domains are ranked first.",
"侧栏的自动化任务页面,以及会话里的任务卡片和提醒标记。": "The Automation tasks page in the sidebar, plus task cards and reminder markers in sessions.",
"新增 {added} 个,移除 {removed} 个": "Added {added}, removed {removed}", "新增 {added} 个,移除 {removed} 个": "Added {added}, removed {removed}",
"移除:{names}": "Removed: {names}", "为智能体提供成员协调、消息通信和共享任务管理工具。": "Provides agents with member coordination, messaging, and shared task management tools.",
"移除 {count} 个": "Removed {count}", "统一加载并安全管理本机 Agent Skills。": "Load and safely manage local Agent Skills.",
"已移除": "Removed", "race / merge 策略同时调用的引擎数。": "How many engines the race / merge strategy calls at once.",
"上下文已压缩": "Context compacted", "启用团队协作、团队工具、成员列表和共享任务看板。": "Enable team collaboration, team tools, a member list, and a shared task board.",
"{count} 次工具调用": "{count} tool calls", "/{command} 不接受附件,请先移除附件": "/{command} does not accept attachments; remove them first",
"{count} 条消息": "{count} messages", "开启思考(网页只区分开/关,等同 High)": "Thinking on (the web UI only distinguishes on/off; equivalent to High)",
"{count} 个 subagent": "{count} subagents",
"API 密钥无效": "Invalid API key",
"缓存读取": "Cache read",
"分": "min",
"执行中…": "Running…",
"{y}年{m}月{d}日": "{y}-{m}-{d}",
"待审批": "Pending approval",
"Cordis 插件": "Cordis plugin",
"读取中…": "Reading…",
"归档会话": "Archive session",
"取消归档": "Unarchive",
"置顶会话": "Pin session",
"取消置顶": "Unpin",
"{n} 个运行中的子智能体:{names}": "{n} running subagents: {names}", "{n} 个运行中的子智能体:{names}": "{n} running subagents: {names}",
"按设定的时间或周期,在会话中自动执行任务。": "Run tasks automatically in a session at a set time or interval.",
"查看团队成员和共享任务,并切换到成员会话。": "View team members and shared tasks, and switch to a member's session.",
"默认时间范围。只对支持时间过滤的引擎生效。": "Default time range. Applies only to engines that support time filtering.",
"(未能写入配置,重启后会回到上次保存的值)": "(Could not write to config; it will revert to the last saved value after restart)",
"{count} 个文件 · {size}": "{count} files · {size}",
"{minutes}分{seconds}秒": "{minutes}m {seconds}s",
"屏蔽的域名,每行一个,子域名一并屏蔽。": "Blocked domains, one per line. Subdomains are blocked as well.",
"本部署没有接受这些值,已保留供你修改。": "This deployment did not accept these values; they were preserved for you to edit.",
"结果语言。auto 使用各引擎默认值。": "Result language. auto uses each engine's default.",
"{count} 个 subagent": "{count} subagents",
"{count} 个子智能体,正在运行": "{count} subagents, running",
"… 已截断,共 {total} 字符": "… truncated, {total} characters total",
"每小时的第 {minutes} 分钟": "At minute {minutes} of every hour",
"{n} 个后台任务:{names}": "{n} background tasks: {names}", "{n} 个后台任务:{names}": "{n} background tasks: {names}",
"{n} 条定时提醒:{names}": "{n} scheduled reminders: {names}", "{n} 条定时提醒:{names}": "{n} scheduled reminders: {names}",
"{n} 项其他工作({kind})": "{n} other jobs ({kind})", "{n} 项其他工作({kind})": "{n} other jobs ({kind})",
"撤销": "Undo", "管理团队成员、成员消息和共享任务。": "Manage team members, member messages, and shared tasks.",
"{n} 个会话": "{n} sessions", "{count} 个后台任务运行中": "{count} background tasks running",
"该插件当前未加载,暂时无法配置。": "This plugin is not loaded, so it cannot be configured yet.",
"当前进程令牌不可用,请刷新看板": "The current process token is unavailable; refresh the dashboard",
"请填数字;留空表示使用默认值。": "Enter a number; leave blank to use the default.",
"Claude Code 风格": "Claude Code style",
"… 其余 {count} 行": "… {count} more lines",
"保存失败:{message}": "Save failed: {message}",
"全部 {count} 个文件": "All {count} files",
"包元信息错误:{error}": "Package metadata error: {error}",
"栏宽不足,拖宽侧边栏后再分栏": "Not enough width; widen the sidebar before splitting",
"用首条消息生成规范会话标题。": "Generate a canonical session title from the first message.",
"读取失败:{message}": "Read failed: {message}",
"(已写入配置,重启后仍生效)": "(Written to config; persists after restart)",
"{count} 个后台任务": "{count} background tasks",
"{count} 个子智能体": "{count} subagents",
"{count} 个工具调用": "{count} tool calls",
"{count} 个诊断异常": "{count} diagnostics issues",
"{count} 次工具调用": "{count} tool calls",
"删除 {provider}": "Remove {provider}",
"在侧边栏打开 {name}": "Open {name} in the sidebar",
"当前工作区或本地应用不可用": "The current workspace or local application is unavailable",
"检查更新失败,请稍后重试。": "Update check failed; try again later.",
"请输入至少 1 分钟的间隔": "Enter an interval of at least 1 minute",
"{count} 个已停用": "{count} disabled",
"{count} 个已启用": "{count} enabled",
"{count} 个被覆盖": "{count} overridden",
"{n} 个子智能体运行中": "{n} subagents running", "{n} 个子智能体运行中": "{n} subagents running",
"{y}年{m}月{d}日": "{y}-{m}-{d}",
"停止 Cordis 插件": "Stop Cordis plugin",
"展开其余 {n} 行输出": "Show {n} more output lines",
"已添加工具:{name}": "Tool added: {name}",
"已移除工具:{name}": "Tool removed: {name}",
"我已了解风险,并愿意继续": "I understand the risk and want to continue",
"新增 {count} 个": "Added {count}",
"无法显示文件位置,请重试": "Could not show the file location; try again",
"每 {count} 小时": "Every {count} hours",
"移除 Cordis 插件": "Remove Cordis plugin",
"移除 {count} 个": "Removed {count}",
"运行 Cordis 插件": "Run Cordis plugin",
"DeepSeek 账号": "DeepSeek account",
"{count} 个成员": "{count} members",
"{count} 个技能": "{count} skills",
"{count} 个提醒": "{count} reminders",
"{count} 个步骤": "{count} steps",
"{count} 条消息": "{count} messages",
"信号 {signal}": "Signal {signal}",
"每 {step} 分钟": "Every {step} minutes",
"统一加载并安全管理本机": "Load and safely manage local",
"该格式文件暂时无法预览": "This file format cannot be previewed yet",
"{seconds}秒": "{seconds}s",
"… 其余 {n} 行": "… {n} more lines",
"完整费率设置加载失败": "Failed to load the full rate settings",
"已继续工作,仍可回答": "Work resumed; still answerable",
"新增:{names}": "Added: {names}",
"本部署的设置为只读。": "This deployment's settings are read-only.",
"正在加载更早的历史…": "Loading earlier history…",
"用 {app} 打开": "Open with {app}",
"移除:{names}": "Removed: {names}",
"第 {turn} 轮": "Turn {turn}",
"退出码 {code}": "Exit code {code}",
"(未获取到账号信息)": "(no account information available)",
"### 什么时候选": "### When to choose",
"Cordis 插件": "Cordis plugin",
"{app}(默认)": "{app} (default)",
"已捕获并保存凭证。": "Credentials captured and saved.",
"总处理 Token": "Total processed tokens",
"查看 {name}": "View {name}",
"确认启用完全权限?": "Enable full access?",
"退出并登录其它账号": "Sign out and sign in to another account",
"API 密钥无效": "Invalid API key",
"使用提供商默认值": "Use provider default",
"允许并行调用工具": "Allow parallel tool calls",
"打开失败,请重试": "Open failed; try again",
"推理 Token": "Reasoning tokens",
"文档渲染中...": "Rendering document...",
"无法获取应用列表": "Could not retrieve the application list",
"没有匹配的模型。": "No matching models.",
"缓存写 / 1M": "Cache write / 1M",
"缓存读 / 1M": "Cache read / 1M",
"自动(引擎默认)": "Auto (engine default)",
"输入 Token": "Input tokens",
"输出 Token": "Output tokens",
"重开链时换新会话": "Start a new session when reopening the chain",
"AI 图像工坊": "AI Image Studio",
"{n} 个会话": "{n} sessions",
"到期前自动重登": "Auto re-login before expiry",
"参数 JSON": "Parameters JSON",
"复制 HTML": "Copy HTML",
"有未保存的修改": "Unsaved changes",
"未找到相关插件": "No matching plugins",
"正在加载模型…": "Loading models…",
"正在打开工作区": "Opening workspace",
"正在生成图片…": "Generating image…",
"正在读取插件…": "Reading plugins…",
"浏览器窗口登录": "Browser window login",
"省 Token": "Save tokens",
"结果 JSON": "Result JSON",
"自定义时间范围": "Custom time range",
"输入 / 1M": "Input / 1M",
"输出 / 1M": "Output / 1M",
"EAC 模型": "EAC models",
"上下文已压缩": "Context compacted",
"关闭成本设置": "Close cost settings",
"关闭用量统计": "Close usage statistics",
"关闭自动刷新": "Disable auto refresh",
"删除费率档位": "Delete rate tier",
"刷新统计数据": "Refresh statistics",
"启用完全权限": "Enable full access",
"工作区内修改": "Modify within workspace",
"已达两格上限": "Two-pane limit reached",
"开启自动刷新": "Enable auto refresh",
"成本统计设置": "Cost statistics settings",
"显示文件位置": "Show file location",
"更多打开方式": "More ways to open",
"白嫖模型接入": "Free model access",
"网关暂不可达": "Gateway unreachable",
"自动授权审查": "Automatic approval review",
"请先选择会话": "Select a session first",
"近 30 天": "Last 30 days",
"近 90 天": "Last 90 days",
"退出当前账号": "Sign out of this account",
"{n}个月": "{n} months",
"{n}分钟": "{n} minutes", "{n}分钟": "{n} minutes",
"{n}小时": "{n} hours", "{n}小时": "{n} hours",
"{n}天": "{n} days",
"{n}个月": "{n} months",
"{n}年": "{n} years",
"{count} 个成员": "{count} members",
"进入目录": "Enter directory",
"添加": "Add",
"{count} 个子智能体": "{count} subagents",
"{count} 个子智能体,正在运行": "{count} subagents, running",
"{count} 个后台任务运行中": "{count} background tasks running",
"已失败": "Failed",
"取消标记": "Unmark",
"请选择模型": "Select a model",
"正在加载模型…": "Loading models…",
"仅可查看": "View only",
"工作区内修改": "Modify within workspace",
"完全权限": "Full access",
"确认启用完全权限?": "Enable full access?",
"我已了解风险,并愿意继续": "I understand the risk and want to continue",
"启用完全权限": "Enable full access",
"### 什么时候选": "### When to choose",
"内置插件": "Built-in plugins",
"下一题": "Next question",
"时长": "Duration",
"用户": "User",
"已压缩": "Compacted",
"助手": "Assistant",
"开始时间": "Start time",
"参数": "Parameters",
"用量": "Usage",
"无内容": "No content",
"结果 JSON": "Result JSON",
"参数 JSON": "Parameters JSON",
"正在加载更早的历史…": "Loading earlier history…",
"{count} 个工具调用": "{count} tool calls",
"{count} 个步骤": "{count} steps",
"关闭详情": "Close details",
"已解除": "Cleared",
"登录状态": "Login status",
"⚪ 未登录": "⚪ Not signed in", "⚪ 未登录": "⚪ Not signed in",
"退出当前账号": "Sign out of this account", "上下文窗口": "Context window",
"退出并登录其它账号": "Sign out and sign in to another account", "任务 ID": "Task ID",
"折叠": "Collapse", "供应商筛选": "Provider filter",
"未知原因": "Unknown reason", "全部供应商": "All providers",
"已捕获并保存凭证。": "Credentials captured and saved.", "全部工作区": "All workspaces",
"连通性测试": "Connectivity test", "切换中……": "Switching…",
"(未获取到账号信息)": "(no account information available)", "在本地打开": "Open locally",
"凭证来源": "Credential source", "复制提示词": "Copy prompt",
"指纹清理": "Fingerprint cleanup",
"允许并发": "Allow concurrency",
"间隔下限": "Minimum interval",
"间隔上限": "Maximum interval",
"快捷预设": "Quick presets",
"允许并行调用工具": "Allow parallel tool calls",
"到期前自动重登": "Auto re-login before expiry",
"调用台账": "Call ledger",
"调用次数": "Call count",
"按账号": "By account",
"总计": "Total",
"输入 Token": "Input tokens",
"按模型": "By model",
"重开链时换新会话": "Start a new session when reopening the chain",
"(未能写入配置,重启后会回到上次保存的值)": "(Could not write to config; it will revert to the last saved value after restart)",
"每分钟": "Every minute",
"每 {step} 分钟": "Every {step} minutes",
"每小时": "Every hour",
"每 {count} 小时": "Every {count} hours",
"每小时的第 {minutes} 分钟": "At minute {minutes} of every hour",
"一": "1",
"二": "2",
"三": "3",
"四": "4",
"五": "5",
"六": "6",
"日": "Day",
"天": "Day",
"正在删除…": "Deleting…",
"{count} 个提醒": "{count} reminders",
"下次运行": "Next run",
"仅一次": "Once only",
"日期": "Date",
"请输入至少 1 分钟的间隔": "Enter an interval of at least 1 minute",
"有未保存的修改": "Unsaved changes",
"保存修改": "Save changes",
"每天": "Daily",
"复制指令": "Copy instructions",
"未配置": "Not configured",
"自动(引擎默认)": "Auto (engine default)",
"时间范围": "Time range",
"保存失败:{message}": "Save failed: {message}",
"查询中…": "Querying…",
"添加账号": "Add account",
"正在生成图片…": "Generating image…",
"在侧边栏打开 {name}": "Open {name} in the sidebar",
"全部 {count} 个文件": "All {count} files",
"本机": "This machine",
"确认移除": "Confirm removal",
"处理中…": "Processing…",
"范围": "Range",
"位置": "Location",
"知道了": "Got it",
"正在更新…": "Updating…",
"运行版本": "Running version",
"{count} 个已启用": "{count} enabled",
"{count} 个已停用": "{count} disabled",
"{count} 个被覆盖": "{count} overridden",
"{count} 个诊断异常": "{count} diagnostics issues",
"{count} 个文件 · {size}": "{count} files · {size}",
"未计价": "Not priced",
"月": "Month",
"少": "Less",
"多": "More",
"请求日志": "Request log",
"同步中…": "Syncing…",
"未知供应商": "Unknown provider",
"混合查看": "Combined view",
"峰谷计划 ": "Peak/off-peak plan", "峰谷计划 ": "Peak/off-peak plan",
"工作区筛选": "Workspace filter",
"工具已更新": "Tools updated",
"当前不可用": "Currently unavailable",
"手机号登录": "Phone number login",
"打开侧边栏": "Open sidebar",
"收起侧边栏": "Collapse sidebar",
"智能体团队": "Agent teams",
"未正常退出": "Exited abnormally",
"未知供应商": "Unknown provider",
"未知工作区": "Unknown workspace",
"正在删除…": "Deleting…",
"正在更新…": "Updating…",
"正在检查…": "Checking…",
"正在读取…": "Reading…",
"登录态探活": "Login health check",
"系统提示词": "System Prompt",
"缓存命中率": "Cache hit rate",
"自动化任务": "Automated tasks",
"自定义规则": "Custom rules", "自定义规则": "Custom rules",
"请选择模型": "Select a model",
"近 7 天": "Last 7 days",
"连通性测试": "Connectivity test",
"(已删除)": "(deleted)",
"{n}天": "{n} days",
"{n}年": "{n} years",
"下次运行": "Next run",
"下载图像": "Download image",
"仅可查看": "View only",
"任务界面": "Task UI",
"任务调度": "Task scheduling",
"会话标题": "Session title",
"保存修改": "Save changes",
"保存失败": "Save failed",
"允许并发": "Allow concurrency",
"免费模型": "Free models",
"免费额度": "Free quota",
"全部模型": "All models",
"关闭思考": "Thinking off",
"关闭详情": "Close details",
"内置插件": "Built-in plugins",
"凭证来源": "Credential source",
"删除时段": "Delete period", "删除时段": "Delete period",
"删除规则": "Delete rule", "删除规则": "Delete rule",
"当前进程令牌不可用,请刷新看板": "The current process token is unavailable; refresh the dashboard", "加载失败": "Load failed",
"删除费率档位": "Delete rate tier", "加载更多": "Load more",
"输入口径": "Input basis", "发送消息": "Send message",
"成本统计设置": "Cost statistics settings", "取消归档": "Unarchive",
"完整费率设置加载失败": "Failed to load the full rate settings", "取消标记": "Unmark",
" 至 ": " to ", "取消编辑": "Cancel edit",
"近 90 天": "Last 90 days", "取消置顶": "Unpin",
"自定义时间范围": "Custom time range", "同步中…": "Syncing…",
"界面语言": "Interface language", "启动失败": "Start failed",
"推理 Token": "Reasoning tokens", "团队协作": "Team collaboration",
"总处理 Token": "Total processed tokens", "团队工具": "Team tools",
"工作区筛选": "Workspace filter", "团队面板": "Team panel",
"供应商筛选": "Provider filter", "图片预览": "Image preview",
"模型筛选": "Model filter", "处理中…": "Processing…",
"刷新统计数据": "Refresh statistics", "复制失败": "Copy failed",
"全部工作区": "All workspaces", "复制指令": "Copy instructions",
"全部供应商": "All providers", "子智能体": "Subagent",
"清除筛选": "Clear filters", "完全权限": "Full access",
"命中率": "Hit rate", "工具调用": "Tool call",
"关闭用量统计": "Close usage statistics", "开启思考": "Thinking on",
"收录时间": "Listed on", "开始时间": "Start time",
"可用": "Available", "归档会话": "Archive session",
"暂不可用": "Temporarily unavailable",
"模型清单": "Model catalog",
"网关暂不可达": "Gateway unreachable",
"当前出口": "Current egress", "当前出口": "Current egress",
"测量中…": "Measuring…", "快捷预设": "Quick presets",
"请求数": "Requests", "快速模式": "Fast mode",
"监听地址": "Listen address", "恢复默认": "Restore Defaults",
"端口": "Port", "执行中…": "Running…",
"显示": "Show", "指纹清理": "Fingerprint cleanup",
"隐藏": "Hide", "排队发送": "Queued to send",
"插件列表": "Plugin list",
"插件升级": "Plugin upgrade", "插件升级": "Plugin upgrade",
"免费模型": "Free models", "插话发送": "Send interjection",
"EAC 模型": "EAC models", "搜索会话": "Search sessions",
"白嫖模型接入": "Free model access", "收录时间": "Listed on",
"免费额度": "Free quota", "收起输出": "Collapse output",
"数据看板": "Dashboard",
"运行日志": "Runtime log",
"网关设置": "Gateway settings",
"聚合渠道": "Aggregated channels",
"省 Token": "Save tokens",
"路由": "Routing",
"阿里系": "Alibaba family",
"手机号登录": "Phone number login",
"复制提示词": "Copy prompt",
"放弃修改": "Discard changes", "放弃修改": "Discard changes",
"复制 HTML": "Copy HTML", "数据看板": "Dashboard",
"下载图像": "Download image", "时间感知": "Time awareness",
"用 {app} 打开": "Open with {app}", "时间范围": "Time range",
"{app}(默认)": "{app} (default)", "显示名称": "Display name",
"无法获取应用列表": "Could not retrieve the application list", "暂不可用": "Temporarily unavailable",
"正在打开工作区": "Opening workspace", "未知原因": "Unknown reason",
"当前工作区或本地应用不可用": "The current workspace or local application is unavailable", "未知模型": "Unknown model",
"在本地打开": "Open locally", "查询中…": "Querying…",
"更多打开方式": "More ways to open", "检查中…": "Checking…",
"显示文件位置": "Show file location", "模型清单": "Model catalog",
"打开失败,请重试": "Open failed; try again", "模型筛选": "Model filter",
"无法显示文件位置,请重试": "Could not show the file location; try again", "测试中…": "Testing…",
"访达": "Finder" "测量中…": "Measuring…",
"深度思考": "Deep thinking",
"混合查看": "Combined view",
"添加插件": "Add plugin",
"添加账号": "Add account",
"清空搜索": "Clear search",
"清除筛选": "Clear filters",
"用量统计": "Usage Statistics",
"界面语言": "Interface language",
"登录状态": "Login status",
"监听地址": "Listen address",
"确认移除": "Confirm removal",
"等待依赖": "Waiting for dependencies",
"等待回答": "Awaiting answer",
"等待审批": "Awaiting approval",
"缓存写入": "Cache Write",
"缓存命中": "Cache Hit",
"缓存读取": "Cache read",
"网关设置": "Gateway settings",
"网页搜索": "Web search",
"置顶会话": "Pin session",
"聚合渠道": "Aggregated channels",
"自动刷新": "Auto refresh",
"自动换行": "Word Wrap",
"计划待审": "Plan awaiting review",
"语音输入": "Voice input",
"请求日志": "Request log",
"读取中…": "Reading…",
"调用台账": "Call ledger",
"调用次数": "Call count",
"输入口径": "Input basis",
"运行命令": "Run command",
"运行日志": "Runtime log",
"运行版本": "Running version",
"进入目录": "Enter directory",
"退出登录": "Sign out",
"间隔上限": "Maximum interval",
"间隔下限": "Minimum interval",
"(未知)": "(unknown)",
" 至 ": " to ",
"上一步": "Back",
"下一步": "Next",
"下一题": "Next question",
"不思考": "No thinking",
"中国版": "China",
"仅一次": "Once only",
"加载中": "Loading",
"去充值": "Add credit",
"命中率": "Hit rate",
"国际版": "International",
"已中断": "Interrupted",
"已保存": "Saved",
"已停止": "Stopped",
"已关闭": "Closed",
"已删除": "Deleted",
"已压缩": "Compacted",
"已失败": "Failed",
"已截断": "Truncated",
"已移除": "Removed",
"已结束": "Ended",
"已覆盖": "Overridden",
"已解除": "Cleared",
"待处理": "Pending",
"待审批": "Pending approval",
"总处理": "Total processed",
"按模型": "By model",
"按账号": "By account",
"提供商": "Provider",
"提供方": "Provider",
"无内容": "No content",
"无输出": "No output",
"未启用": "Disabled",
"未计价": "Not priced",
"未运行": "Not running",
"未配置": "Not configured",
"每分钟": "Every minute",
"每小时": "Every hour",
"知道了": "Got it",
"纯文本": "Plain text",
"网页版": "Web",
"请求数": "Requests",
"账号库": "Account pool",
"运行中": "Running",
"进行中": "In progress",
"阿里系": "Alibaba family",
" 天": " days",
"代码": "Code",
"位置": "Location",
"免费": "Free",
"全屏": "Full screen",
"内容": "Content",
"分钟": "minute",
"刚刚": "Just now",
"助手": "Assistant",
"单次": "Once",
"压缩": "Compact",
"参数": "Parameters",
"反馈": "Feedback",
"可用": "Available",
"周一": "Monday",
"周三": "Wednesday",
"周二": "Tuesday",
"周五": "Friday",
"周六": "Saturday",
"周四": "Thursday",
"周日": "Sunday",
"商汤": "SenseTime",
"回合": "Turn",
"图片": "Image",
"失败": "Failed",
"字节": "ByteDance",
"完成": "Done",
"对话": "Conversation",
"小时": "hour",
"层级": "Level",
"工具": "Tool",
"异常": "Error",
"思考": "Thinking",
"总计": "Total",
"成本": "Cost",
"打开": "Open",
"折叠": "Collapse",
"拒绝": "Deny",
"指令": "Instructions",
"推理": "Reasoning",
"提交": "Submit",
"插件": "Plugin",
"撤销": "Undo",
"文件": "File",
"新增": "Added",
"日期": "Date",
"时区": "Time zone",
"时长": "Duration",
"时间": "Time",
"显示": "Show",
"智谱": "Zhipu",
"有道": "Youdao",
"本机": "This machine",
"权限": "Permissions",
"查看": "View",
"标准": "Standard",
"每周": "Weekly",
"每天": "Daily",
"测试": "Test",
"消息": "Message",
"添加": "Add",
"用户": "User",
"用量": "Usage",
"登录": "Sign in",
"目标": "Goal",
"移除": "Remove",
"空闲": "Idle",
"端口": "Port",
"简洁": "Concise",
"终端": "Terminal",
"结果": "Result",
"继续": "Continue",
"网页": "Web",
"脚注": "Footnote",
"腾讯": "Tencent",
"范围": "Range",
"计划": "Plan",
"讯飞": "iFlytek",
"访达": "Finder",
"详细": "Detailed",
"调用": "Call",
"账号": "Account",
"路由": "Routing",
"跳过": "Skip",
"输入": "Input",
"输出": "Output",
"速度": "Speed",
"重复": "Repeat",
"隐藏": "Hide",
"风格": "Style",
"首字": "First token",
"默认": "Default",
"一": "1",
"三": "3",
"二": "2",
"五": "5",
"六": "6",
"分": "min",
"四": "4",
"多": "More",
"天": "Day",
"少": "Less",
"无": "None",
"日": "Day",
"月": "Month",
"秒": "sec"
} }

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
import { translatesEndpoint, translateJson } from "./json.js";
import { translateScript } from "./rewrite.js"; import { translateScript } from "./rewrite.js";
export const name = "chinese2english"; export const name = "chinese2english";
@ -16,21 +17,11 @@ function isScriptUrl(url: string): boolean {
return JS_URL.test(url); return JS_URL.test(url);
} }
/** /** Rewrites `/plugins/...` JavaScript responses in place. */
* Translates Chinese UI text at the bundle transport layer. function wrapBundles(ctx: any) {
*
* Every third-party plugin bundle is served through `/plugins/...` by the
* clientModules service, and combination bundles carry several plugins in one
* body. Intercepting the response here reaches all of them at once, before any
* code runs, without touching React trees or the locale store.
*
* `bundleResource` yields the body as a Buffer, so the wrapper decodes UTF-8,
* rewrites, and hands the bytes back in the shape the server expects.
*/
export function apply(ctx: any) {
const registry = ctx.clientModules; const registry = ctx.clientModules;
if (!registry || typeof registry.bundleResource !== "function") { if (!registry || typeof registry.bundleResource !== "function") {
console.error("[chinese2english] clientModules.bundleResource unavailable; translation disabled"); console.error("[chinese2english] clientModules.bundleResource unavailable; bundle translation disabled");
return; return;
} }
@ -82,3 +73,50 @@ export function apply(ctx: any) {
} }
}; };
} }
/**
* Rewrites server-authored UI copy returned by the `/api` RPC gateway.
*
* Plugin descriptors and model/provider names reach the browser as JSON, not as
* bundle strings, so the bundle seam cannot see them. The gateway resolves its
* own `dispatchRpc` per request, so overriding the instance method intercepts
* every `/api` call without claiming the interceptor slot the gateway owns.
*/
function wrapRpc(ctx: any) {
const gateway = ctx.get("typertGateway");
if (!gateway || typeof gateway.dispatchRpc !== "function") {
console.warn("[chinese2english] typertGateway unavailable; API copy left untranslated");
return;
}
const original = gateway.dispatchRpc;
gateway.dispatchRpc = async function (endpoint: string, ...rest: any[]) {
const result = await original.call(this, endpoint, ...rest);
if (!translatesEndpoint(endpoint)) return result;
if (!result || typeof result !== "object" || result.ok !== true) return result;
try {
const value = translateJson(result.value);
return value === result.value ? result : { ...result, value };
} catch (error) {
console.error(`[chinese2english] could not translate ${endpoint}:`, error);
return result;
}
};
}
/**
* Renders Chinese UI text as English.
*
* Two seams, because the text arrives two ways: plugin bundle source over
* `/plugins/...`, and server-authored copy over `/api`. Both are rewritten
* before the bytes leave the process, so no client-side ordering, DOM walking,
* or React tree mutation is involved.
*/
export function apply(ctx: any) {
wrapBundles(ctx);
// The gateway may not be mounted in every composition; load without it.
if (ctx.get("typertGateway") !== undefined) wrapRpc(ctx);
else ctx.inject(["typertGateway"], (gatewayCtx: any) => wrapRpc(gatewayCtx));
}

132
src/json.ts Normal file
View file

@ -0,0 +1,132 @@
import { hasChinese, substitute } from "./rewrite.js";
/**
* RPC endpoints whose payload is server-authored UI copy: plugin descriptors,
* model and provider names, and settings schema descriptions.
*
* This is an allowlist on purpose. Other endpoints carry user content —
* `/api/session/list` returns the operator's own prompts and transcripts — and
* rewriting those would corrupt displayed history.
*/
const TRANSLATED_ENDPOINTS = new Set([
"pluginManager/listBundles",
"pluginManager/listPlugins",
"pluginInventory/list",
"session/modelCatalog",
"llm/listProviders",
"settings/describe",
]);
const LOCALE_KEY = /^(zh|zh-cn|zh_cn|zh-hans|zh-hant)$/i;
const ENGLISH_KEY = /^en(-|$)/i;
/**
* Keys whose string values are display copy. Dictionary rewriting is confined
* to these: an enum member or an id that happens to contain Chinese must never
* be rewritten, because those values are compared, not shown.
*/
const DISPLAY_KEYS = new Set([
"name",
"description",
"title",
"label",
"summary",
"subtitle",
"tooltip",
"hint",
"caption",
]);
/**
* Bilingual descriptors are published as one string with both languages joined
* by ` · ` — `"… community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。"`.
* Dropping the Chinese half keeps the author's own English, which is better
* than any dictionary pass and works for every such descriptor at once.
*/
const BILINGUAL_SEPARATOR = " · ";
function preferEnglishHalf(value: string): string | undefined {
if (!value.includes(BILINGUAL_SEPARATOR)) return undefined;
const halves = value.split(BILINGUAL_SEPARATOR);
if (halves.length !== 2) return undefined;
const [left, right] = halves;
const leftChinese = hasChinese(left);
const rightChinese = hasChinese(right);
if (leftChinese === rightChinese) return undefined;
return (rightChinese ? left : right).trim() || undefined;
}
export function translatesEndpoint(endpoint: string): boolean {
return TRANSLATED_ENDPOINTS.has(endpoint);
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Rewrites server-authored UI copy inside an RPC result.
*
* A locale map (`{ zh, en }`) is resolved rather than translated: the author's
* own English beats a dictionary pass, so the Chinese side is overwritten with
* the English sibling. Consumers that read `.zh` directly — which is why this
* text stayed Chinese with the active locale already `en` — then render
* English, while consumers that resolve through the locale chain are unaffected
* because they already picked `.en`.
*
* Chinese with no English sibling falls back to the dictionary, which leaves it
* untouched unless every character is covered.
*/
export function translateJson<T>(value: T): T {
if (Array.isArray(value)) {
let changed = false;
const next = value.map((item) => {
const translated = translateJson(item);
if (translated !== item) changed = true;
return translated;
});
return (changed ? next : value) as unknown as T;
}
if (!isPlainObject(value)) return value;
let out: Record<string, unknown> | undefined;
const edit = () => (out ??= { ...value });
const keys = Object.keys(value);
const englishKey = keys.find((key) => ENGLISH_KEY.test(key));
const english = englishKey === undefined ? undefined : value[englishKey];
const hasEnglish = typeof english === "string" && english.length > 0;
// A locale map is any object offering a language-tagged key. Every string
// inside one is display copy, whatever the sibling key happens to be named.
const isLocaleMap = keys.some((key) => LOCALE_KEY.test(key) || ENGLISH_KEY.test(key));
for (const key of keys) {
const current = value[key];
if (typeof current === "string" && hasChinese(current)) {
if (LOCALE_KEY.test(key)) {
// An English sibling wins, but only when it is itself free of Chinese.
// Some plugins publish the same bilingual string under both keys, and
// copying that across would leave the Chinese in place.
if (hasEnglish && !hasChinese(english)) {
edit()[key] = english;
continue;
}
edit()[key] = preferEnglishHalf(current) ?? substitute(current);
continue;
}
if (DISPLAY_KEYS.has(key) || isLocaleMap) {
// `en` is not a locale key here, yet its value may still be bilingual:
// plugins occasionally swap their own keys, leaving Chinese under `en`.
edit()[key] = preferEnglishHalf(current) ?? substitute(current);
continue;
}
}
const translated = translateJson(current);
if (translated !== current) edit()[key] = translated;
}
return (out ?? value) as unknown as T;
}