dsh-plugin-labels 0.0.0-stage → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +177 -3
- package/cordis.patch.yml +4 -0
- package/icon.svg +5 -0
- package/lib/client.js +407 -0
- package/lib/index.js +1084 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -6
- package/scripts/gen-doc.mjs +306 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qyy2005
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,177 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# dsh-plugin-labels
|
|
2
|
+
|
|
3
|
+
[](https://github.com/qyy2005/dsh-plugin-labels/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
给 DSH(DeepSeek Harness)**已安装插件**配「中文名 + 一句话说明」:官方「插件」页的已安装插件行、插件包卡片与详情页都会**原地显示中文名与中文描述**(由 host 覆盖显示元数据);新装插件会在启动后自动翻译。**本插件没有任何 UI 入口**(没有 tab、没有设置项),管理通过 loopback HTTP API 或让 AI 代改。
|
|
7
|
+
|
|
8
|
+
- 当前版本:**0.8.0**(本仓库即源码目录;通过 `plugin_manager install_bundle` 从 GitHub 或 npm 安装到 DSH profile)
|
|
9
|
+
- 只支持 DSH 桌面版 Web GUI(开发验证版本 0.2.1-rc.2)
|
|
10
|
+
|
|
11
|
+
## 功能
|
|
12
|
+
|
|
13
|
+
- **中文名 + 一句话说明**:每个插件一条中文名(≤10 字)和一句说明(≤80 字);说明必须写清**怎么用**,不只是“是什么”(见下方「标签格式」)。
|
|
14
|
+
- **官方页原地中文**:官方「插件」页的已安装插件行、点开的详情页,以及插件包卡片(`listBundles`)都会直接显示中文名与中文描述;未翻译的插件回退英文。默认开启。
|
|
15
|
+
- **自动翻译新插件**:host 启动约 4 秒后自动补一次缺失标签(每次最多 20 条,只补空缺、不覆盖手改),不用点任何按钮;可用 `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` 关闭。
|
|
16
|
+
- **手改优先**:手动设置的标签不会被自动翻译覆盖;显式“重翻”才会覆盖。
|
|
17
|
+
- **无 UI 管理入口**:通过 4 个 loopback HTTP API(见「使用」)或让 AI/脚本直接调用它们来翻译、手改、重翻、导入导出。
|
|
18
|
+
- **导出 / 导入 API**:导出带 `source`(`ai` / `manual`)的 JSON,导入时保留来源,只接受已安装插件名(≤64KB)。
|
|
19
|
+
- **缓存 v2**:标签缓存在 `<DSH_HOME>/plugin-labels.json`,UTF-8 无 BOM、原子写,旧 v1 数据可迁移读取。
|
|
20
|
+
|
|
21
|
+
### 标签格式
|
|
22
|
+
|
|
23
|
+
- **中文名**:≤10 个汉字,命名它“做什么”,不直译包名、不用营销词。
|
|
24
|
+
- **一句话说明**:≤80 字,必须同时回答:①这插件解决什么问题;②在 DSH 里从哪里进入 / 怎么操作(例如:设置 → 某个分区、侧栏 → 某个页面、在对话里下指令、装好自动生效);③必要时给一句可执行的第一步。
|
|
25
|
+
- 模型无法从包名/描述判断入口时,会被要求写「**入口以界面为准**」并给出用户能做的第一步;**不会编造不存在的菜单或按钮**。
|
|
26
|
+
|
|
27
|
+
## 界面
|
|
28
|
+
|
|
29
|
+
官方「插件」页的已安装插件行、插件包卡片与详情页直接显示中文名与中文描述(v0.6.0+,v0.7.0 起补上包级卡片):
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
v0.7.0 起插件**没有任何 UI 入口**:没有「中文名」tab,也没有设置项。中文名由 host 覆盖官方显示元数据实现,管理与编辑走 HTTP API(见「使用」)。
|
|
34
|
+
|
|
35
|
+
## 官方插件页原地显示中文
|
|
36
|
+
|
|
37
|
+
v0.6.0 起,host 半边会在运行时包装宿主内部的显示元数据,把标签缓存里的中文名与一句话说明作为 `{en, zh}` 覆盖到官方页面上。**这是纯显示层覆盖**:不改任何插件包文件,也不影响包身份、安装、更新与卸载;**默认开启**。v0.6.1 起改用 symbol 标记识别 wrapper(Cordis 的 `ctx.get` 返回 traceable Proxy、函数引用每次不同),修复了 `patched` 状态误判与重复包装;v0.7.0 起同时包装 `pluginManager.listBundles()`,让插件包卡片也显示中文。
|
|
38
|
+
|
|
39
|
+
- **两处覆盖**:`pluginPackages.metaOf()`(官方「插件」页的已安装插件行 + 详情页标题/描述)和 `pluginManager.listBundles()`(插件包卡片,它本来直接读 `readPluginMeta`)。
|
|
40
|
+
- **效果**:官方页面上的英文名原地显示为中文名,描述显示中文一句话;没有标签的插件回退英文(与原行为一致)。
|
|
41
|
+
- **原理**:官方页面通过 `pluginPackages.metaOf(specifier, parentURL)` 读取每个包的显示元数据(它会读该包 `locale/*.json` 的 `meta`,缺省回退 `package.json` 的 name/description);插件包卡片则由 `pluginManager.listBundles()` 直接 `readPluginMeta`。官方没有行级扩展点,所以这里在进程内包装这两个方法,用标签缓存补上 `title`/`description` 的 `{en, zh}` 语言映射,其余字段(如 `icon`、`error`)原样保留。
|
|
42
|
+
- **失败安全**:拿不到服务、方法不是函数、赋值失败或读缓存失败时,全部静默 warn 并回退宿主原返回值;包装可被 disposer 恢复,也能在服务实例被替换(HMR/刷新)后重新包装。
|
|
43
|
+
- **限制**:依赖 DSH 内部服务,DSH 升级后可能失效(失效时自动回退英文,插件其它功能不受影响);仅影响显示,不影响包身份/安装/更新;若某些页面直接读 `package.json` 而不走这两个方法,则不受影响。
|
|
44
|
+
- **关闭方式**:启动 DSH 前设置环境变量 `DSH_PLUGIN_LABELS_OFFICIAL_META=0`,然后重启 DSH(会同时关闭上述两处覆盖)。未设置、设为 `1` 或其它值都保持默认开启。
|
|
45
|
+
- **生效方式**:host 代码在进程启动时安装,**需要重启 DSH 一次**;重启后 `GET /api/plugin-labels/list` 的响应里 `officialMeta` 字段(`{enabled, patched, service, bundles}`)可确认两处覆盖是否已生效,默认应为 `enabled:true, patched:true, bundles:true`。
|
|
46
|
+
|
|
47
|
+
## 自动翻译新插件
|
|
48
|
+
|
|
49
|
+
host 启动约 **4 秒**后会自动补一次缺失标签:
|
|
50
|
+
|
|
51
|
+
- 默认开启;每次启动最多翻译 **20** 条(`MAX_PENDING_PER_CALL`),只补空缺、**不会覆盖手改标签**,剩余的下次启动/手动调用再补。
|
|
52
|
+
- 关闭方式:启动 DSH 前设置 `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0`,然后重启 DSH;未设置/`1`/其它值均为开启。
|
|
53
|
+
- 与手动翻译共用同一个单飞锁与 120 秒超时;失败只写日志(warn),不抛错、不阻塞启动。
|
|
54
|
+
- 手动补翻译:`POST /api/plugin-labels/translate`(见「使用」)。
|
|
55
|
+
|
|
56
|
+
## 安装
|
|
57
|
+
|
|
58
|
+
### 方式一:npm 包(推荐,`npm publish` 后)
|
|
59
|
+
|
|
60
|
+
在 DSH 插件页 → 添加插件 → 填包名 `dsh-plugin-labels`;或在创造模式会话里 `plugin_manager` → `install_bundle` → `target: "dsh-plugin-labels"`。发布后 profile 依赖形如
|
|
61
|
+
`"dsh-plugin-labels": "dsh-plugin-labels@^0.8.0"`。
|
|
62
|
+
|
|
63
|
+
### 方式二:从 Git 仓库安装
|
|
64
|
+
|
|
65
|
+
在 DSH 插件页 → 添加插件 → 填本仓库的 Git 地址(`https://github.com/qyy2005/dsh-plugin-labels`),DSH 会拉取并安装;此时 profile 依赖形如 `github:qyy2005/dsh-plugin-labels`。私有仓库需具备相应访问权限。
|
|
66
|
+
|
|
67
|
+
### 方式三:本机目录安装(开发用,`link:` 形式)
|
|
68
|
+
|
|
69
|
+
用 `plugin_manager` → `install_bundle` → `target` 填本目录绝对路径,profile 依赖形如 `link:<本机插件目录绝对路径>`,`node_modules/dsh-plugin-labels` 是指向本目录的符号链接。
|
|
70
|
+
|
|
71
|
+
- 适合开发:源码即已装内容,改完刷新/重启即可;**源码目录不可删除或移动**。
|
|
72
|
+
- 改 host `lib/index.js` 后需重启 DSH 才生效(v0.7.0 起 client 已无 UI)。
|
|
73
|
+
- 不建议手工编辑 profile 的 `package.json` 来安装(易被 pnpm 清理)。
|
|
74
|
+
|
|
75
|
+
## 使用
|
|
76
|
+
|
|
77
|
+
v0.7.0 起没有 UI 入口,全部通过 4 个 loopback HTTP API 操作(也可以让 AI/脚本代改):
|
|
78
|
+
|
|
79
|
+
```powershell
|
|
80
|
+
# 查看已装插件 + 标签 + 诊断字段(officialMeta)
|
|
81
|
+
Invoke-WebRequest -UseBasicParsing http://127.0.0.1:<端口>/api/plugin-labels/list
|
|
82
|
+
|
|
83
|
+
# 补翻译(无参 = 只补空缺;最多 20 条/次)
|
|
84
|
+
Invoke-RestMethod -Method Post -ContentType 'application/json' `
|
|
85
|
+
-Body '{}' http://127.0.0.1:<端口>/api/plugin-labels/translate
|
|
86
|
+
|
|
87
|
+
# 显式重翻指定插件
|
|
88
|
+
Invoke-RestMethod -Method Post -ContentType 'application/json' `
|
|
89
|
+
-Body '{"names":["@linxin666/dsh-remote-web-ui"]}' http://127.0.0.1:<端口>/api/plugin-labels/translate
|
|
90
|
+
|
|
91
|
+
# 手改一条(source=manual),或恢复 AI:{"name":"...","action":"reset"}
|
|
92
|
+
Invoke-RestMethod -Method Post -ContentType 'application/json' `
|
|
93
|
+
-Body '{"name":"@linxin666/dsh-remote-web-ui","title":"远程网页访问","oneLine":"扫码配对手机和电脑共用网页界面。"}' `
|
|
94
|
+
http://127.0.0.1:<端口>/api/plugin-labels/label
|
|
95
|
+
|
|
96
|
+
# 导入导出的 JSON(只保留已装插件名,保留 ai/manual 来源)
|
|
97
|
+
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile labels.json `
|
|
98
|
+
http://127.0.0.1:<端口>/api/plugin-labels/import
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
端口就是当前 DSH Web GUI 的端口;这些路由只接受 loopback 请求且必须是 POST(`/list` 为 GET)。自动翻译会替你补空缺,手改过的条目不会被自动覆盖。
|
|
102
|
+
|
|
103
|
+
## 插件速查文档
|
|
104
|
+
|
|
105
|
+
`node scripts/gen-doc.mjs` 会在**本地**生成 `PLUGINS.zh.md` 与 `PLUGINS.zh.html`:中文名、原始包名、一句话作用、版本、说明(AI 生成 / 手改 / 待翻译)。HTML 是单文件、内联样式、可搜索、随系统深浅色,双击即可打开,无需联网。内容来自你本机的标签缓存与已装插件清单,**默认不提交到仓库**(已 gitignore)。
|
|
106
|
+
|
|
107
|
+
重新生成(装/卸插件,或翻译/手改标签之后):
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
node scripts/gen-doc.mjs # 默认读取 $env:DSH_HOME(或 ~/.dsh)的 desktop profile
|
|
111
|
+
node scripts/gen-doc.mjs --home "$env:DSH_HOME" --profile desktop
|
|
112
|
+
npm run docs # 等价于 node scripts/gen-doc.mjs
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
生成器只读本地标签缓存与 profile 清单,产物不含本机路径、`DSH_HOME`、用户名或 token;生成的两个文件只留在本地(已 gitignore),**不要手工编辑**。
|
|
116
|
+
|
|
117
|
+
## 兼容性
|
|
118
|
+
|
|
119
|
+
- DSH 桌面版 **0.2.1-rc.2**(Web GUI);依赖官方 client 基座模块与 host 服务,只 `require("react")`。
|
|
120
|
+
- 本插件不依赖任何 UI slot:显示覆盖包装宿主的 `pluginPackages.metaOf()`(官方页行/详情)与 `pluginManager.listBundles()`(插件包卡片),client 只注册 `locale` 字典。
|
|
121
|
+
- 拿不到这些内部服务时静默回退:不显示中文(回退英文),插件其它功能与 API 仍可用。
|
|
122
|
+
|
|
123
|
+
## 开发说明
|
|
124
|
+
|
|
125
|
+
- **无构建步骤**:host 是 ESM 的 `lib/index.js`(`inject: ["webServer"]`);client 是手写的 `__ModuleLoader__` bundle `lib/client.js`(v0.7.0 起只注册 locale,无 UI),仅 `require("react")`。
|
|
126
|
+
- **管理 API**(client 已无 UI,脚本/AI 直接调用):
|
|
127
|
+
- `GET /api/plugin-labels/list`
|
|
128
|
+
- `POST /api/plugin-labels/translate`(无参=补空缺;`{names:[...]}`=显式重翻)
|
|
129
|
+
- `POST /api/plugin-labels/label`(`{name,title,oneLine}` 或 `{name,action:"reset"}`)
|
|
130
|
+
- `POST /api/plugin-labels/import`
|
|
131
|
+
- **自动翻译**:`apply` 里注册一个 4 秒后执行一次的定时器(`unref`),调用 `translatePending` 补空缺;可用 `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` 关闭;异常只 warn。
|
|
132
|
+
- **显示覆盖**:`installOfficialMeta` / `installBundleMeta` 用 symbol 标记识别 wrapper,幂等、可 dispose、服务替换后重打、失败静默回退。`DSH_PLUGIN_LABELS_OFFICIAL_META=0` 同时关闭两处覆盖。
|
|
133
|
+
- **安全围栏**:loopback 校验、`sec-fetch-site` 跨站拒绝、POST-only、翻译单飞锁(并发 409)、`AbortSignal.timeout(120s)`、未预期异常统一 500 不泄漏路径。
|
|
134
|
+
- **缓存**:`<DSH_HOME>/plugin-labels.json`,UTF-8 无 BOM,临时文件 + `rename` 原子写;缓存 `version: 2`,旧 v1 数据可迁移读取,单条非法条目丢弃。
|
|
135
|
+
- **验证**:`node --check lib/index.js`、`node --check lib/client.js`;host 可用桩 `ctx` 跑冒烟。
|
|
136
|
+
- 修改 host 后需重启 DSH 生效(本插件已没有需要刷新页面的 UI 改动)。
|
|
137
|
+
|
|
138
|
+
## 目录结构
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
dsh-plugin-labels/
|
|
142
|
+
├─ .gitignore # 忽略 node_modules、日志与本地内部文档
|
|
143
|
+
├─ .github/
|
|
144
|
+
│ ├─ ISSUE_TEMPLATE/ # Bug / Feature 的 issue forms
|
|
145
|
+
│ └─ workflows/ci.yml # GitHub Actions:node --check + JSON/BOM 校验
|
|
146
|
+
├─ docs/
|
|
147
|
+
│ └─ official-page.png # 官方「插件」页原地显示中文名的截图
|
|
148
|
+
├─ package.json # manifest(dsh.bundle 组合 patch、dsh.client 平台与 inject)
|
|
149
|
+
├─ cordis.patch.yml # bundle 组合 patch
|
|
150
|
+
├─ lib/
|
|
151
|
+
│ ├─ index.js # host:HTTP API + 显示覆盖 + 自动翻译 + 标签缓存
|
|
152
|
+
│ └─ client.js # client:模块骨架,仅注册 locale(v0.7.0 起无 UI)
|
|
153
|
+
├─ locale/
|
|
154
|
+
│ ├─ zh.json # 插件元数据(中文)
|
|
155
|
+
│ └─ en.json # 插件元数据(英文)
|
|
156
|
+
├─ icon.svg # 插件图标
|
|
157
|
+
├─ scripts/
|
|
158
|
+
│ └─ gen-doc.mjs # 生成 PLUGINS.zh.* 速查文档(本地生成,已 gitignore)
|
|
159
|
+
├─ README.md
|
|
160
|
+
└─ LICENSE
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT,见 [LICENSE](LICENSE)。Copyright (c) 2026 qyy2005
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## English summary
|
|
170
|
+
|
|
171
|
+
`dsh-plugin-labels` is a DSH (DeepSeek Harness) plugin that gives every installed plugin a Chinese name and a one-line explanation. The labels are shown **in place** on the official Plugins page: the installed-plugin rows, the detail pages and the plugin package cards (`pluginManager.listBundles`) all display the Chinese name and description, falling back to English for plugins without a label. **The plugin has no UI entry point** (no tab, no settings item): labels are managed through four loopback HTTP routes (or by an AI/script calling them), and results are cached under `<DSH_HOME>/plugin-labels.json` with manual edits never overwritten by automatic translation.
|
|
172
|
+
|
|
173
|
+
Each label is a Chinese name (≤10 characters) plus a one-line explanation (≤80 characters) that must explain **how to use** the plugin: the problem it solves, where in DSH the user enters or operates it, and a concrete first step. When the entry point cannot be inferred, the model is instructed to write 入口以界面为准 instead of inventing menus.
|
|
174
|
+
|
|
175
|
+
Newly installed plugins are translated automatically: about 4 seconds after host startup one pass fills up to 20 missing labels (set `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` and restart DSH to disable it). The display overrides can be disabled together with `DSH_PLUGIN_LABELS_OFFICIAL_META=0`; `GET /api/plugin-labels/list` reports `officialMeta: {enabled, patched, service, bundles}` for diagnostics. Both overrides require one DSH restart after installation.
|
|
176
|
+
|
|
177
|
+
The client half is a hand-written `__ModuleLoader__` bundle with no build step that only requires `react` (v0.7.0: locale registration only, no UI); the host half is an ESM plugin exposing four loopback-only HTTP routes plus the display overrides and the startup auto-translate. Verified on DSH desktop 0.2.1-rc.2. MIT licensed.
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Plugin Labels">
|
|
2
|
+
<rect x="6" y="14" width="52" height="36" rx="8" fill="#4D6BFE" opacity="0.16"/>
|
|
3
|
+
<rect x="6" y="14" width="52" height="36" rx="8" fill="none" stroke="#4D6BFE" stroke-width="2"/>
|
|
4
|
+
<path d="M20 26h24M20 33h18M20 40h12" stroke="#4D6BFE" stroke-width="3" stroke-linecap="round"/>
|
|
5
|
+
</svg>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-labels — browser half (v0.7.0: no UI).
|
|
3
|
+
*
|
|
4
|
+
* This bundle no longer renders a tab anywhere. The Chinese names are shown in
|
|
5
|
+
* place by the host half, which wraps the official display metadata: the
|
|
6
|
+
* Plugins page rows/detail pages (`pluginPackages.metaOf`) and the plugin
|
|
7
|
+
* package cards (`pluginManager.listBundles`). Management (translate / edit /
|
|
8
|
+
* re-translate / import / export) happens through the host's loopback HTTP API,
|
|
9
|
+
* e.g. by an AI agent or a script.
|
|
10
|
+
*
|
|
11
|
+
* What remains is the module skeleton: the factory returns `{ inject, apply }`,
|
|
12
|
+
* registers the `plugin-labels` locale dictionaries and has no side effects.
|
|
13
|
+
* Only `react` is required. The row component below is kept as reference for
|
|
14
|
+
* the removed tab: it is not registered anywhere and never renders.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
window.__ModuleLoader__.load({
|
|
18
|
+
id: "dsh-plugin-labels",
|
|
19
|
+
factory(require) {
|
|
20
|
+
const React = require("react");
|
|
21
|
+
|
|
22
|
+
const API_LIST = "/api/plugin-labels/list";
|
|
23
|
+
const API_TRANSLATE = "/api/plugin-labels/translate";
|
|
24
|
+
const API_LABEL = "/api/plugin-labels/label";
|
|
25
|
+
const API_IMPORT = "/api/plugin-labels/import";
|
|
26
|
+
|
|
27
|
+
const dictZh = {
|
|
28
|
+
title: "插件中文名",
|
|
29
|
+
tabLabel: "中文名",
|
|
30
|
+
subtitle: "给已装插件配中文名与一句话说明,侧栏插件页与设置里都能查看。只有点翻译才会花 token,翻过就缓存,可手动编辑。",
|
|
31
|
+
loading: "读取中…",
|
|
32
|
+
ready: "已装",
|
|
33
|
+
translated: "已翻译",
|
|
34
|
+
pending: "未翻译",
|
|
35
|
+
translate: "翻译未翻译的",
|
|
36
|
+
translating: "翻译中…",
|
|
37
|
+
nonePending: "全部已翻译",
|
|
38
|
+
untranslated: "未翻译",
|
|
39
|
+
official: "官方内置",
|
|
40
|
+
manual: "手改",
|
|
41
|
+
ai: "AI",
|
|
42
|
+
edit: "编辑",
|
|
43
|
+
save: "保存",
|
|
44
|
+
cancel: "取消",
|
|
45
|
+
resetAi: "恢复AI",
|
|
46
|
+
retranslate: "重翻",
|
|
47
|
+
export: "导出",
|
|
48
|
+
import: "导入",
|
|
49
|
+
search: "搜索中文名、包名或说明…",
|
|
50
|
+
english: "英文原文",
|
|
51
|
+
noDescription: "(该插件未提供描述)",
|
|
52
|
+
saved: "已保存",
|
|
53
|
+
failed: "失败",
|
|
54
|
+
done: "完成,新增",
|
|
55
|
+
items: "条",
|
|
56
|
+
imported: "导入完成:",
|
|
57
|
+
skipped: "条跳过",
|
|
58
|
+
titlePlaceholder: "中文名(≤8字)",
|
|
59
|
+
oneLinePlaceholder: "一句话说明",
|
|
60
|
+
empty: "没有匹配的插件",
|
|
61
|
+
};
|
|
62
|
+
const dictEn = {
|
|
63
|
+
title: "Plugin Labels",
|
|
64
|
+
tabLabel: "Labels",
|
|
65
|
+
subtitle: "Chinese names and one-line explanations for installed plugins, shown on the sidebar Plugins page and in Settings. Tokens are spent only when you translate; results are cached and editable.",
|
|
66
|
+
loading: "Loading…",
|
|
67
|
+
ready: "Installed",
|
|
68
|
+
translated: "Translated",
|
|
69
|
+
pending: "Pending",
|
|
70
|
+
translate: "Translate missing",
|
|
71
|
+
translating: "Translating…",
|
|
72
|
+
nonePending: "All translated",
|
|
73
|
+
untranslated: "Untranslated",
|
|
74
|
+
official: "Built-in",
|
|
75
|
+
manual: "Edited",
|
|
76
|
+
ai: "AI",
|
|
77
|
+
edit: "Edit",
|
|
78
|
+
save: "Save",
|
|
79
|
+
cancel: "Cancel",
|
|
80
|
+
resetAi: "Reset to AI",
|
|
81
|
+
retranslate: "Re-translate",
|
|
82
|
+
export: "Export",
|
|
83
|
+
import: "Import",
|
|
84
|
+
search: "Search name, package or description…",
|
|
85
|
+
english: "English description",
|
|
86
|
+
noDescription: "(no description)",
|
|
87
|
+
saved: "Saved",
|
|
88
|
+
failed: "Failed",
|
|
89
|
+
done: "Done; added",
|
|
90
|
+
items: "items",
|
|
91
|
+
imported: "Imported:",
|
|
92
|
+
skipped: "skipped",
|
|
93
|
+
titlePlaceholder: "Chinese name (≤8 chars)",
|
|
94
|
+
oneLinePlaceholder: "One-line explanation",
|
|
95
|
+
empty: "No matching plugin",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Component-local styles: inherited colors only, removed on unmount. */
|
|
99
|
+
const CSS = [
|
|
100
|
+
".dsh-pl{display:flex;flex-direction:column;gap:12px;padding:6px 4px 24px;font-size:13px;line-height:1.5;max-width:100%;box-sizing:border-box}",
|
|
101
|
+
".dsh-pl-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
102
|
+
".dsh-pl-search{flex:1 1 200px;min-width:160px;padding:6px 10px;font:inherit;font-size:13px;color:inherit;background:color-mix(in srgb,currentColor 4%,transparent);border:1px solid color-mix(in srgb,currentColor 16%,transparent);border-radius:8px;outline:none}",
|
|
103
|
+
".dsh-pl-search:focus{border-color:color-mix(in srgb,currentColor 40%,transparent)}",
|
|
104
|
+
".dsh-pl-btn{padding:5px 10px;font:inherit;font-size:12.5px;color:inherit;background:transparent;border:1px solid color-mix(in srgb,currentColor 18%,transparent);border-radius:8px;cursor:pointer;white-space:nowrap}",
|
|
105
|
+
".dsh-pl-btn:hover:not(:disabled){background:color-mix(in srgb,currentColor 8%,transparent)}",
|
|
106
|
+
".dsh-pl-btn:disabled{opacity:.45;cursor:not-allowed}",
|
|
107
|
+
".dsh-pl-primary{font-weight:600;border-color:color-mix(in srgb,currentColor 38%,transparent);background:color-mix(in srgb,currentColor 10%,transparent)}",
|
|
108
|
+
".dsh-pl-stats{font-size:12px;opacity:.6}",
|
|
109
|
+
".dsh-pl-notice{font-size:12px;opacity:.75}",
|
|
110
|
+
".dsh-pl-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}",
|
|
111
|
+
".dsh-pl-row{display:flex;gap:8px 12px;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;padding:11px 8px;border-bottom:1px solid color-mix(in srgb,currentColor 8%,transparent);border-radius:8px}",
|
|
112
|
+
".dsh-pl-row:hover{background:color-mix(in srgb,currentColor 4%,transparent)}",
|
|
113
|
+
".dsh-pl-main{min-width:0;flex:1 1 260px}",
|
|
114
|
+
".dsh-pl-name{font-weight:600;font-size:14px;line-height:1.4;overflow-wrap:anywhere}",
|
|
115
|
+
".dsh-pl-name.dsh-pl-empty{opacity:.45;font-weight:500}",
|
|
116
|
+
".dsh-pl-meta{margin-top:3px;font-family:ui-monospace,Consolas,monospace;font-size:11.5px;line-height:1.4;opacity:.6;display:flex;align-items:center;gap:6px;flex-wrap:wrap;overflow-wrap:anywhere}",
|
|
117
|
+
".dsh-pl-tag{font-family:inherit;font-size:11px;padding:0 6px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);border-radius:99px;opacity:.85}",
|
|
118
|
+
".dsh-pl-one{margin-top:6px;font-size:12.5px;line-height:1.55;opacity:.78;overflow-wrap:anywhere}",
|
|
119
|
+
".dsh-pl-en{margin-top:6px;font-size:11.5px;opacity:.5}",
|
|
120
|
+
".dsh-pl-en summary{cursor:pointer;outline:none}",
|
|
121
|
+
".dsh-pl-en div{margin-top:2px;white-space:pre-wrap;overflow-wrap:anywhere}",
|
|
122
|
+
".dsh-pl-actions{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;flex:0 0 auto;margin-left:auto}",
|
|
123
|
+
".dsh-pl-act{padding:3px 8px;font:inherit;font-size:12px;color:inherit;background:transparent;border:none;border-radius:6px;cursor:pointer;opacity:.7}",
|
|
124
|
+
".dsh-pl-act:hover{background:color-mix(in srgb,currentColor 8%,transparent);opacity:1}",
|
|
125
|
+
".dsh-pl-edit{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;flex:1 1 100%}",
|
|
126
|
+
".dsh-pl-input{flex:1 1 150px;padding:4px 8px;font:inherit;font-size:12.5px;color:inherit;background:color-mix(in srgb,currentColor 4%,transparent);border:1px solid color-mix(in srgb,currentColor 28%,transparent);border-radius:7px;outline:none}",
|
|
127
|
+
".dsh-pl-hint{font-size:12px;opacity:.55;margin-top:4px}",
|
|
128
|
+
].join("");
|
|
129
|
+
|
|
130
|
+
function LabelsSection(props) {
|
|
131
|
+
const t = props && typeof props.t === "function" ? props.t : (key) => key;
|
|
132
|
+
const [state, setState] = React.useState({
|
|
133
|
+
phase: "loading",
|
|
134
|
+
plugins: [],
|
|
135
|
+
pending: 0,
|
|
136
|
+
translated: 0,
|
|
137
|
+
error: "",
|
|
138
|
+
busy: false,
|
|
139
|
+
notice: "",
|
|
140
|
+
query: "",
|
|
141
|
+
editing: null,
|
|
142
|
+
saving: false,
|
|
143
|
+
importing: false,
|
|
144
|
+
});
|
|
145
|
+
const fileRef = React.useRef(null);
|
|
146
|
+
|
|
147
|
+
const load = React.useCallback(() => {
|
|
148
|
+
return fetch(API_LIST, { headers: { accept: "application/json" }, credentials: "same-origin" })
|
|
149
|
+
.then((response) => response.json())
|
|
150
|
+
.then((data) => {
|
|
151
|
+
if (data && data.ok) {
|
|
152
|
+
setState((previous) => Object.assign({}, previous, {
|
|
153
|
+
phase: "ready",
|
|
154
|
+
plugins: Array.isArray(data.plugins) ? data.plugins : [],
|
|
155
|
+
pending: Number(data.pending) || 0,
|
|
156
|
+
translated: Number(data.translated) || 0,
|
|
157
|
+
error: "",
|
|
158
|
+
}));
|
|
159
|
+
} else {
|
|
160
|
+
setState((previous) => Object.assign({}, previous, { phase: "error", error: String((data && data.error) || "unknown") }));
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
.catch((error) => {
|
|
164
|
+
setState((previous) => Object.assign({}, previous, { phase: "error", error: String((error && error.message) || error) }));
|
|
165
|
+
});
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
React.useEffect(() => {
|
|
169
|
+
load().catch(() => {});
|
|
170
|
+
}, [load]);
|
|
171
|
+
|
|
172
|
+
const post = (url, body) => fetch(url, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
175
|
+
credentials: "same-origin",
|
|
176
|
+
body: JSON.stringify(body || {}),
|
|
177
|
+
}).then((response) => response.json());
|
|
178
|
+
|
|
179
|
+
const ok = (data) => {
|
|
180
|
+
if (data && data.ok) return data;
|
|
181
|
+
throw new Error(String((data && data.error) || "unknown"));
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const run = (promise, onDone) => promise
|
|
185
|
+
.then(ok)
|
|
186
|
+
.then((data) => { onDone(data); return load(); })
|
|
187
|
+
.catch((error) => setState((previous) => Object.assign({}, previous, {
|
|
188
|
+
busy: false,
|
|
189
|
+
saving: false,
|
|
190
|
+
importing: false,
|
|
191
|
+
notice: t("failed") + ":" + String((error && error.message) || error),
|
|
192
|
+
})));
|
|
193
|
+
|
|
194
|
+
const translate = () => {
|
|
195
|
+
setState((previous) => Object.assign({}, previous, { busy: true, notice: "" }));
|
|
196
|
+
run(post(API_TRANSLATE, {}), (data) => setState((previous) => Object.assign({}, previous, {
|
|
197
|
+
busy: false,
|
|
198
|
+
notice: t("done") + " " + (Number(data.added) || 0) + " " + t("items"),
|
|
199
|
+
})));
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const retranslate = (pluginName) => {
|
|
203
|
+
setState((previous) => Object.assign({}, previous, { busy: true, notice: "" }));
|
|
204
|
+
run(post(API_TRANSLATE, { names: [pluginName] }), (data) => setState((previous) => Object.assign({}, previous, {
|
|
205
|
+
busy: false,
|
|
206
|
+
notice: t("done") + " " + (Number(data.added) || 0) + " " + t("items"),
|
|
207
|
+
})));
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const beginEdit = (plugin) => setState((previous) => Object.assign({}, previous, {
|
|
211
|
+
editing: { name: plugin.name, title: plugin.label ? plugin.label.title : "", oneLine: plugin.label ? plugin.label.oneLine : "" },
|
|
212
|
+
notice: "",
|
|
213
|
+
}));
|
|
214
|
+
const cancelEdit = () => setState((previous) => Object.assign({}, previous, { editing: null }));
|
|
215
|
+
const changeField = (key, value) => setState((previous) => (previous.editing ? Object.assign({}, previous, { editing: Object.assign({}, previous.editing, { [key]: value }) }) : previous));
|
|
216
|
+
|
|
217
|
+
const saveEdit = () => {
|
|
218
|
+
const editing = state.editing;
|
|
219
|
+
if (!editing) return;
|
|
220
|
+
setState((previous) => Object.assign({}, previous, { saving: true }));
|
|
221
|
+
run(post(API_LABEL, { name: editing.name, title: editing.title, oneLine: editing.oneLine }), () => setState((previous) => Object.assign({}, previous, {
|
|
222
|
+
saving: false,
|
|
223
|
+
editing: null,
|
|
224
|
+
notice: t("saved"),
|
|
225
|
+
})));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const resetLabel = (pluginName) => {
|
|
229
|
+
setState((previous) => Object.assign({}, previous, { notice: "" }));
|
|
230
|
+
run(post(API_LABEL, { name: pluginName, action: "reset" }), () => {});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const exportLabels = () => {
|
|
234
|
+
const entries = {};
|
|
235
|
+
state.plugins.forEach((plugin) => {
|
|
236
|
+
if (!plugin.label) return;
|
|
237
|
+
// Carry the origin so re-importing never promotes AI output to
|
|
238
|
+
// "manual" (or the other way around).
|
|
239
|
+
entries[plugin.name] = {
|
|
240
|
+
title: plugin.label.title,
|
|
241
|
+
oneLine: plugin.label.oneLine,
|
|
242
|
+
source: plugin.label.source === "manual" ? "manual" : "ai",
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
const text = JSON.stringify({ version: 2, exportedAt: new Date().toISOString(), entries }, null, 2);
|
|
246
|
+
const blob = new Blob([text], { type: "application/json" });
|
|
247
|
+
const url = URL.createObjectURL(blob);
|
|
248
|
+
const anchor = document.createElement("a");
|
|
249
|
+
anchor.href = url;
|
|
250
|
+
anchor.download = "plugin-labels.json";
|
|
251
|
+
anchor.click();
|
|
252
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const importFile = (event) => {
|
|
256
|
+
const file = event.target.files && event.target.files[0];
|
|
257
|
+
event.target.value = "";
|
|
258
|
+
if (!file) return;
|
|
259
|
+
const reader = new FileReader();
|
|
260
|
+
reader.onload = () => {
|
|
261
|
+
let parsed;
|
|
262
|
+
try {
|
|
263
|
+
parsed = JSON.parse(String(reader.result));
|
|
264
|
+
} catch (error) {
|
|
265
|
+
setState((previous) => Object.assign({}, previous, { notice: t("failed") + ":" + String((error && error.message) || error) }));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
setState((previous) => Object.assign({}, previous, { importing: true }));
|
|
269
|
+
run(post(API_IMPORT, parsed), (data) => setState((previous) => Object.assign({}, previous, {
|
|
270
|
+
importing: false,
|
|
271
|
+
notice: t("imported") + " " + (Number(data.applied) || 0) + " · " + (Number(data.skipped) || 0) + " " + t("skipped"),
|
|
272
|
+
})));
|
|
273
|
+
};
|
|
274
|
+
reader.readAsText(file);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const query = state.query.trim().toLowerCase();
|
|
278
|
+
const rows = state.plugins.filter((plugin) => {
|
|
279
|
+
if (query === "") return true;
|
|
280
|
+
const haystack = [plugin.name, plugin.description || "", plugin.label ? plugin.label.title : "", plugin.label ? plugin.label.oneLine : ""].join(" ").toLowerCase();
|
|
281
|
+
return haystack.indexOf(query) >= 0;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const stats = state.phase === "loading"
|
|
285
|
+
? t("loading")
|
|
286
|
+
: state.phase === "error"
|
|
287
|
+
? t("failed") + ":" + state.error
|
|
288
|
+
: t("ready") + " " + state.plugins.length + " · " + t("translated") + " " + (state.translated || 0) + " · " + t("pending") + " " + (state.pending || 0);
|
|
289
|
+
|
|
290
|
+
const canTranslate = state.phase === "ready" && !state.busy && (state.pending || 0) > 0;
|
|
291
|
+
|
|
292
|
+
const renderRow = (plugin) => {
|
|
293
|
+
const isEditing = state.editing !== null && state.editing.name === plugin.name;
|
|
294
|
+
// Third line: the Chinese one-liner, or the English description when
|
|
295
|
+
// the plugin has not been translated yet.
|
|
296
|
+
const oneLine = plugin.label && plugin.label.oneLine ? plugin.label.oneLine : (plugin.description || t("noDescription"));
|
|
297
|
+
return React.createElement(
|
|
298
|
+
"li",
|
|
299
|
+
{ key: plugin.name, className: "dsh-pl-row" },
|
|
300
|
+
React.createElement(
|
|
301
|
+
"div",
|
|
302
|
+
{ className: "dsh-pl-main" },
|
|
303
|
+
// 1) Chinese name replaces the English name as the headline.
|
|
304
|
+
plugin.label
|
|
305
|
+
? React.createElement("div", { className: "dsh-pl-name", title: plugin.label.title }, plugin.label.title)
|
|
306
|
+
: React.createElement("div", { className: "dsh-pl-name dsh-pl-empty" }, t("untranslated")),
|
|
307
|
+
// 2) Original package name + version + badges.
|
|
308
|
+
React.createElement(
|
|
309
|
+
"div",
|
|
310
|
+
{ className: "dsh-pl-meta" },
|
|
311
|
+
React.createElement("span", { className: "dsh-pl-original" }, plugin.name + (plugin.version ? " v" + plugin.version : "")),
|
|
312
|
+
plugin.kind === "official-or-missing" ? React.createElement("span", { className: "dsh-pl-tag" }, t("official")) : null,
|
|
313
|
+
plugin.label && plugin.label.source === "manual" ? React.createElement("span", { className: "dsh-pl-tag" }, t("manual")) : null,
|
|
314
|
+
plugin.label && plugin.label.source === "ai" ? React.createElement("span", { className: "dsh-pl-tag" }, t("ai")) : null,
|
|
315
|
+
),
|
|
316
|
+
// 3) One-line Chinese explanation (edit inputs live here while
|
|
317
|
+
// editing so the actions keep their place on the right).
|
|
318
|
+
isEditing
|
|
319
|
+
? React.createElement(
|
|
320
|
+
"div",
|
|
321
|
+
{ className: "dsh-pl-edit" },
|
|
322
|
+
React.createElement("input", {
|
|
323
|
+
className: "dsh-pl-input",
|
|
324
|
+
value: state.editing.title,
|
|
325
|
+
placeholder: t("titlePlaceholder"),
|
|
326
|
+
onChange: (event) => changeField("title", event.target.value),
|
|
327
|
+
}),
|
|
328
|
+
React.createElement("input", {
|
|
329
|
+
className: "dsh-pl-input",
|
|
330
|
+
value: state.editing.oneLine,
|
|
331
|
+
placeholder: t("oneLinePlaceholder"),
|
|
332
|
+
onChange: (event) => changeField("oneLine", event.target.value),
|
|
333
|
+
}),
|
|
334
|
+
)
|
|
335
|
+
: React.createElement("div", { className: "dsh-pl-one" }, oneLine),
|
|
336
|
+
// 4) The full English description stays collapsed below the fold.
|
|
337
|
+
React.createElement(
|
|
338
|
+
"details",
|
|
339
|
+
{ className: "dsh-pl-en" },
|
|
340
|
+
React.createElement("summary", null, t("english")),
|
|
341
|
+
React.createElement("div", null, plugin.description || t("noDescription")),
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
React.createElement(
|
|
345
|
+
"div",
|
|
346
|
+
{ className: "dsh-pl-actions" },
|
|
347
|
+
isEditing
|
|
348
|
+
? [
|
|
349
|
+
React.createElement("button", { key: "save", type: "button", className: "dsh-pl-act", disabled: state.saving || state.busy, onClick: saveEdit }, t("save")),
|
|
350
|
+
React.createElement("button", { key: "cancel", type: "button", className: "dsh-pl-act", onClick: cancelEdit }, t("cancel")),
|
|
351
|
+
]
|
|
352
|
+
: [
|
|
353
|
+
React.createElement("button", { key: "edit", type: "button", className: "dsh-pl-act", disabled: state.busy, onClick: () => beginEdit(plugin) }, t("edit")),
|
|
354
|
+
React.createElement("button", { key: "retranslate", type: "button", className: "dsh-pl-act", disabled: state.busy, onClick: () => retranslate(plugin.name) }, t("retranslate")),
|
|
355
|
+
plugin.label && plugin.label.source === "manual"
|
|
356
|
+
? React.createElement("button", { key: "reset", type: "button", className: "dsh-pl-act", disabled: state.busy, onClick: () => resetLabel(plugin.name) }, t("resetAi"))
|
|
357
|
+
: null,
|
|
358
|
+
],
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return React.createElement(
|
|
364
|
+
"div",
|
|
365
|
+
{ className: "dsh-pl" },
|
|
366
|
+
React.createElement("style", null, CSS),
|
|
367
|
+
React.createElement("div", { className: "dsh-pl-hint" }, t("subtitle")),
|
|
368
|
+
React.createElement(
|
|
369
|
+
"div",
|
|
370
|
+
{ className: "dsh-pl-bar" },
|
|
371
|
+
React.createElement("input", {
|
|
372
|
+
className: "dsh-pl-search",
|
|
373
|
+
value: state.query,
|
|
374
|
+
placeholder: t("search"),
|
|
375
|
+
onChange: (event) => setState((previous) => Object.assign({}, previous, { query: event.target.value })),
|
|
376
|
+
}),
|
|
377
|
+
React.createElement(
|
|
378
|
+
"button",
|
|
379
|
+
{
|
|
380
|
+
type: "button",
|
|
381
|
+
className: "dsh-pl-btn" + (canTranslate ? " dsh-pl-primary" : ""),
|
|
382
|
+
disabled: !canTranslate,
|
|
383
|
+
onClick: canTranslate ? translate : undefined,
|
|
384
|
+
},
|
|
385
|
+
state.busy ? t("translating") : (state.pending || 0) > 0 ? t("translate") + "(" + state.pending + ")" : t("nonePending"),
|
|
386
|
+
),
|
|
387
|
+
React.createElement("button", { type: "button", className: "dsh-pl-btn", onClick: exportLabels }, t("export")),
|
|
388
|
+
React.createElement("button", { type: "button", className: "dsh-pl-btn", disabled: state.importing || state.busy, onClick: () => fileRef.current && fileRef.current.click() }, state.importing ? t("loading") : t("import")),
|
|
389
|
+
React.createElement("input", { type: "file", accept: ".json,application/json", ref: fileRef, style: { display: "none" }, onChange: importFile }),
|
|
390
|
+
),
|
|
391
|
+
React.createElement("div", { className: "dsh-pl-stats" }, stats + (rows.length !== state.plugins.length ? " · " + rows.length + " / " + state.plugins.length : "")),
|
|
392
|
+
state.notice ? React.createElement("div", { className: "dsh-pl-notice" }, state.notice) : null,
|
|
393
|
+
React.createElement("ul", { className: "dsh-pl-list" }, rows.length > 0 ? rows.map(renderRow) : React.createElement("li", { className: "dsh-pl-hint" }, t("empty"))),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function apply(ctx) {
|
|
398
|
+
ctx.effect(() => ctx.locale.register("plugin-labels", { zh: dictZh, en: dictEn }), "plugin-labels: dictionaries");
|
|
399
|
+
// No slots are registered: v0.7.0 removed the last labels tab.
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
inject: ["locale"],
|
|
404
|
+
apply,
|
|
405
|
+
};
|
|
406
|
+
},
|
|
407
|
+
});
|