fix: translate plugin bundles at the transport layer
The plugin never translated anything. Its client half monkey-patched locale internals that do not mutate the locale snapshot, and its DOM walker was disabled after it crashed the marketplace UI. Nothing else was left. Move translation to the bundle transport instead. Every third-party bundle is served through /plugins/ by the clientModules service, so wrapping bundleResource reaches all of them before any code runs, with no client-side ordering race and no React tree mutation. Two bugs surfaced while verifying against the live server: - The script check tested the path before the query string, but combo requests are /plugins/??a/client.js,b/client.js&rev=... where the path is just /plugins/. Every request was rejected. - bundleResource yields a Buffer, not a string, so the string guard rejected every request a second time. Rewriting is all-or-nothing per literal: Chinese has no word boundaries, so replacing known keys wherever they appear corrupts larger words (当前工作区 became "Saat IniWorkspace"). A literal is translated only when every Chinese character is covered by a dictionary entry. Two upstream entries were Malay rather than English and are now corrected. Each rewritten bundle is parsed with new Function before being served; if that fails the original bytes are returned, so a bad dictionary entry cannot break the UI. Verified against the live bundles: both combos compile, and a browser A/B shows 105 Chinese characters in the Plugins view without the plugin and 0 with it. The dictionary grows to 1,195 entries (45% of literals, zero partial rewrites) via a curated supplement merged last so it overrides harvested sources.
This commit is contained in:
parent
dd5629bc6a
commit
e3962c1591
16 changed files with 3650 additions and 866 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
node_modules/\n.DS_Store
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
72
README.md
72
README.md
|
|
@ -1,37 +1,83 @@
|
||||||
# chinese2english
|
# chinese2english
|
||||||
|
|
||||||
> Universal English translation patch for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) third-party plugins.
|
> Renders Chinese UI text in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) third-party plugins as English.
|
||||||
|
|
||||||
Automatically intercepts and translates Chinese UI strings, buttons, dialogs, and labels in third-party community plugins (like `@michengai/dsh-skills-manager`, model routers, etc.) into clean English in real-time.
|
Most community plugins ship hardcoded Chinese inside their client bundles. Those
|
||||||
|
strings never reach DSH's locale dictionaries, so they stay Chinese even when the
|
||||||
|
interface language is English. This plugin translates them at the transport
|
||||||
|
layer, before any of that code runs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The Problem It Solves
|
## How it works
|
||||||
|
|
||||||
Most DeepSeek Harness community plugins are authored in Chinese with either missing English dictionaries or hardcoded Chinese strings inside their React components. Even when your DSH interface is set to English, those plugins remain in Chinese.
|
Plugin bundles are served by DSH's `clientModules` service over `/plugins/...`.
|
||||||
|
This plugin wraps `clientModules.bundleResource`, decodes each JavaScript
|
||||||
|
response, rewrites the Chinese string literals it can translate, and returns the
|
||||||
|
result. Because interception happens server-side, it covers every plugin at once,
|
||||||
|
including combination bundles that carry dozens of them, and it runs before any
|
||||||
|
plugin code is evaluated.
|
||||||
|
|
||||||
**`chinese2english`** fixes this globally across all your installed plugins using an intelligent real-time translation observer and comprehensive technical DSH dictionary.
|
Two deliberate choices:
|
||||||
|
|
||||||
|
- **All-or-nothing literals.** Chinese has no word boundaries, so replacing known
|
||||||
|
words wherever they appear corrupts larger words (`当前工作区` would become
|
||||||
|
`Saat IniWorkspace`). A literal is translated only when every Chinese character
|
||||||
|
in it is covered by a dictionary entry; anything else is left in Chinese, since
|
||||||
|
half-translated text reads worse than untranslated text.
|
||||||
|
- **A compile gate.** Each rewritten bundle is parsed with `new Function` before
|
||||||
|
it is served. If a rewrite ever produced invalid JavaScript, the original bytes
|
||||||
|
are served instead, so a bad dictionary entry cannot break the UI.
|
||||||
|
|
||||||
|
The client half does one thing: it selects the `en` locale through
|
||||||
|
`ctx.locale.setLocale`, which persists the preference. It never touches the DOM.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## What it does not cover
|
||||||
|
|
||||||
- **Real-Time DOM Translation:** Detects dynamically mounted modals, tabs, dialogs, and tooltips and translates Chinese text on the fly.
|
Translation works on string literals inside served bundles. Text that arrives
|
||||||
- **DSH Locale Integration:** Intercepts DSH's `ctx.locale` to provide English fallbacks for untranslated namespaces.
|
another way stays as it is:
|
||||||
- **Preserves Code Blocks:** Intelligently ignores `<pre>`, `<code>`, inputs, and script tags so actual code and user inputs are never modified.
|
|
||||||
- **Zero Configuration:** Works automatically out-of-the-box upon installation.
|
- Plugin **descriptors** in `package.json` and `README.md` files (marketplace
|
||||||
|
listings, plugin titles).
|
||||||
|
- **Server-side API prose** that is fetched as JSON at runtime.
|
||||||
|
- The long tail of one-off literals that the dictionary does not yet cover.
|
||||||
|
|
||||||
|
The shipped dictionary holds roughly 1,200 entries and covers about 45% of the
|
||||||
|
Chinese literals in a typical plugin set. Coverage is concentrated on strings
|
||||||
|
that appear repeatedly; rare one-off sentences are usually left in Chinese.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install directly with the official DSH CLI:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dsh plugin --profile web add chinese2english
|
dsh plugin --profile web add chinese2english
|
||||||
```
|
```
|
||||||
|
|
||||||
*(To remove later: `dsh plugin --profile web remove chinese2english`)*
|
Remove it with `dsh plugin --profile web remove chinese2english`.
|
||||||
|
|
||||||
|
Restart DSH after installing, so the plugin is loaded and its transport wrapper
|
||||||
|
is installed before the first bundle request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dict # rebuild src/dictionary.json from its sources
|
||||||
|
npm run build # bundle the server and client halves into lib/
|
||||||
|
```
|
||||||
|
|
||||||
|
Dictionary sources, merged in this order:
|
||||||
|
|
||||||
|
1. `dshmarket_dict.json` — harvested pairs.
|
||||||
|
2. `UI_TRANSLATIONS` in `patch-plugins.py` — harvested pairs.
|
||||||
|
3. `src/dictionary.extra.json` — curated entries, merged last so they win.
|
||||||
|
|
||||||
|
Add or fix translations in `src/dictionary.extra.json`, then run
|
||||||
|
`npm run dict && npm run build`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
372
lib/client.js
372
lib/client.js
|
|
@ -22,368 +22,32 @@ var index_exports = {};
|
||||||
__export(index_exports, {
|
__export(index_exports, {
|
||||||
apply: () => apply,
|
apply: () => apply,
|
||||||
inject: () => inject,
|
inject: () => inject,
|
||||||
name: () => name,
|
name: () => name
|
||||||
translateElement: () => translateElement,
|
|
||||||
translateText: () => translateText
|
|
||||||
});
|
});
|
||||||
module.exports = __toCommonJS(index_exports);
|
module.exports = __toCommonJS(index_exports);
|
||||||
|
var name = "chinese2english";
|
||||||
// src/client/dictionary.ts
|
|
||||||
var DICTIONARY = {
|
|
||||||
// DSH Marketplace & Store
|
|
||||||
"\u63D2\u4EF6\u5E02\u573A": "Plugin Marketplace",
|
|
||||||
"\u901B\u4E00\u901B\uFF0C\u70B9\u4E00\u4E0B\uFF0C\u88C5\u597D\u3002": "Browse, click, and install.",
|
|
||||||
"\u5168\u90E8\u63D2\u4EF6": "All Plugins",
|
|
||||||
"\u5DF2\u5B89\u88C5\u63D2\u4EF6": "Installed Plugins",
|
|
||||||
"\u63D2\u4EF6": "Plugins",
|
|
||||||
"\u5E02\u573A": "Marketplace",
|
|
||||||
"\u70ED\u95E8\u63D2\u4EF6": "Popular Plugins",
|
|
||||||
"\u6700\u65B0\u4E0A\u67B6": "Recently Added",
|
|
||||||
"\u63A8\u8350\u63D2\u4EF6": "Recommended Plugins",
|
|
||||||
"\u4E0B\u8F7D\u91CF": "Downloads",
|
|
||||||
"\u8BC4\u5206": "Rating",
|
|
||||||
"\u4F5C\u8005": "Author",
|
|
||||||
"\u5B89\u88C5\u91CF": "Installs",
|
|
||||||
"\u5B89\u88C5\u63D2\u4EF6": "Install Plugin",
|
|
||||||
"\u5378\u8F7D\u63D2\u4EF6": "Uninstall Plugin",
|
|
||||||
"\u66F4\u65B0\u63D2\u4EF6": "Update Plugin",
|
|
||||||
"\u7B2C\u4E09\u65B9\u63D2\u4EF6": "Third-Party Plugins",
|
|
||||||
"\u5B98\u65B9\u63D2\u4EF6": "Official Plugins",
|
|
||||||
"\u641C\u7D22\u63D2\u4EF6": "Search plugins...",
|
|
||||||
"\u641C\u7D22\u63D2\u4EF6\u540D\u79F0\u6216\u4F5C\u8005": "Search by plugin name or author...",
|
|
||||||
"\u4E00\u952E\u5B89\u88C5": "Install",
|
|
||||||
"\u4E00\u952E\u66F4\u65B0": "Update",
|
|
||||||
"\u6B63\u5728\u4E0B\u8F7D": "Downloading...",
|
|
||||||
"\u6B63\u5728\u5B89\u88C5": "Installing...",
|
|
||||||
"\u5B89\u88C5\u5B8C\u6210": "Installed",
|
|
||||||
"\u5378\u8F7D\u5B8C\u6210": "Uninstalled",
|
|
||||||
// Skills Manager Navigation & Tabs
|
|
||||||
"\u6280\u80FD\u7BA1\u7406": "Skills Manager",
|
|
||||||
"\u6280\u80FD\u7BA1\u7406\u5668": "Skills Manager",
|
|
||||||
"\u7EDF\u4E00\u52A0\u8F7D\u548C\u7BA1\u7406\u672C\u673A Agent Skills\u3002": "Load and manage Agent Skills on this machine in one place.",
|
|
||||||
"\u6280\u80FD\u5217\u8868": "Skills List",
|
|
||||||
"\u5DF2\u5B89\u88C5\u6280\u80FD": "Installed Skills",
|
|
||||||
"\u53EF\u7528\u6280\u80FD": "Available Skills",
|
|
||||||
"\u53D1\u73B0\u6280\u80FD": "Discover Skills",
|
|
||||||
"\u4ED3\u5E93\u8BA2\u9605": "Repositories",
|
|
||||||
"\u4ED3\u5E93\u7BA1\u7406": "Repositories",
|
|
||||||
"\u6280\u80FD\u6765\u6E90": "Skill Sources",
|
|
||||||
"\u5168\u5C40\u6280\u80FD": "Global Skills",
|
|
||||||
"\u9879\u76EE\u6280\u80FD": "Project Skills",
|
|
||||||
"\u516C\u5171 Agent": "Shared Agent",
|
|
||||||
"\u9879\u76EE Agent": "Project Agent",
|
|
||||||
"\u9879\u76EE Skills": "Project Skills",
|
|
||||||
"\u4F1A\u8BDD\u6280\u80FD": "Session Skills",
|
|
||||||
"\u56DE\u6536\u7AD9": "Trash",
|
|
||||||
"\u8BCA\u65AD\u5F02\u5E38": "Diagnostics",
|
|
||||||
"\u5F02\u5E38\u8BCA\u65AD": "Diagnostics",
|
|
||||||
"\u6E90\u6587\u4EF6\u53EA\u8BFB": "Read-only",
|
|
||||||
"\u53EA\u8BFB": "Read-only",
|
|
||||||
"\u521B\u5EFA\u4F4D\u7F6E": "Location",
|
|
||||||
"\u5168\u5C40 DSH": "Global DSH",
|
|
||||||
"\u9879\u76EE DSH": "Project DSH",
|
|
||||||
"DSH \u6280\u80FD": "DSH Skills",
|
|
||||||
// Actions & Buttons
|
|
||||||
"\u521B\u5EFA\u6280\u80FD": "Create Skill",
|
|
||||||
"\u521B\u5EFA\u65B0\u6280\u80FD": "Create Skill",
|
|
||||||
"\u65B0\u5EFA\u6280\u80FD": "New Skill",
|
|
||||||
"\u7ACB\u5373\u521B\u5EFA": "Create Now",
|
|
||||||
"\u5BFC\u5165\u6280\u80FD": "Import Skill",
|
|
||||||
"\u5BFC\u51FA\u6280\u80FD": "Export Skill",
|
|
||||||
"\u5BFC\u5165": "Import",
|
|
||||||
"\u5BFC\u51FA": "Export",
|
|
||||||
"\u5237\u65B0": "Refresh",
|
|
||||||
"\u68C0\u67E5\u66F4\u65B0": "Check Updates",
|
|
||||||
"\u6B63\u5728\u68C0\u67E5\u66F4\u65B0\u2026": "Checking for updates...",
|
|
||||||
"\u6B63\u5728\u68C0\u67E5\u66F4\u65B0...": "Checking for updates...",
|
|
||||||
"\u68C0\u67E5\u5E76\u66F4\u65B0": "Check & Update",
|
|
||||||
"\u66F4\u65B0": "Update",
|
|
||||||
"\u7BA1\u7406\u66F4\u65B0": "Manage Updates",
|
|
||||||
"\u7ACB\u5373\u66F4\u65B0": "Update Now",
|
|
||||||
"\u786E\u8BA4\u66F4\u65B0": "Confirm Update",
|
|
||||||
"\u56DE\u6EDA": "Rollback",
|
|
||||||
"\u786E\u8BA4\u56DE\u6EDA": "Confirm Rollback",
|
|
||||||
"\u6062\u590D": "Restore",
|
|
||||||
"\u79FB\u5230\u56DE\u6536\u7AD9": "Move to Trash",
|
|
||||||
"\u79FB\u5165\u56DE\u6536\u7AD9": "Move to Trash",
|
|
||||||
"\u6C38\u4E45\u5220\u9664": "Delete Forever",
|
|
||||||
"\u6E05\u7A7A\u56DE\u6536\u7AD9": "Empty Trash",
|
|
||||||
"\u5220\u9664": "Delete",
|
|
||||||
"\u542F\u7528": "Enable",
|
|
||||||
"\u505C\u7528": "Disable",
|
|
||||||
"\u5DF2\u542F\u7528": "Enabled",
|
|
||||||
"\u5DF2\u505C\u7528": "Disabled",
|
|
||||||
"\u5168\u90E8\u542F\u7528": "Enable All",
|
|
||||||
"\u5168\u90E8\u505C\u7528": "Disable All",
|
|
||||||
"\u542F\u505C\u6765\u6E90": "Toggle Source",
|
|
||||||
"\u542F\u505C\u6280\u80FD": "Toggle Skill",
|
|
||||||
"\u5B89\u88C5": "Install",
|
|
||||||
"\u5DF2\u5B89\u88C5": "Installed",
|
|
||||||
"\u5378\u8F7D": "Uninstall",
|
|
||||||
"\u67E5\u770B\u8BE6\u60C5": "View Details",
|
|
||||||
"\u67E5\u770B\u8BF4\u660E": "View Docs",
|
|
||||||
"\u67E5\u770B": "View",
|
|
||||||
"\u66F4\u591A": "More",
|
|
||||||
"\u53D6\u6D88": "Cancel",
|
|
||||||
"\u4FDD\u5B58": "Save",
|
|
||||||
"\u5173\u95ED": "Close",
|
|
||||||
"\u786E\u5B9A": "OK",
|
|
||||||
"\u590D\u5236": "Copy",
|
|
||||||
"\u5DF2\u590D\u5236": "Copied",
|
|
||||||
"\u590D\u5236\u6210\u529F": "Copied",
|
|
||||||
"\u641C\u7D22": "Search",
|
|
||||||
"\u91CD\u8BD5": "Retry",
|
|
||||||
"\u9009\u62E9\u6587\u4EF6": "Choose File",
|
|
||||||
"\u9009\u62E9\u6587\u4EF6\u5939": "Choose Folder",
|
|
||||||
"\u95EE\u9898\u53CD\u9988": "Issues",
|
|
||||||
"\u8BBE\u7F6E": "Settings",
|
|
||||||
// Status & Filters
|
|
||||||
"\u5168\u90E8\u6765\u6E90": "All Sources",
|
|
||||||
"\u5168\u90E8\u72B6\u6001": "All Statuses",
|
|
||||||
"\u8C03\u7528\u72B6\u6001": "Invocation Status",
|
|
||||||
"\u72B6\u6001": "Status",
|
|
||||||
"\u6765\u6E90": "Source",
|
|
||||||
"\u88AB\u8986\u76D6": "Shadowed",
|
|
||||||
"\u540D\u79F0": "Name",
|
|
||||||
"\u7248\u672C": "Version",
|
|
||||||
"\u5927\u5C0F": "Size",
|
|
||||||
"\u7C7B\u578B": "Type",
|
|
||||||
"\u64CD\u4F5C": "Actions",
|
|
||||||
"\u8BE6\u60C5": "Details",
|
|
||||||
"\u63CF\u8FF0": "Description",
|
|
||||||
"\u7B80\u4ECB": "Description",
|
|
||||||
"\u6B63\u6587": "Body",
|
|
||||||
"\u6E90\u6587\u4EF6": "Source File",
|
|
||||||
"\u66F4\u65B0\u65F6\u95F4": "Updated",
|
|
||||||
"\u521B\u5EFA\u65F6\u95F4": "Created",
|
|
||||||
"\u5220\u9664\u4E8E": "Deleted at",
|
|
||||||
"\u6765\u6E90\uFF1A": "Source: ",
|
|
||||||
// Empty & Loading States
|
|
||||||
"\u6B63\u5728\u52A0\u8F7D\u6280\u80FD\u2026": "Loading skills...",
|
|
||||||
"\u6B63\u5728\u52A0\u8F7D\u6280\u80FD...": "Loading skills...",
|
|
||||||
"\u6B63\u5728\u52A0\u8F7D\u2026": "Loading...",
|
|
||||||
"\u6B63\u5728\u52A0\u8F7D...": "Loading...",
|
|
||||||
"\u52A0\u8F7D\u4E2D\u2026": "Loading...",
|
|
||||||
"\u52A0\u8F7D\u4E2D...": "Loading...",
|
|
||||||
"\u6B63\u5728\u8BFB\u53D6\u8BF4\u660E\u2026": "Loading documentation...",
|
|
||||||
"\u6B63\u5728\u8BFB\u53D6\u8BF4\u660E...": "Loading documentation...",
|
|
||||||
"\u6CA1\u6709\u5339\u914D\u7684\u6280\u80FD\u3002": "No matching skills.",
|
|
||||||
"\u6CA1\u6709\u5339\u914D\u7684\u6280\u80FD": "No matching skills",
|
|
||||||
"\u8BE5\u6765\u6E90\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u6682\u65F6\u6CA1\u6709\u6280\u80FD\u3002": "This source directory does not exist or has no skills.",
|
|
||||||
"\u56DE\u6536\u7AD9\u4E3A\u7A7A\u3002": "Trash is empty.",
|
|
||||||
"\u56DE\u6536\u7AD9\u4E3A\u7A7A": "Trash is empty",
|
|
||||||
"\u6682\u65E0\u6280\u80FD": "No skills available",
|
|
||||||
"\u6682\u65E0\u6570\u636E": "No data available",
|
|
||||||
"\u672A\u63D0\u4F9B\u7B80\u4ECB": "No description provided",
|
|
||||||
"\u672A\u53D1\u73B0\u8BCA\u65AD\u95EE\u9898\u3002": "No diagnostic issues found.",
|
|
||||||
// Upload & Import Modal
|
|
||||||
"\u62D6\u62FD\u6280\u80FD\u5230\u6B64\u5904": "Drop skill here",
|
|
||||||
"\u652F\u6301 .zip\u3001\u6280\u80FD\u6587\u4EF6\u5939\u6216\u5355\u4E2A SKILL.md": "Supports .zip, a skill folder, or single SKILL.md",
|
|
||||||
"\u6587\u4EF6\u8981\u6C42": "Requirements",
|
|
||||||
"\u538B\u7F29\u5305\u6216\u6587\u4EF6\u5939\u9700\u5305\u542B SKILL.md": "Archives or folders must contain SKILL.md",
|
|
||||||
"SKILL.md \u9700\u5305\u542B YAML \u683C\u5F0F\u7684\u6280\u80FD\u540D\u79F0\u548C\u63CF\u8FF0": "SKILL.md must include YAML name and description",
|
|
||||||
"\u5BFC\u5165\u65F6\u590D\u5236\u5B8C\u6574\u5185\u5BB9\uFF0C\u4E0D\u4FEE\u6539\u539F\u59CB\u6765\u6E90": "Import copies full content without modifying source",
|
|
||||||
"\u5BFC\u5165\u5230\u5168\u5C40 DSH": "Import to global DSH",
|
|
||||||
"\u79FB\u9664\u6240\u9009\u5185\u5BB9": "Remove selection",
|
|
||||||
"\u8BF7\u9009\u62E9 .zip \u6216\u5355\u4E2A SKILL.md\u3002": "Please choose a .zip or single SKILL.md.",
|
|
||||||
"\u6240\u9009\u6587\u4EF6\u5939\u4E2D\u6CA1\u6709\u627E\u5230 SKILL.md\u3002": "No SKILL.md found in the selected folder.",
|
|
||||||
// Creation Form
|
|
||||||
"\u4F8B\u5982 code-review-helper": "e.g. code-review-helper",
|
|
||||||
"\u4E00\u53E5\u8BDD\u8BF4\u660E\u4EC0\u4E48\u65F6\u5019\u4F7F\u7528": "One sentence describing when to use it",
|
|
||||||
"\u6B63\u6587\uFF08Markdown\uFF09": "Body (Markdown)",
|
|
||||||
"\u5199\u4E0B\u6280\u80FD\u8981\u9075\u5FAA\u7684\u6307\u4EE4\u3001\u6B65\u9AA4\u548C\u8FB9\u754C\u2026": "Write the instructions, steps, and boundaries...",
|
|
||||||
"\u521B\u5EFA\u548C\u5BFC\u5165\u5747\u4FDD\u5B58\u5230\u5168\u5C40 DSH\uFF0C\u8DE8\u9879\u76EE\u53EF\u7528\u3002": "Created and imported skills are saved to global DSH and available across projects.",
|
|
||||||
// General Settings & UI
|
|
||||||
"\u901A\u7528\u8BBE\u7F6E": "General Settings",
|
|
||||||
"\u6A21\u578B\u8BBE\u7F6E": "Model Settings",
|
|
||||||
"\u5916\u89C2\u8BBE\u7F6E": "Appearance",
|
|
||||||
"\u8BED\u8A00\u8BBE\u7F6E": "Language",
|
|
||||||
"\u5FEB\u6377\u952E": "Shortcuts",
|
|
||||||
"\u5173\u4E8E": "About",
|
|
||||||
"\u7248\u672C\u53F7": "Version",
|
|
||||||
"\u68C0\u67E5\u65B0\u7248\u672C": "Check for updates",
|
|
||||||
"\u8BED\u8A00": "Language",
|
|
||||||
"\u7B80\u4F53\u4E2D\u6587": "Simplified Chinese",
|
|
||||||
"\u82F1\u6587": "English",
|
|
||||||
"\u6DF1\u8272\u6A21\u5F0F": "Dark Mode",
|
|
||||||
"\u6D45\u8272\u6A21\u5F0F": "Light Mode",
|
|
||||||
"\u8DDF\u968F\u7CFB\u7EDF": "Follow System",
|
|
||||||
"\u9000\u51FA": "Exit",
|
|
||||||
"\u91CD\u542F": "Restart",
|
|
||||||
// Model & Chat Concepts
|
|
||||||
"\u6A21\u578B": "Models",
|
|
||||||
"\u4F1A\u8BDD": "Sessions",
|
|
||||||
"\u65B0\u4F1A\u8BDD": "New Session",
|
|
||||||
"\u5386\u53F2\u4F1A\u8BDD": "History",
|
|
||||||
"\u5DE5\u4F5C\u533A": "Workspace",
|
|
||||||
"\u6682\u65E0\u4F1A\u8BDD": "No sessions",
|
|
||||||
"\u53D1\u9001": "Send",
|
|
||||||
"\u505C\u6B62\u751F\u6210": "Stop generating",
|
|
||||||
"\u91CD\u65B0\u751F\u6210": "Regenerate",
|
|
||||||
"\u8017\u65F6": "Duration",
|
|
||||||
"\u6B65\u9AA4": "Steps",
|
|
||||||
"\u8F6E\u6B21": "Turns"
|
|
||||||
};
|
|
||||||
var SORTED_PAIRS = Object.entries(DICTIONARY).sort(
|
|
||||||
(a, b) => b[0].length - a[0].length
|
|
||||||
);
|
|
||||||
var DYNAMIC_PATTERNS = [
|
|
||||||
[/(\d+)\s*个技能/g, "$1 skills"],
|
|
||||||
[/(\d+)\s*个已启用/g, "$1 enabled"],
|
|
||||||
[/(\d+)\s*个已停用/g, "$1 disabled"],
|
|
||||||
[/(\d+)\s*个被覆盖/g, "$1 shadowed"],
|
|
||||||
[/(\d+)\s*个诊断异常/g, "$1 issues"],
|
|
||||||
[/(\d+)\s*个文件/g, "$1 files"],
|
|
||||||
[/(\d+)\s*个待处理技能/g, "$1 pending skills"],
|
|
||||||
[/来源:/g, "Source: "],
|
|
||||||
[/创建于\s*([0-9:\-\/ ]+)/g, "Created at $1"],
|
|
||||||
[/更新于\s*([0-9:\-\/ ]+)/g, "Updated at $1"],
|
|
||||||
[/删除于\s*([0-9:\-\/ ]+)/g, "Deleted at $1"],
|
|
||||||
[/被\s*(.+?)\s*覆盖/g, "Shadowed by $1"]
|
|
||||||
];
|
|
||||||
|
|
||||||
// src/client/index.ts
|
|
||||||
var name = "chinese2english-client";
|
|
||||||
var inject = ["locale"];
|
var inject = ["locale"];
|
||||||
var CHINESE_REGEX = /[\u4e00-\u9fa5]/;
|
function apply(ctx) {
|
||||||
function translateText(text) {
|
const locale = ctx.locale;
|
||||||
if (!text || !CHINESE_REGEX.test(text)) return text;
|
if (!locale || typeof locale.setLocale !== "function") {
|
||||||
const trimmed = text.trim();
|
console.warn("[chinese2english] locale service unavailable; UI language left unchanged");
|
||||||
if (DICTIONARY[trimmed]) {
|
|
||||||
return text.replace(trimmed, DICTIONARY[trimmed]);
|
|
||||||
}
|
|
||||||
for (const [pattern, replacement] of DYNAMIC_PATTERNS) {
|
|
||||||
if (pattern.test(trimmed)) {
|
|
||||||
return text.replace(trimmed, trimmed.replace(pattern, replacement));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let result = text;
|
|
||||||
let changed = false;
|
|
||||||
for (const [cn, en] of SORTED_PAIRS) {
|
|
||||||
if (cn.length >= 2 && result.includes(cn)) {
|
|
||||||
result = result.replaceAll(cn, en);
|
|
||||||
changed = true;
|
|
||||||
if (!CHINESE_REGEX.test(result)) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changed ? result : text;
|
|
||||||
}
|
|
||||||
function translateElement(root) {
|
|
||||||
if (!root) return;
|
|
||||||
if (root.nodeType === Node.TEXT_NODE) {
|
|
||||||
const val = root.nodeValue;
|
|
||||||
if (val && CHINESE_REGEX.test(val)) {
|
|
||||||
const t = translateText(val);
|
|
||||||
if (t !== val) root.nodeValue = t;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (root.nodeType !== Node.ELEMENT_NODE) return;
|
const pin = (reason) => {
|
||||||
const el = root;
|
|
||||||
const tag = el.tagName;
|
|
||||||
if (tag === "SCRIPT" || tag === "STYLE" || tag === "PRE" || tag === "CODE") return;
|
|
||||||
for (const attr of ["placeholder", "title", "aria-label", "alt"]) {
|
|
||||||
const val = el.getAttribute(attr);
|
|
||||||
if (val && CHINESE_REGEX.test(val)) {
|
|
||||||
el.setAttribute(attr, translateText(val));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
|
|
||||||
let textNode = walker.nextNode();
|
|
||||||
while (textNode) {
|
|
||||||
const val = textNode.nodeValue;
|
|
||||||
if (val && CHINESE_REGEX.test(val)) {
|
|
||||||
const parent = textNode.parentElement;
|
|
||||||
if (parent) {
|
|
||||||
const pTag = parent.tagName;
|
|
||||||
if (pTag !== "SCRIPT" && pTag !== "STYLE" && pTag !== "PRE" && pTag !== "CODE") {
|
|
||||||
const t = translateText(val);
|
|
||||||
if (t !== val) textNode.nodeValue = t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
textNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function enforceEnglishStorage() {
|
|
||||||
if (typeof window === "undefined" || !window.localStorage) return;
|
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
if (locale.getLocale?.().active === "en") return;
|
||||||
const key = localStorage.key(i);
|
locale.setLocale("en");
|
||||||
if (key && (key.includes("lang") || key.includes("locale") || key.includes("all-usage") || key.includes("dsh"))) {
|
} catch (error) {
|
||||||
const val = localStorage.getItem(key);
|
console.error(`[chinese2english] could not select English (${reason}):`, error);
|
||||||
if (val === "zh" || val === "zh-CN") {
|
|
||||||
localStorage.setItem(key, "en");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function apply(ctx) {
|
|
||||||
console.log("[chinese2english] Activating English locale & translation engine...");
|
|
||||||
enforceEnglishStorage();
|
|
||||||
if (ctx.locale) {
|
|
||||||
try {
|
|
||||||
if (typeof ctx.locale.setLocale === "function") {
|
|
||||||
ctx.locale.setLocale("en");
|
|
||||||
}
|
|
||||||
if (typeof ctx.locale.resolveActive === "function") {
|
|
||||||
ctx.locale.resolveActive = () => "en";
|
|
||||||
}
|
|
||||||
if (typeof ctx.locale.getSnapshot === "function") {
|
|
||||||
const origSnap = ctx.locale.getSnapshot.bind(ctx.locale);
|
|
||||||
ctx.locale.getSnapshot = () => {
|
|
||||||
const s = origSnap();
|
|
||||||
return s ? { ...s, active: "en" } : s;
|
|
||||||
};
|
};
|
||||||
}
|
pin("initial");
|
||||||
if (typeof ctx.locale.publish === "function") {
|
ctx.effect(
|
||||||
ctx.locale.publish("en", true);
|
() => ctx.on("locale/change", (snapshot) => {
|
||||||
}
|
if (snapshot?.active !== "en") pin("locale change");
|
||||||
if (typeof ctx.locale.translate === "function") {
|
}),
|
||||||
const origTranslate = ctx.locale.translate.bind(ctx.locale);
|
"chinese2english: keep the UI locale on English"
|
||||||
ctx.locale.translate = function(ns, key, params) {
|
);
|
||||||
const val = origTranslate(ns, key, params);
|
|
||||||
if (typeof val === "string" && CHINESE_REGEX.test(val)) {
|
|
||||||
return translateText(val);
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (ctx.locale.dicts && typeof ctx.locale.dicts.entries === "function") {
|
|
||||||
for (const [ns, localesMap] of ctx.locale.dicts.entries()) {
|
|
||||||
const zh = localesMap?.get("zh");
|
|
||||||
let en = localesMap?.get("en");
|
|
||||||
if (!en) {
|
|
||||||
en = {};
|
|
||||||
localesMap?.set("en", en);
|
|
||||||
}
|
|
||||||
if (zh) {
|
|
||||||
for (const [k, v] of Object.entries(zh)) {
|
|
||||||
if ((!en[k] || en[k] === k || CHINESE_REGEX.test(en[k])) && typeof v === "string") {
|
|
||||||
en[k] = translateText(v) || v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[dsh-plugin-english] Locale patch error:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof document !== "undefined") {
|
|
||||||
document.documentElement.lang = "en";
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
if (document.documentElement.lang !== "en") {
|
|
||||||
document.documentElement.lang = "en";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
enforceEnglishStorage();
|
|
||||||
}
|
}
|
||||||
return module.exports; } });
|
return module.exports; } });
|
||||||
//# sourceMappingURL=client.js.map
|
//# sourceMappingURL=client.js.map
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
1513
lib/index.js
1513
lib/index.js
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
|
@ -30,16 +30,16 @@
|
||||||
"patch": "./cordis.patch.yml"
|
"patch": "./cordis.patch.yml"
|
||||||
},
|
},
|
||||||
"client": {
|
"client": {
|
||||||
"immediately": true,
|
|
||||||
"inject": [
|
"inject": [
|
||||||
"@deepseek-ai/dsh-client-locale"
|
"locale"
|
||||||
],
|
],
|
||||||
"platform": "web"
|
"platform": "web"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"patch": "python3 patch-plugins.py",
|
"dict": "node scripts/merge-dict.mjs",
|
||||||
"build": "node scripts/build.mjs"
|
"build": "node scripts/build.mjs",
|
||||||
|
"build:dict": "node scripts/merge-dict.mjs && node scripts/build.mjs"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@deepseek-ai/cordis": ">=4.0.0"
|
"@deepseek-ai/cordis": ">=4.0.0"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
// --bundle inlines src/dictionary.json into the server output, so lib/ is the
|
||||||
|
// only thing that has to ship.
|
||||||
console.log("Building server backend...");
|
console.log("Building server backend...");
|
||||||
execSync("npx esbuild src/index.ts --outfile=lib/index.js --platform=node --format=esm --target=node20 --sourcemap", { stdio: "inherit" });
|
execSync(
|
||||||
|
"npx esbuild src/index.ts --outfile=lib/index.js --platform=node --format=esm --target=node20 --bundle --sourcemap",
|
||||||
|
{ stdio: "inherit" },
|
||||||
|
);
|
||||||
|
|
||||||
console.log("Building client frontend bundle...");
|
console.log("Building client frontend bundle...");
|
||||||
const banner = 'window.__ModuleLoader__.load({ id: "chinese2english", factory: (require) => { var module = { exports: {} }; var exports = module.exports;';
|
const banner = 'window.__ModuleLoader__.load({ id: "chinese2english", factory: (require) => { var module = { exports: {} }; var exports = module.exports;';
|
||||||
const footer = 'return module.exports; } });';
|
const footer = 'return module.exports; } });';
|
||||||
execSync(`npx esbuild src/client/index.ts --outfile=lib/client.js --platform=browser --format=cjs --target=es2022 --bundle --banner:js='${banner}' --footer:js='${footer}' --sourcemap`, { stdio: "inherit" });
|
execSync(
|
||||||
|
`npx esbuild src/client/index.ts --outfile=lib/client.js --platform=browser --format=cjs --target=es2022 --bundle --banner:js='${banner}' --footer:js='${footer}' --sourcemap`,
|
||||||
|
{ stdio: "inherit" },
|
||||||
|
);
|
||||||
|
|
||||||
console.log("Build complete!");
|
console.log("Build complete!");
|
||||||
|
|
|
||||||
63
scripts/merge-dict.mjs
Normal file
63
scripts/merge-dict.mjs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
// Merges the two translation sources already in this repo into one canonical
|
||||||
|
// dictionary payload. Dependency-free; rerun with `npm run dict`.
|
||||||
|
|
||||||
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const pairs = new Map();
|
||||||
|
|
||||||
|
const marketPath = join(root, "dshmarket_dict.json");
|
||||||
|
if (existsSync(marketPath)) {
|
||||||
|
const data = JSON.parse(readFileSync(marketPath, "utf8"));
|
||||||
|
for (const [zh, en] of Object.entries(data)) pairs.set(zh, en);
|
||||||
|
}
|
||||||
|
|
||||||
|
// patch-plugins.py carries its pairs inline as `"zh": "en",` lines.
|
||||||
|
const patchPath = join(root, "patch-plugins.py");
|
||||||
|
if (existsSync(patchPath)) {
|
||||||
|
const src = readFileSync(patchPath, "utf8");
|
||||||
|
const body = src.slice(src.indexOf("UI_TRANSLATIONS = {"));
|
||||||
|
const re = /^\s*"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*$/gm;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(body))) pairs.set(m[1], m[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Curated supplement, measured against the real served bundles. Merged last so
|
||||||
|
// it overrides anything the harvested sources got wrong.
|
||||||
|
const extraPath = join(root, "src", "dictionary.extra.json");
|
||||||
|
if (existsSync(extraPath)) {
|
||||||
|
const data = JSON.parse(readFileSync(extraPath, "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
|
||||||
|
// inject foreign words into an English UI, so they are replaced by name.
|
||||||
|
const NON_ENGLISH = new Map([
|
||||||
|
["当前", "Current"],
|
||||||
|
["本地", "Local"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const clean = {};
|
||||||
|
let dropped = 0;
|
||||||
|
for (const [zh, en] of pairs) {
|
||||||
|
// Guard the substitution contract: a key must be Chinese and must actually
|
||||||
|
// differ from its replacement, otherwise rewriting is a no-op with risk.
|
||||||
|
if (!/[\u4e00-\u9fa5]/.test(zh) || !en || zh === en) {
|
||||||
|
dropped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
clean[zh] = NON_ENGLISH.get(zh) ?? en;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = Object.fromEntries(
|
||||||
|
Object.entries(clean).sort((a, b) => b[0].length - a[0].length || a[0].localeCompare(b[0])),
|
||||||
|
);
|
||||||
|
writeFileSync(join(root, "src", "dictionary.json"), JSON.stringify(sorted, null, 2) + "\n");
|
||||||
|
|
||||||
|
const lengths = Object.keys(sorted).map((k) => k.length);
|
||||||
|
console.log(
|
||||||
|
`dictionary: ${Object.keys(sorted).length} entries` +
|
||||||
|
` (dropped ${dropped}), key length ${Math.min(...lengths)}-${Math.max(...lengths)}`,
|
||||||
|
);
|
||||||
|
|
@ -1,228 +0,0 @@
|
||||||
/**
|
|
||||||
* Comprehensive Chinese-to-English translation dictionary for DeepSeek Harness plugins
|
|
||||||
*/
|
|
||||||
export const DICTIONARY: Record<string, string> = {
|
|
||||||
// DSH Marketplace & Store
|
|
||||||
"插件市场": "Plugin Marketplace",
|
|
||||||
"逛一逛,点一下,装好。": "Browse, click, and install.",
|
|
||||||
"全部插件": "All Plugins",
|
|
||||||
"已安装插件": "Installed Plugins",
|
|
||||||
"插件": "Plugins",
|
|
||||||
"市场": "Marketplace",
|
|
||||||
"热门插件": "Popular Plugins",
|
|
||||||
"最新上架": "Recently Added",
|
|
||||||
"推荐插件": "Recommended Plugins",
|
|
||||||
"下载量": "Downloads",
|
|
||||||
"评分": "Rating",
|
|
||||||
"作者": "Author",
|
|
||||||
"安装量": "Installs",
|
|
||||||
"安装插件": "Install Plugin",
|
|
||||||
"卸载插件": "Uninstall Plugin",
|
|
||||||
"更新插件": "Update Plugin",
|
|
||||||
"第三方插件": "Third-Party Plugins",
|
|
||||||
"官方插件": "Official Plugins",
|
|
||||||
"搜索插件": "Search plugins...",
|
|
||||||
"搜索插件名称或作者": "Search by plugin name or author...",
|
|
||||||
"一键安装": "Install",
|
|
||||||
"一键更新": "Update",
|
|
||||||
"正在下载": "Downloading...",
|
|
||||||
"正在安装": "Installing...",
|
|
||||||
"安装完成": "Installed",
|
|
||||||
"卸载完成": "Uninstalled",
|
|
||||||
|
|
||||||
// Skills Manager Navigation & Tabs
|
|
||||||
"技能管理": "Skills Manager",
|
|
||||||
"技能管理器": "Skills Manager",
|
|
||||||
"统一加载和管理本机 Agent Skills。": "Load and manage Agent Skills on this machine in one place.",
|
|
||||||
"技能列表": "Skills List",
|
|
||||||
"已安装技能": "Installed Skills",
|
|
||||||
"可用技能": "Available Skills",
|
|
||||||
"发现技能": "Discover Skills",
|
|
||||||
"仓库订阅": "Repositories",
|
|
||||||
"仓库管理": "Repositories",
|
|
||||||
"技能来源": "Skill Sources",
|
|
||||||
"全局技能": "Global Skills",
|
|
||||||
"项目技能": "Project Skills",
|
|
||||||
"公共 Agent": "Shared Agent",
|
|
||||||
"项目 Agent": "Project Agent",
|
|
||||||
"项目 Skills": "Project Skills",
|
|
||||||
"会话技能": "Session Skills",
|
|
||||||
"回收站": "Trash",
|
|
||||||
"诊断异常": "Diagnostics",
|
|
||||||
"异常诊断": "Diagnostics",
|
|
||||||
"源文件只读": "Read-only",
|
|
||||||
"只读": "Read-only",
|
|
||||||
"创建位置": "Location",
|
|
||||||
"全局 DSH": "Global DSH",
|
|
||||||
"项目 DSH": "Project DSH",
|
|
||||||
"DSH 技能": "DSH Skills",
|
|
||||||
|
|
||||||
// Actions & Buttons
|
|
||||||
"创建技能": "Create Skill",
|
|
||||||
"创建新技能": "Create Skill",
|
|
||||||
"新建技能": "New Skill",
|
|
||||||
"立即创建": "Create Now",
|
|
||||||
"导入技能": "Import Skill",
|
|
||||||
"导出技能": "Export Skill",
|
|
||||||
"导入": "Import",
|
|
||||||
"导出": "Export",
|
|
||||||
"刷新": "Refresh",
|
|
||||||
"检查更新": "Check Updates",
|
|
||||||
"正在检查更新…": "Checking for updates...",
|
|
||||||
"正在检查更新...": "Checking for updates...",
|
|
||||||
"检查并更新": "Check & Update",
|
|
||||||
"更新": "Update",
|
|
||||||
"管理更新": "Manage Updates",
|
|
||||||
"立即更新": "Update Now",
|
|
||||||
"确认更新": "Confirm Update",
|
|
||||||
"回滚": "Rollback",
|
|
||||||
"确认回滚": "Confirm Rollback",
|
|
||||||
"恢复": "Restore",
|
|
||||||
"移到回收站": "Move to Trash",
|
|
||||||
"移入回收站": "Move to Trash",
|
|
||||||
"永久删除": "Delete Forever",
|
|
||||||
"清空回收站": "Empty Trash",
|
|
||||||
"删除": "Delete",
|
|
||||||
"启用": "Enable",
|
|
||||||
"停用": "Disable",
|
|
||||||
"已启用": "Enabled",
|
|
||||||
"已停用": "Disabled",
|
|
||||||
"全部启用": "Enable All",
|
|
||||||
"全部停用": "Disable All",
|
|
||||||
"启停来源": "Toggle Source",
|
|
||||||
"启停技能": "Toggle Skill",
|
|
||||||
"安装": "Install",
|
|
||||||
"已安装": "Installed",
|
|
||||||
"卸载": "Uninstall",
|
|
||||||
"查看详情": "View Details",
|
|
||||||
"查看说明": "View Docs",
|
|
||||||
"查看": "View",
|
|
||||||
"更多": "More",
|
|
||||||
"取消": "Cancel",
|
|
||||||
"保存": "Save",
|
|
||||||
"关闭": "Close",
|
|
||||||
"确定": "OK",
|
|
||||||
"复制": "Copy",
|
|
||||||
"已复制": "Copied",
|
|
||||||
"复制成功": "Copied",
|
|
||||||
"搜索": "Search",
|
|
||||||
"重试": "Retry",
|
|
||||||
"选择文件": "Choose File",
|
|
||||||
"选择文件夹": "Choose Folder",
|
|
||||||
"问题反馈": "Issues",
|
|
||||||
"设置": "Settings",
|
|
||||||
|
|
||||||
// Status & Filters
|
|
||||||
"全部来源": "All Sources",
|
|
||||||
"全部状态": "All Statuses",
|
|
||||||
"调用状态": "Invocation Status",
|
|
||||||
"状态": "Status",
|
|
||||||
"来源": "Source",
|
|
||||||
"被覆盖": "Shadowed",
|
|
||||||
"名称": "Name",
|
|
||||||
"版本": "Version",
|
|
||||||
"大小": "Size",
|
|
||||||
"类型": "Type",
|
|
||||||
"操作": "Actions",
|
|
||||||
"详情": "Details",
|
|
||||||
"描述": "Description",
|
|
||||||
"简介": "Description",
|
|
||||||
"正文": "Body",
|
|
||||||
"源文件": "Source File",
|
|
||||||
"更新时间": "Updated",
|
|
||||||
"创建时间": "Created",
|
|
||||||
"删除于": "Deleted at",
|
|
||||||
"来源:": "Source: ",
|
|
||||||
|
|
||||||
// Empty & Loading States
|
|
||||||
"正在加载技能…": "Loading skills...",
|
|
||||||
"正在加载技能...": "Loading skills...",
|
|
||||||
"正在加载…": "Loading...",
|
|
||||||
"正在加载...": "Loading...",
|
|
||||||
"加载中…": "Loading...",
|
|
||||||
"加载中...": "Loading...",
|
|
||||||
"正在读取说明…": "Loading documentation...",
|
|
||||||
"正在读取说明...": "Loading documentation...",
|
|
||||||
"没有匹配的技能。": "No matching skills.",
|
|
||||||
"没有匹配的技能": "No matching skills",
|
|
||||||
"该来源目录不存在或暂时没有技能。": "This source directory does not exist or has no skills.",
|
|
||||||
"回收站为空。": "Trash is empty.",
|
|
||||||
"回收站为空": "Trash is empty",
|
|
||||||
"暂无技能": "No skills available",
|
|
||||||
"暂无数据": "No data available",
|
|
||||||
"未提供简介": "No description provided",
|
|
||||||
"未发现诊断问题。": "No diagnostic issues found.",
|
|
||||||
|
|
||||||
// Upload & Import Modal
|
|
||||||
"拖拽技能到此处": "Drop skill here",
|
|
||||||
"支持 .zip、技能文件夹或单个 SKILL.md": "Supports .zip, a skill folder, or single SKILL.md",
|
|
||||||
"文件要求": "Requirements",
|
|
||||||
"压缩包或文件夹需包含 SKILL.md": "Archives or folders must contain SKILL.md",
|
|
||||||
"SKILL.md 需包含 YAML 格式的技能名称和描述": "SKILL.md must include YAML name and description",
|
|
||||||
"导入时复制完整内容,不修改原始来源": "Import copies full content without modifying source",
|
|
||||||
"导入到全局 DSH": "Import to global DSH",
|
|
||||||
"移除所选内容": "Remove selection",
|
|
||||||
"请选择 .zip 或单个 SKILL.md。": "Please choose a .zip or single SKILL.md.",
|
|
||||||
"所选文件夹中没有找到 SKILL.md。": "No SKILL.md found in the selected folder.",
|
|
||||||
|
|
||||||
// Creation Form
|
|
||||||
"例如 code-review-helper": "e.g. code-review-helper",
|
|
||||||
"一句话说明什么时候使用": "One sentence describing when to use it",
|
|
||||||
"正文(Markdown)": "Body (Markdown)",
|
|
||||||
"写下技能要遵循的指令、步骤和边界…": "Write the instructions, steps, and boundaries...",
|
|
||||||
"创建和导入均保存到全局 DSH,跨项目可用。": "Created and imported skills are saved to global DSH and available across projects.",
|
|
||||||
|
|
||||||
// General Settings & UI
|
|
||||||
"通用设置": "General Settings",
|
|
||||||
"模型设置": "Model Settings",
|
|
||||||
"外观设置": "Appearance",
|
|
||||||
"语言设置": "Language",
|
|
||||||
"快捷键": "Shortcuts",
|
|
||||||
"关于": "About",
|
|
||||||
"版本号": "Version",
|
|
||||||
"检查新版本": "Check for updates",
|
|
||||||
"语言": "Language",
|
|
||||||
"简体中文": "Simplified Chinese",
|
|
||||||
"英文": "English",
|
|
||||||
"深色模式": "Dark Mode",
|
|
||||||
"浅色模式": "Light Mode",
|
|
||||||
"跟随系统": "Follow System",
|
|
||||||
"退出": "Exit",
|
|
||||||
"重启": "Restart",
|
|
||||||
|
|
||||||
// Model & Chat Concepts
|
|
||||||
"模型": "Models",
|
|
||||||
"会话": "Sessions",
|
|
||||||
"新会话": "New Session",
|
|
||||||
"历史会话": "History",
|
|
||||||
"工作区": "Workspace",
|
|
||||||
"暂无会话": "No sessions",
|
|
||||||
"发送": "Send",
|
|
||||||
"停止生成": "Stop generating",
|
|
||||||
"重新生成": "Regenerate",
|
|
||||||
"耗时": "Duration",
|
|
||||||
"步骤": "Steps",
|
|
||||||
"轮次": "Turns",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sorted array of [chinese, english] pairs, longest Chinese phrases first
|
|
||||||
// (ensures compound phrases are matched before single words)
|
|
||||||
export const SORTED_PAIRS: Array<[string, string]> = Object.entries(DICTIONARY).sort(
|
|
||||||
(a, b) => b[0].length - a[0].length
|
|
||||||
);
|
|
||||||
|
|
||||||
// Dynamic pattern replacements
|
|
||||||
export const DYNAMIC_PATTERNS: Array<[RegExp, string]> = [
|
|
||||||
[/(\d+)\s*个技能/g, "$1 skills"],
|
|
||||||
[/(\d+)\s*个已启用/g, "$1 enabled"],
|
|
||||||
[/(\d+)\s*个已停用/g, "$1 disabled"],
|
|
||||||
[/(\d+)\s*个被覆盖/g, "$1 shadowed"],
|
|
||||||
[/(\d+)\s*个诊断异常/g, "$1 issues"],
|
|
||||||
[/(\d+)\s*个文件/g, "$1 files"],
|
|
||||||
[/(\d+)\s*个待处理技能/g, "$1 pending skills"],
|
|
||||||
[/来源:/g, "Source: "],
|
|
||||||
[/创建于\s*([0-9:\-\/ ]+)/g, "Created at $1"],
|
|
||||||
[/更新于\s*([0-9:\-\/ ]+)/g, "Updated at $1"],
|
|
||||||
[/删除于\s*([0-9:\-\/ ]+)/g, "Deleted at $1"],
|
|
||||||
[/被\s*(.+?)\s*覆盖/g, "Shadowed by $1"],
|
|
||||||
];
|
|
||||||
|
|
@ -1,264 +1,40 @@
|
||||||
import { DICTIONARY, SORTED_PAIRS, DYNAMIC_PATTERNS } from "./dictionary.js";
|
export const name = "chinese2english";
|
||||||
|
|
||||||
export const name = "chinese2english-client";
|
|
||||||
export const inject = ["locale"];
|
export const inject = ["locale"];
|
||||||
|
|
||||||
const CHINESE_REGEX = /[\u4e00-\u9fa5]/;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates a text string safely without corrupting sentences:
|
* Pins the UI language to English.
|
||||||
* 1. Checks exact match on the entire string first.
|
*
|
||||||
* 2. Matches full dynamic count/date patterns.
|
* Hardcoded Chinese inside plugin bundles is handled on the server by the
|
||||||
* 3. Matches multi-word phrases from longest to shortest (never single characters).
|
* bundle-transport rewrite in `src/index.ts`. Text that flows through the
|
||||||
|
* locale service is only English when the active locale is `en`, so this half
|
||||||
|
* makes that selection explicit and durable. Walking the DOM is deliberately
|
||||||
|
* avoided: it corrupts React trees and cannot reach text that arrives as data.
|
||||||
*/
|
*/
|
||||||
export function translateText(text: string): string {
|
export function apply(ctx: any) {
|
||||||
if (!text || !CHINESE_REGEX.test(text)) return text;
|
const locale = ctx.locale;
|
||||||
|
if (!locale || typeof locale.setLocale !== "function") {
|
||||||
const trimmed = text.trim();
|
console.warn("[chinese2english] locale service unavailable; UI language left unchanged");
|
||||||
// 1. Direct exact dictionary match (highest fidelity)
|
|
||||||
if (DICTIONARY[trimmed]) {
|
|
||||||
return text.replace(trimmed, DICTIONARY[trimmed]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Dynamic patterns (e.g. "3 个已启用" -> "3 enabled")
|
|
||||||
for (const [pattern, replacement] of DYNAMIC_PATTERNS) {
|
|
||||||
if (pattern.test(trimmed)) {
|
|
||||||
return text.replace(trimmed, trimmed.replace(pattern, replacement));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Multi-word phrase matches (2+ characters only, longest first)
|
|
||||||
let result = text;
|
|
||||||
let changed = false;
|
|
||||||
for (const [cn, en] of SORTED_PAIRS) {
|
|
||||||
if (cn.length >= 2 && result.includes(cn)) {
|
|
||||||
result = result.replaceAll(cn, en);
|
|
||||||
changed = true;
|
|
||||||
if (!CHINESE_REGEX.test(result)) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return changed ? result : text;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isTranslating = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Traverses elements using TreeWalker and translates whole text nodes.
|
|
||||||
*/
|
|
||||||
export function translateElement(root: Node) {
|
|
||||||
if (!root) return;
|
|
||||||
|
|
||||||
if (root.nodeType === Node.TEXT_NODE) {
|
|
||||||
const val = root.nodeValue;
|
|
||||||
if (val && CHINESE_REGEX.test(val)) {
|
|
||||||
const t = translateText(val);
|
|
||||||
if (t !== val) root.nodeValue = t;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.nodeType !== Node.ELEMENT_NODE) return;
|
const pin = (reason: string) => {
|
||||||
const el = root as HTMLElement;
|
|
||||||
|
|
||||||
const tag = el.tagName;
|
|
||||||
// Ignore code, pre, scripts, styles
|
|
||||||
if (tag === "SCRIPT" || tag === "STYLE" || tag === "PRE" || tag === "CODE") return;
|
|
||||||
|
|
||||||
// Attributes (placeholder, title, aria-label)
|
|
||||||
for (const attr of ["placeholder", "title", "aria-label", "alt"]) {
|
|
||||||
const val = el.getAttribute(attr);
|
|
||||||
if (val && CHINESE_REGEX.test(val)) {
|
|
||||||
el.setAttribute(attr, translateText(val));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fast native TreeWalker
|
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
|
|
||||||
let textNode = walker.nextNode();
|
|
||||||
while (textNode) {
|
|
||||||
const val = textNode.nodeValue;
|
|
||||||
if (val && CHINESE_REGEX.test(val)) {
|
|
||||||
const parent = textNode.parentElement;
|
|
||||||
if (parent) {
|
|
||||||
const pTag = parent.tagName;
|
|
||||||
if (pTag !== "SCRIPT" && pTag !== "STYLE" && pTag !== "PRE" && pTag !== "CODE") {
|
|
||||||
const t = translateText(val);
|
|
||||||
if (t !== val) textNode.nodeValue = t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
textNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startDOMTranslator() {
|
|
||||||
if (typeof document === "undefined") return;
|
|
||||||
|
|
||||||
// Force document language to English immediately
|
|
||||||
document.documentElement.lang = "en";
|
|
||||||
|
|
||||||
const queue = new Set<Node>();
|
|
||||||
let frameRequested = false;
|
|
||||||
|
|
||||||
const flush = () => {
|
|
||||||
frameRequested = false;
|
|
||||||
isTranslating = true;
|
|
||||||
try {
|
try {
|
||||||
const nodes = Array.from(queue);
|
if (locale.getLocale?.().active === "en") return;
|
||||||
queue.clear();
|
locale.setLocale("en");
|
||||||
for (const n of nodes) {
|
} catch (error) {
|
||||||
if (n.isConnected) translateElement(n);
|
console.error(`[chinese2english] could not select English (${reason}):`, error);
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isTranslating = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
pin("initial");
|
||||||
if (isTranslating) return;
|
|
||||||
|
|
||||||
for (const m of mutations) {
|
// A late language pack or an external preference can republish the snapshot
|
||||||
if (m.type === "childList") {
|
// with another active locale; re-assert English when that happens.
|
||||||
for (let i = 0; i < m.addedNodes.length; i++) {
|
ctx.effect(
|
||||||
queue.add(m.addedNodes[i]);
|
() =>
|
||||||
}
|
ctx.on("locale/change", (snapshot: any) => {
|
||||||
} else if (m.type === "characterData") {
|
if (snapshot?.active !== "en") pin("locale change");
|
||||||
if (m.target.nodeValue && CHINESE_REGEX.test(m.target.nodeValue)) {
|
}),
|
||||||
queue.add(m.target);
|
"chinese2english: keep the UI locale on English",
|
||||||
}
|
);
|
||||||
} else if (m.type === "attributes") {
|
|
||||||
queue.add(m.target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!frameRequested && queue.size > 0) {
|
|
||||||
frameRequested = true;
|
|
||||||
requestAnimationFrame(flush);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const root = document.documentElement || document.body;
|
|
||||||
if (root) {
|
|
||||||
translateElement(root);
|
|
||||||
observer.observe(root, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
characterData: true,
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["placeholder", "title", "aria-label", "alt"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Periodic safety check every 1s to sweep any lazily mounted modal content
|
|
||||||
setInterval(() => {
|
|
||||||
if (document.body && !isTranslating) {
|
|
||||||
isTranslating = true;
|
|
||||||
try {
|
|
||||||
translateElement(document.body);
|
|
||||||
} finally {
|
|
||||||
isTranslating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force English locale in localStorage across known plugin keys
|
|
||||||
function enforceEnglishStorage() {
|
|
||||||
if (typeof window === "undefined" || !window.localStorage) return;
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
const key = localStorage.key(i);
|
|
||||||
if (key && (key.includes("lang") || key.includes("locale") || key.includes("all-usage") || key.includes("dsh"))) {
|
|
||||||
const val = localStorage.getItem(key);
|
|
||||||
if (val === "zh" || val === "zh-CN") {
|
|
||||||
localStorage.setItem(key, "en");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function apply(ctx: any) {
|
|
||||||
console.log("[chinese2english] Activating English locale & translation engine...");
|
|
||||||
|
|
||||||
enforceEnglishStorage();
|
|
||||||
|
|
||||||
// 1. Force DSH core locale service to English
|
|
||||||
if (ctx.locale) {
|
|
||||||
try {
|
|
||||||
// Force active locale to "en"
|
|
||||||
if (typeof ctx.locale.setLocale === "function") {
|
|
||||||
ctx.locale.setLocale("en");
|
|
||||||
}
|
|
||||||
if (typeof ctx.locale.resolveActive === "function") {
|
|
||||||
ctx.locale.resolveActive = () => "en";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intercept snapshot
|
|
||||||
if (typeof ctx.locale.getSnapshot === "function") {
|
|
||||||
const origSnap = ctx.locale.getSnapshot.bind(ctx.locale);
|
|
||||||
ctx.locale.getSnapshot = () => {
|
|
||||||
const s = origSnap();
|
|
||||||
return s ? { ...s, active: "en" } : s;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish active: "en" to trigger all native bilingual plugin updates
|
|
||||||
if (typeof ctx.locale.publish === "function") {
|
|
||||||
ctx.locale.publish("en", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intercept translate function
|
|
||||||
if (typeof ctx.locale.translate === "function") {
|
|
||||||
const origTranslate = ctx.locale.translate.bind(ctx.locale);
|
|
||||||
ctx.locale.translate = function (ns: string, key: string, params: any) {
|
|
||||||
const val = origTranslate(ns, key, params);
|
|
||||||
if (typeof val === "string" && CHINESE_REGEX.test(val)) {
|
|
||||||
return translateText(val);
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-patch all plugin registered dictionaries with English translations
|
|
||||||
if (ctx.locale.dicts && typeof ctx.locale.dicts.entries === "function") {
|
|
||||||
for (const [ns, localesMap] of ctx.locale.dicts.entries()) {
|
|
||||||
const zh = localesMap?.get("zh");
|
|
||||||
let en = localesMap?.get("en");
|
|
||||||
if (!en) {
|
|
||||||
en = {};
|
|
||||||
localesMap?.set("en", en);
|
|
||||||
}
|
|
||||||
if (zh) {
|
|
||||||
for (const [k, v] of Object.entries(zh)) {
|
|
||||||
if ((!en[k] || en[k] === k || CHINESE_REGEX.test(en[k])) && typeof v === "string") {
|
|
||||||
en[k] = translateText(v) || v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[dsh-plugin-english] Locale patch error:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Keep document.documentElement.lang set to "en"
|
|
||||||
if (typeof document !== "undefined") {
|
|
||||||
document.documentElement.lang = "en";
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
if (document.documentElement.lang !== "en") {
|
|
||||||
document.documentElement.lang = "en";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. DOM translation engine (disabled: direct React DOM mutation causes Market UI crash)
|
|
||||||
// startDOMTranslator();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start immediately on script load as well
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
enforceEnglishStorage();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
402
src/dictionary.extra.json
Normal file
402
src/dictionary.extra.json
Normal file
|
|
@ -0,0 +1,402 @@
|
||||||
|
{
|
||||||
|
"输出": "Output",
|
||||||
|
"运行中": "Running",
|
||||||
|
"输入": "Input",
|
||||||
|
"失败": "Failed",
|
||||||
|
"移除": "Remove",
|
||||||
|
"恢复默认": "Restore Defaults",
|
||||||
|
"缓存命中": "Cache Hit",
|
||||||
|
"已删除": "Deleted",
|
||||||
|
"推理": "Reasoning",
|
||||||
|
"文档渲染中...": "Rendering document...",
|
||||||
|
"进行中": "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.",
|
||||||
|
"等待审批": "Awaiting approval",
|
||||||
|
"图片预览": "Image preview",
|
||||||
|
"第 {turn} 轮": "Turn {turn}",
|
||||||
|
"简洁": "Concise",
|
||||||
|
"详细": "Detailed",
|
||||||
|
"标准": "Standard",
|
||||||
|
"已添加工具:{name}": "Tool added: {name}",
|
||||||
|
"已移除工具:{name}": "Tool removed: {name}",
|
||||||
|
"新增:{names}": "Added: {names}",
|
||||||
|
"新增 {count} 个": "Added {count}",
|
||||||
|
"新增 {added} 个,移除 {removed} 个": "Added {added}, removed {removed}",
|
||||||
|
"移除:{names}": "Removed: {names}",
|
||||||
|
"移除 {count} 个": "Removed {count}",
|
||||||
|
"已移除": "Removed",
|
||||||
|
"上下文已压缩": "Context compacted",
|
||||||
|
"{count} 次工具调用": "{count} tool calls",
|
||||||
|
"{count} 条消息": "{count} messages",
|
||||||
|
"{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} background tasks: {names}",
|
||||||
|
"{n} 条定时提醒:{names}": "{n} scheduled reminders: {names}",
|
||||||
|
"{n} 项其他工作({kind})": "{n} other jobs ({kind})",
|
||||||
|
"撤销": "Undo",
|
||||||
|
"{n} 个会话": "{n} sessions",
|
||||||
|
"{n} 个子智能体运行中": "{n} subagents running",
|
||||||
|
"{n}分钟": "{n} minutes",
|
||||||
|
"{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",
|
||||||
|
"退出当前账号": "Sign out of this account",
|
||||||
|
"退出并登录其它账号": "Sign out and sign in to another account",
|
||||||
|
"折叠": "Collapse",
|
||||||
|
"未知原因": "Unknown reason",
|
||||||
|
"已捕获并保存凭证。": "Credentials captured and saved.",
|
||||||
|
"连通性测试": "Connectivity test",
|
||||||
|
"(未获取到账号信息)": "(no account information available)",
|
||||||
|
"凭证来源": "Credential source",
|
||||||
|
"指纹清理": "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",
|
||||||
|
"自定义规则": "Custom rules",
|
||||||
|
"删除时段": "Delete period",
|
||||||
|
"删除规则": "Delete rule",
|
||||||
|
"当前进程令牌不可用,请刷新看板": "The current process token is unavailable; refresh the dashboard",
|
||||||
|
"删除费率档位": "Delete rate tier",
|
||||||
|
"输入口径": "Input basis",
|
||||||
|
"成本统计设置": "Cost statistics settings",
|
||||||
|
"完整费率设置加载失败": "Failed to load the full rate settings",
|
||||||
|
" 至 ": " to ",
|
||||||
|
"近 90 天": "Last 90 days",
|
||||||
|
"自定义时间范围": "Custom time range",
|
||||||
|
"界面语言": "Interface language",
|
||||||
|
"推理 Token": "Reasoning tokens",
|
||||||
|
"总处理 Token": "Total processed tokens",
|
||||||
|
"工作区筛选": "Workspace filter",
|
||||||
|
"供应商筛选": "Provider filter",
|
||||||
|
"模型筛选": "Model filter",
|
||||||
|
"刷新统计数据": "Refresh statistics",
|
||||||
|
"全部工作区": "All workspaces",
|
||||||
|
"全部供应商": "All providers",
|
||||||
|
"清除筛选": "Clear filters",
|
||||||
|
"命中率": "Hit rate",
|
||||||
|
"关闭用量统计": "Close usage statistics",
|
||||||
|
"收录时间": "Listed on",
|
||||||
|
"可用": "Available",
|
||||||
|
"暂不可用": "Temporarily unavailable",
|
||||||
|
"模型清单": "Model catalog",
|
||||||
|
"网关暂不可达": "Gateway unreachable",
|
||||||
|
"当前出口": "Current egress",
|
||||||
|
"测量中…": "Measuring…",
|
||||||
|
"请求数": "Requests",
|
||||||
|
"监听地址": "Listen address",
|
||||||
|
"端口": "Port",
|
||||||
|
"显示": "Show",
|
||||||
|
"隐藏": "Hide",
|
||||||
|
"插件升级": "Plugin upgrade",
|
||||||
|
"免费模型": "Free models",
|
||||||
|
"EAC 模型": "EAC models",
|
||||||
|
"白嫖模型接入": "Free model access",
|
||||||
|
"免费额度": "Free quota",
|
||||||
|
"数据看板": "Dashboard",
|
||||||
|
"运行日志": "Runtime log",
|
||||||
|
"网关设置": "Gateway settings",
|
||||||
|
"聚合渠道": "Aggregated channels",
|
||||||
|
"省 Token": "Save tokens",
|
||||||
|
"路由": "Routing",
|
||||||
|
"阿里系": "Alibaba family",
|
||||||
|
"手机号登录": "Phone number login",
|
||||||
|
"复制提示词": "Copy prompt",
|
||||||
|
"放弃修改": "Discard changes",
|
||||||
|
"复制 HTML": "Copy HTML",
|
||||||
|
"下载图像": "Download image",
|
||||||
|
"用 {app} 打开": "Open with {app}",
|
||||||
|
"{app}(默认)": "{app} (default)",
|
||||||
|
"无法获取应用列表": "Could not retrieve the application list",
|
||||||
|
"正在打开工作区": "Opening workspace",
|
||||||
|
"当前工作区或本地应用不可用": "The current workspace or local application is unavailable",
|
||||||
|
"在本地打开": "Open locally",
|
||||||
|
"更多打开方式": "More ways to open",
|
||||||
|
"显示文件位置": "Show file location",
|
||||||
|
"打开失败,请重试": "Open failed; try again",
|
||||||
|
"无法显示文件位置,请重试": "Could not show the file location; try again",
|
||||||
|
"访达": "Finder"
|
||||||
|
}
|
||||||
1197
src/dictionary.json
Normal file
1197
src/dictionary.json
Normal file
File diff suppressed because it is too large
Load diff
86
src/index.ts
86
src/index.ts
|
|
@ -1,6 +1,84 @@
|
||||||
export const name = "chinese2english";
|
import { translateScript } from "./rewrite.js";
|
||||||
export const inject = [];
|
|
||||||
|
|
||||||
export function apply(ctx: any) {
|
export const name = "chinese2english";
|
||||||
// Service initialized
|
export const inject = ["clientModules"];
|
||||||
|
|
||||||
|
const JS_TYPE = /^text\/javascript\b/i;
|
||||||
|
// Combo requests look like `/plugins/??a/client.js,b/client.js&rev=...`: the
|
||||||
|
// path is only `/plugins/`, so the file names live in the query string and the
|
||||||
|
// test has to run against the whole URL. Source maps (`client.js.map`) do not
|
||||||
|
// match because `.js` is followed by `.`, not a separator.
|
||||||
|
const JS_URL = /\.js(?:[,&?]|$)/;
|
||||||
|
const MAX_BODY = 16 * 1024 * 1024;
|
||||||
|
const MAX_CACHE = 64;
|
||||||
|
|
||||||
|
function isScriptUrl(url: string): boolean {
|
||||||
|
return JS_URL.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates Chinese UI text at the bundle transport layer.
|
||||||
|
*
|
||||||
|
* 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;
|
||||||
|
if (!registry || typeof registry.bundleResource !== "function") {
|
||||||
|
console.error("[chinese2english] clientModules.bundleResource unavailable; translation disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const original = registry.bundleResource;
|
||||||
|
const cache = new Map<string, Buffer | string>();
|
||||||
|
|
||||||
|
registry.bundleResource = async function (method: string, url: string, ...rest: any[]) {
|
||||||
|
const response = await original.call(this, method, url, ...rest);
|
||||||
|
|
||||||
|
if (response?.status !== 200) return response;
|
||||||
|
if (!isScriptUrl(url)) return response;
|
||||||
|
|
||||||
|
const raw = response.body;
|
||||||
|
const isBuffer = Buffer.isBuffer(raw);
|
||||||
|
if (!isBuffer && typeof raw !== "string") return response;
|
||||||
|
|
||||||
|
const contentType = response.headers?.["content-type"];
|
||||||
|
if (typeof contentType !== "string" || !JS_TYPE.test(contentType)) return response;
|
||||||
|
if (raw.length > MAX_BODY) return response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let body = cache.get(url);
|
||||||
|
if (body === undefined) {
|
||||||
|
const source = isBuffer ? raw.toString("utf8") : raw;
|
||||||
|
const rewritten = translateScript(source);
|
||||||
|
// Preserve the incoming body type: the Web route accepts either, but
|
||||||
|
// `fetchBundle` hands the value to `Uint8Array.from`.
|
||||||
|
body = isBuffer ? Buffer.from(rewritten, "utf8") : rewritten;
|
||||||
|
if (cache.size >= MAX_CACHE) {
|
||||||
|
const oldest = cache.keys().next();
|
||||||
|
if (!oldest.done) cache.delete(oldest.value);
|
||||||
|
}
|
||||||
|
cache.set(url, body);
|
||||||
|
}
|
||||||
|
if (method === "HEAD") return response;
|
||||||
|
if (typeof body === "string") {
|
||||||
|
if (body === raw) return response;
|
||||||
|
} else if (body.length === raw.length && (!isBuffer || body.equals(raw))) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
headers: { ...response.headers, "cache-control": "no-cache" },
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[chinese2english] could not translate ${url}:`, error);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
118
src/rewrite.ts
Normal file
118
src/rewrite.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import DICTIONARY from "./dictionary.json";
|
||||||
|
import { decodeRaw, encodeValue, isObjectKeyPosition, scanLiterals } from "./strings.js";
|
||||||
|
|
||||||
|
const CHINESE = /[\u4e00-\u9fa5]/;
|
||||||
|
|
||||||
|
// Longest key first so the segmentation below prefers `缓存命中率` over `缓存命中`.
|
||||||
|
const KEYS = Object.keys(DICTIONARY).sort((a, b) => b.length - a.length);
|
||||||
|
|
||||||
|
// Keys grouped by their first character: the cover search only ever needs to
|
||||||
|
// try keys that can actually start at the current offset.
|
||||||
|
const BY_FIRST = new Map<string, string[]>();
|
||||||
|
for (const key of KEYS) {
|
||||||
|
const first = key[0];
|
||||||
|
const bucket = BY_FIRST.get(first);
|
||||||
|
if (bucket) bucket.push(key);
|
||||||
|
else BY_FIRST.set(first, [key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a Chinese string only when the whole string is accounted for.
|
||||||
|
*
|
||||||
|
* Chinese has no word boundaries, so replacing known keys wherever they occur
|
||||||
|
* rewrites fragments of larger words: `当前工作区` would become
|
||||||
|
* `Saat IniWorkspace`. Instead the string is segmented left to right, and a
|
||||||
|
* translation is emitted only when every Chinese character is covered by a
|
||||||
|
* dictionary key. Anything else is left in Chinese, because half-translated
|
||||||
|
* text is worse than untranslated text.
|
||||||
|
*/
|
||||||
|
export function substitute(value: string): string {
|
||||||
|
if (!CHINESE.test(value)) return value;
|
||||||
|
if (DICTIONARY[value] !== undefined) return DICTIONARY[value];
|
||||||
|
|
||||||
|
const pieces: string[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < value.length) {
|
||||||
|
const char = value[i];
|
||||||
|
if (!CHINESE.test(char)) {
|
||||||
|
// Non-Chinese glue (spaces, ASCII, punctuation, placeholders) is copied
|
||||||
|
// through; it never blocks coverage on its own.
|
||||||
|
let j = i;
|
||||||
|
while (j < value.length && !CHINESE.test(value[j])) j++;
|
||||||
|
pieces.push(value.slice(i, j));
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = BY_FIRST.get(char);
|
||||||
|
let match: string | undefined;
|
||||||
|
if (candidates) {
|
||||||
|
for (const key of candidates) {
|
||||||
|
if (value.startsWith(key, i)) {
|
||||||
|
match = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match === undefined) return value;
|
||||||
|
pieces.push(DICTIONARY[match]);
|
||||||
|
i += match.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pieces.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasChinese(text: string): boolean {
|
||||||
|
return CHINESE.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrites Chinese UI text inside JavaScript string literals.
|
||||||
|
*
|
||||||
|
* Returns the original string unchanged when nothing was translated, so the
|
||||||
|
* caller can skip rebuilding the response. Object keys are deliberately left
|
||||||
|
* alone: translating `{ "中文": 1 }` would break the lookup that reads it.
|
||||||
|
*/
|
||||||
|
export function rewriteScript(source: string): string {
|
||||||
|
if (!hasChinese(source)) return source;
|
||||||
|
|
||||||
|
const spans = scanLiterals(source);
|
||||||
|
let out = "";
|
||||||
|
let cursor = 0;
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
for (const span of spans) {
|
||||||
|
if (span.quote === "`") continue;
|
||||||
|
if (span.raw.includes("${")) continue;
|
||||||
|
if (isObjectKeyPosition(source, span)) continue;
|
||||||
|
|
||||||
|
const value = decodeRaw(span.raw);
|
||||||
|
if (!hasChinese(value)) continue;
|
||||||
|
|
||||||
|
const next = substitute(value);
|
||||||
|
if (next === value) continue;
|
||||||
|
|
||||||
|
out += source.slice(cursor, span.start) + span.quote + encodeValue(next, span.quote) + span.quote;
|
||||||
|
cursor = span.end;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed ? out + source.slice(cursor) : source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compiles the candidate so a bad rewrite can never reach the browser. */
|
||||||
|
export function compiles(source: string): boolean {
|
||||||
|
try {
|
||||||
|
new Function(source);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function translateScript(source: string): string {
|
||||||
|
const next = rewriteScript(source);
|
||||||
|
if (next === source) return source;
|
||||||
|
return compiles(next) ? next : source;
|
||||||
|
}
|
||||||
150
src/strings.ts
Normal file
150
src/strings.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
// String-literal scanner: finds JavaScript string/template literals without a
|
||||||
|
// parser, skipping comments and regex literals so their contents stay intact.
|
||||||
|
|
||||||
|
const REGEX_PREFIX = new Set([
|
||||||
|
"", "(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "+", "-", "*", "%", "~", "^", "<", ">", "\n",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export interface LiteralSpan {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
quote: string;
|
||||||
|
raw: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scanLiterals(src: string): LiteralSpan[] {
|
||||||
|
const out: LiteralSpan[] = [];
|
||||||
|
const n = src.length;
|
||||||
|
let i = 0;
|
||||||
|
let lastSig = "";
|
||||||
|
while (i < n) {
|
||||||
|
const c = src[i];
|
||||||
|
if (c === "/" && src[i + 1] === "/") {
|
||||||
|
const e = src.indexOf("\n", i);
|
||||||
|
i = e < 0 ? n : e + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "/" && src[i + 1] === "*") {
|
||||||
|
const e = src.indexOf("*/", i + 2);
|
||||||
|
i = e < 0 ? n : e + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "/" && REGEX_PREFIX.has(lastSig)) {
|
||||||
|
let j = i + 1;
|
||||||
|
let inClass = false;
|
||||||
|
let ok = false;
|
||||||
|
while (j < n) {
|
||||||
|
const d = src[j];
|
||||||
|
if (d === "\\") {
|
||||||
|
j += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d === "\n") break;
|
||||||
|
if (d === "[") inClass = true;
|
||||||
|
else if (d === "]") inClass = false;
|
||||||
|
else if (d === "/" && !inClass) {
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
i = j + 1;
|
||||||
|
lastSig = "/";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c === '"' || c === "'" || c === "`") {
|
||||||
|
let j = i + 1;
|
||||||
|
while (j < n) {
|
||||||
|
const d = src[j];
|
||||||
|
if (d === "\\") {
|
||||||
|
j += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d === c) break;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
if (j < n) {
|
||||||
|
out.push({ start: i, end: j + 1, quote: c, raw: src.slice(i + 1, j) });
|
||||||
|
i = j + 1;
|
||||||
|
lastSig = c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i = n;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!/\s/.test(c)) lastSig = c;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeRaw(raw: string): string {
|
||||||
|
if (!raw.includes("\\")) return raw;
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < raw.length; i++) {
|
||||||
|
const c = raw[i];
|
||||||
|
if (c !== "\\") {
|
||||||
|
out += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const d = raw[++i];
|
||||||
|
switch (d) {
|
||||||
|
case "n": out += "\n"; break;
|
||||||
|
case "r": out += "\r"; break;
|
||||||
|
case "t": out += "\t"; break;
|
||||||
|
case "b": out += "\b"; break;
|
||||||
|
case "f": out += "\f"; break;
|
||||||
|
case "v": out += "\v"; break;
|
||||||
|
case "0": out += "\0"; break;
|
||||||
|
case "x": out += String.fromCharCode(parseInt(raw.slice(i + 1, i + 3), 16)); i += 2; break;
|
||||||
|
case "u":
|
||||||
|
if (raw[i + 1] === "{") {
|
||||||
|
const close = raw.indexOf("}", i + 2);
|
||||||
|
if (close < 0) { out += "u"; break; }
|
||||||
|
out += String.fromCodePoint(parseInt(raw.slice(i + 2, close), 16));
|
||||||
|
i = close;
|
||||||
|
} else {
|
||||||
|
out += String.fromCharCode(parseInt(raw.slice(i + 1, i + 5), 16));
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "\n": break;
|
||||||
|
case "\r": if (raw[i + 1] === "\n") i++; break;
|
||||||
|
default: out += d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeValue(value: string, quote: string): string {
|
||||||
|
let out = "";
|
||||||
|
for (const ch of value) {
|
||||||
|
if (ch === "\\") out += "\\\\";
|
||||||
|
else if (ch === quote) out += "\\" + quote;
|
||||||
|
else if (ch === "\n") out += "\\n";
|
||||||
|
else if (ch === "\r") out += "\\r";
|
||||||
|
else if (ch === "\u2028") out += "\\u2028";
|
||||||
|
else if (ch === "\u2029") out += "\\u2029";
|
||||||
|
else out += ch;
|
||||||
|
}
|
||||||
|
if (quote === "`") out = out.replace(/\$\{/g, "\\${");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The character before this literal, ignoring whitespace/comments-free trivia.
|
||||||
|
// Used to spot a genuine object key (`{ "中文": …`) and leave it alone.
|
||||||
|
export function precedingSignificant(src: string, index: number): string {
|
||||||
|
let i = index - 1;
|
||||||
|
while (i >= 0 && /\s/.test(src[i])) i--;
|
||||||
|
return i < 0 ? "" : src[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObjectKeyPosition(src: string, span: LiteralSpan): boolean {
|
||||||
|
const prev = precedingSignificant(src, span.start);
|
||||||
|
if (prev !== "{" && prev !== ",") return false;
|
||||||
|
let j = span.end;
|
||||||
|
while (j < src.length && /\s/.test(src[j])) j++;
|
||||||
|
return src[j] === ":";
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue