cline-kit 0.1.1 → 0.2.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/CHANGELOG.md +51 -2
- package/README.md +39 -8
- package/README.zh-CN.md +31 -4
- package/dictionaries/ja.json +9 -2
- package/dictionaries/ko.json +9 -2
- package/dictionaries/vi.json +9 -2
- package/dictionaries/zh-CN.json +9 -2
- package/dictionaries/zh-TW.json +9 -2
- package/package.json +2 -2
- package/src/cli.js +75 -6
- package/src/config.js +3 -1
- package/src/dict.js +54 -1
- package/src/doctor.js +18 -1
- package/src/engine.js +14 -0
- package/src/features/index.js +22 -1
- package/src/features/language-picker.js +182 -0
- package/src/injector.js +31 -1
- package/src/payload.js +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,55 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); dictionary data changes are versioned inside
|
|
5
5
|
each `dictionaries/<locale>.json` rather than here.
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-09-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`language-picker` feature - the interface language is now chosen inside Cline.** Settings gains an
|
|
12
|
+
"Interface language" row under Dark mode, built from the same markup as the rows around it, listing
|
|
13
|
+
each language in its own script (English / 日本語 / 한국어 / Tiếng Việt / 简体中文 / 繁體中文).
|
|
14
|
+
Clicking writes one pending intent into `localStorage`; the resident injector consumes it on its next
|
|
15
|
+
cycle, stores it as the configured dictionary, and re-injects, so the open window follows in ~4 s with
|
|
16
|
+
no reload and the choice survives a restart. `ckit feature disable language-picker` removes the row.
|
|
17
|
+
- `none` / `English` as a first-class language: run the overlay for the sidebar only and leave every
|
|
18
|
+
string Cline shipped. Available in the app row, `ckit locales none` and `--dictionary=none`; with it
|
|
19
|
+
selected `ckit update` reports "translation is off" instead of fetching.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The terminal is now the secondary route, not the only one: `ckit locales`, `--help` and both READMEs
|
|
24
|
+
point at Settings first, and `ckit install` / the first `ckit start` say "pick it inside Cline".
|
|
25
|
+
- Language names are listed in the language they name. `label` inside a dictionary is that language's
|
|
26
|
+
own wording (English for the machine-assisted packs), so a list built from it read as
|
|
27
|
+
"简体中文 / Japanese / Korean" - a translation of whatever is on screen instead of a choice.
|
|
28
|
+
- Per-feature build hashes now cover the feature's **config** as well as its source. Keyed on source
|
|
29
|
+
alone, a locale switch left `language-picker` running with the previous `current` until a full reload.
|
|
30
|
+
- `ckit doctor` summarises each feature with its own numbers instead of assuming the sidebar's, and a
|
|
31
|
+
feature that is only mounted some of the time (a Settings row) no longer reads as a failure.
|
|
32
|
+
- The overlay leaves its own UI alone: nodes marked `data-ckit-ui` are skipped by the translator
|
|
33
|
+
(memoised per node), which is what keeps the language row from being re-translated by itself.
|
|
34
|
+
- `npm test` is 28 checks: feature text is compared against **every** dictionary and every feature,
|
|
35
|
+
the picker's choice list and native names are pinned, `none` is verified as a real setting, and a
|
|
36
|
+
config-only change is required to bump the payload version.
|
|
37
|
+
|
|
38
|
+
## [0.1.2] - 2026-09-21
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `ckit locales` — lists the interface languages that ship with the kit (code, native name, string
|
|
43
|
+
count, dictionary version, `*` on the active one) and `ckit locales <code>` switches to one. Language
|
|
44
|
+
choice used to be reachable only through `ckit config --dictionary=<code>`, which no one finds without
|
|
45
|
+
reading the middle of the README; the old flag still works and writes the same field.
|
|
46
|
+
- `ckit install` prints the current language and the switch command every time, and the first successful
|
|
47
|
+
`ckit start` says it once (`hintLanguageShown` in `%APPDATA%\cline-kit\config.json`).
|
|
48
|
+
- `scripts/cli-locale-switch-check.js` — drives the CLI as a subprocess and probes the live DOM, so the
|
|
49
|
+
check covers the command a user types rather than the internal config write.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- `npm test` is 25 checks: the new one asserts every bundled locale appears in `ckit locales`, that
|
|
54
|
+
`--help` documents each command, and that an unknown code fails instead of storing a broken language.
|
|
55
|
+
|
|
7
56
|
## [0.1.1] - 2026-09-20
|
|
8
57
|
|
|
9
58
|
### Changed
|
|
@@ -60,8 +109,8 @@ First public release. Windows only.
|
|
|
60
109
|
- `ckit audit` — walks the screens over CDP, filters hidden-but-mounted panels by hit-testing each node,
|
|
61
110
|
and reports only strings the dictionary does not cover.
|
|
62
111
|
- `ckit.cmd` so a release-zip download has an entry point without a global install.
|
|
63
|
-
- CI: `npm test` plus a CLI smoke pass on tag, a portable Windows zip artifact, and npm publish
|
|
64
|
-
|
|
112
|
+
- CI: `npm test` plus a CLI smoke pass on tag, a portable Windows zip artifact, and `npm publish` through
|
|
113
|
+
npm's OIDC trusted publishing - the repository holds no npm credential.
|
|
65
114
|
|
|
66
115
|
### Changed
|
|
67
116
|
|
package/README.md
CHANGED
|
@@ -13,9 +13,10 @@ Every project you registered but have not opened yet is simply invisible. `cline
|
|
|
13
13
|
them listed, with the same styling as the native groups, and lets you switch into an empty project
|
|
14
14
|
straight from the sidebar.
|
|
15
15
|
|
|
16
|
-
**Secondary feature:** UI locale packs
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
**Secondary feature:** UI locale packs — 简体中文 (reference, proofread), 繁體中文, 日本語, 한국어,
|
|
17
|
+
Tiếng Việt, plus an `English` opt-out. Pick one inside Cline itself: Settings gains an **Interface
|
|
18
|
+
language** row. Locales exist because the same injection channel can carry them — they are not what
|
|
19
|
+
the project is for.
|
|
19
20
|
|
|
20
21
|
## The sidebar problem
|
|
21
22
|
|
|
@@ -81,13 +82,14 @@ ckit.cmd start
|
|
|
81
82
|
git clone https://github.com/chentaoxing/Cline-kit.git
|
|
82
83
|
cd cline-kit
|
|
83
84
|
npm install -g . # or call it directly: node src/cli.js <command>
|
|
84
|
-
npm test #
|
|
85
|
+
npm test # 28 checks, no dependencies
|
|
85
86
|
```
|
|
86
87
|
|
|
87
88
|
Then in every case:
|
|
88
89
|
|
|
89
90
|
```bash
|
|
90
91
|
ckit doctor # confirms the overlay is live inside the running window
|
|
92
|
+
ckit locales # optional: list / switch the interface language
|
|
91
93
|
```
|
|
92
94
|
|
|
93
95
|
`ckit install` locates the Cline shortcut in the Start Menu / Desktop, saves its original target in
|
|
@@ -106,11 +108,34 @@ then gives you the enhanced sidebar. If you would rather keep your own launcher,
|
|
|
106
108
|
| `ckit attach` | Inject once into a Cline you started yourself (`--port=N`), without owning the shortcut |
|
|
107
109
|
| `ckit features` | List feature plugins and whether each is on |
|
|
108
110
|
| `ckit feature enable\|disable <id>` | Toggle a feature (applies within ~4 s, no restart) |
|
|
111
|
+
| `ckit locales` | List the bundled interface languages |
|
|
112
|
+
| `ckit locales <code>` | Switch language — e.g. `ckit locales ja` (applies within ~4 s, no restart) |
|
|
109
113
|
| `ckit install` / `uninstall` | Repoint / restore your Cline shortcut |
|
|
110
114
|
| `ckit update` | Pull the latest locale dictionary from GitHub (`--force` to check now) |
|
|
111
115
|
| `ckit audit` | Walk the UI and list strings still in English |
|
|
112
116
|
| `ckit dict` | Dictionary statistics and the local override path |
|
|
113
|
-
| `ckit config` | Inspect or set `--cline-path`, `--port`, `--auto-update=on\|off
|
|
117
|
+
| `ckit config` | Inspect or set `--cline-path`, `--port`, `--auto-update=on\|off`, `--dictionary=<code>` |
|
|
118
|
+
|
|
119
|
+
### Choosing a language
|
|
120
|
+
|
|
121
|
+
**In the app:** open Cline's Settings (the gear at the bottom of the sidebar) and use the **Interface
|
|
122
|
+
language** row. It sits under Dark mode, looks like its neighbours, and takes effect in about four
|
|
123
|
+
seconds — no restart, and the choice is remembered. `English` there means "change nothing".
|
|
124
|
+
|
|
125
|
+
**From the terminal**, which is the same setting:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
ckit locales # what ships, how many strings each covers, which one is active
|
|
129
|
+
ckit locales ja # switch; the open window changes within ~4 s, no restart
|
|
130
|
+
ckit locales none # stop replacing Cline's own text
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`ckit install` prints the current language and this command, and the first `ckit start` says it once.
|
|
134
|
+
`ckit config --dictionary=<code>` is the equivalent low-level write. Once a non-default locale is
|
|
135
|
+
selected, `ckit update` hot-updates *that* dictionary.
|
|
136
|
+
|
|
137
|
+
The row is added by this kit — Cline has no language setting of its own. Turn it off with
|
|
138
|
+
`ckit feature disable language-picker`.
|
|
114
139
|
|
|
115
140
|
## Features
|
|
116
141
|
|
|
@@ -123,14 +148,16 @@ then gives you the enhanced sidebar. If you would rather keep your own launcher,
|
|
|
123
148
|
session; otherwise the kit keeps project grouping on. Two projects that share a folder name are
|
|
124
149
|
labelled with their parent folder - `LLM (workspace)` - and every row carries the full path as its
|
|
125
150
|
tooltip. Design notes: [`docs/features.zh-CN.md`](docs/features.zh-CN.md).
|
|
151
|
+
* **`language-picker`** (on by default) — the Interface language row inside Cline's own Settings page.
|
|
152
|
+
See [Choosing a language](#choosing-a-language).
|
|
126
153
|
* **locale packs** (`dictionaries/<locale>.json`) — whole-string text replacement only, so model
|
|
127
154
|
names, provider names, tool identifiers and code cannot be mangled. The corpus is 476 strings plus
|
|
128
155
|
30 pattern rules, built by combining a UI walk with extraction from the app's own source. Five
|
|
129
156
|
dictionaries ship: **zh-CN** (reference, proofread against the running app), **zh-TW**, **ja**,
|
|
130
157
|
**ko**, **vi** - complete but machine-assisted and *not* reviewed by native speakers, so a pull
|
|
131
|
-
request fixing a term is genuinely welcome.
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
request fixing a term is genuinely welcome. Choose one in Cline's Settings, or with
|
|
159
|
+
`ckit locales` — see [Choosing a language](#choosing-a-language). Authoring guide:
|
|
160
|
+
[`docs/dictionary-pipeline.zh-CN.md`](docs/dictionary-pipeline.zh-CN.md).
|
|
134
161
|
|
|
135
162
|
## Limitations
|
|
136
163
|
|
|
@@ -143,6 +170,10 @@ then gives you the enhanced sidebar. If you would rather keep your own launcher,
|
|
|
143
170
|
repointed) has no debug port to attach to, so you get plain Cline.
|
|
144
171
|
* **Cline updates can break things.** If the app renames a label, the locale leaves it in English; if
|
|
145
172
|
it restructures the sidebar, `sidebar-groups` needs updating. Run `ckit audit` and open an issue.
|
|
173
|
+
* **The language row is ours, not Cline's.** It is inserted into the Settings page next to Dark mode,
|
|
174
|
+
which is the right place to look but not a location Cline documents. If that page is restructured,
|
|
175
|
+
`language-picker` needs its anchor updated (`ckit doctor` reports whether the row is present);
|
|
176
|
+
`ckit feature disable language-picker` removes it.
|
|
146
177
|
* **Per-model description blurbs stay English** — free-form text from a remote provider catalogue.
|
|
147
178
|
* **Same-named projects.** Cline's own group headers expose only the folder name, never the path, so if
|
|
148
179
|
the registry holds two different `LLM` folders and one already has a native group, the kit cannot tell
|
package/README.zh-CN.md
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
在界面上根本不存在。cline-kit 把所有已登记项目常驻列出,样式与原生分组一致,并且可以直接从侧边栏
|
|
11
11
|
切进一个还没有会话的项目。
|
|
12
12
|
|
|
13
|
-
**附属功能:**
|
|
13
|
+
**附属功能:** 界面语言包,随包 5 份:**zh-CN**(基准,逐条对着界面校过)、繁體中文 / 日本語 / 한국어 /
|
|
14
|
+
Tiếng Việt,另加一项 `English` 表示不替换。选择入口在 Cline 里面:设置页会多出一行**界面语言**。
|
|
14
15
|
语言包之所以在这里,是因为同一条注入通道顺带能承载它——它不是这个项目存在的理由。
|
|
15
16
|
|
|
16
17
|
## 要解决的问题
|
|
@@ -73,13 +74,14 @@ ckit.cmd start
|
|
|
73
74
|
git clone https://github.com/chentaoxing/Cline-kit.git
|
|
74
75
|
cd cline-kit
|
|
75
76
|
npm install -g . # 或直接用:node src/cli.js <命令>
|
|
76
|
-
npm test #
|
|
77
|
+
npm test # 28 项无依赖自检
|
|
77
78
|
```
|
|
78
79
|
|
|
79
80
|
装完统一确认一次:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
83
|
ckit doctor # 直接问运行中的窗口:增强层到底进没进去
|
|
84
|
+
ckit locales # 可选:看有哪些语言、换语言
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
`ckit install` 会在开始菜单 / 桌面找到 Cline 快捷方式,把原始目标备份进 `%APPDATA%\cline-kit\config.json`,
|
|
@@ -97,12 +99,32 @@ ckit doctor # 直接问运行中的窗口:增强层到底进没进
|
|
|
97
99
|
| `ckit attach` | 往你自己启动、已开调试端口的 Cline 注入一次(`--port=N`),不接管快捷方式 |
|
|
98
100
|
| `ckit features` | 列出功能插件与开关状态 |
|
|
99
101
|
| `ckit feature enable\|disable <id>` | 开关某个插件(约 4 秒生效,无需重启) |
|
|
102
|
+
| `ckit locales` | 列出随包发行的界面语言、字符串数与当前语言 |
|
|
103
|
+
| `ckit locales <语言>` | 切换语言,如 `ckit locales ja`(约 4 秒生效,无需重启) |
|
|
100
104
|
| `ckit install` / `uninstall` | 改写 / 还原 Cline 快捷方式 |
|
|
101
105
|
| `ckit update` | 从 GitHub 拉取最新语言词典(`--force` 立即检查) |
|
|
102
106
|
| `ckit audit` | 走查界面,列出仍是英文的字符串 |
|
|
103
107
|
| `ckit dict` | 词典统计与本地覆盖文件路径 |
|
|
104
108
|
| `ckit config` | 查看或设置 `--cline-path`、`--port`、`--auto-update=on\|off`、`--dictionary=<语言>`、`--hide=<路径>` |
|
|
105
109
|
|
|
110
|
+
### 选择语言
|
|
111
|
+
|
|
112
|
+
**在软件里**:点开 Cline 侧边栏底部的齿轮进「设置」,用**界面语言**那一行。它排在「深色模式」下面,
|
|
113
|
+
样式和邻居一致,点完约 4 秒生效——不用重启,选择会记住。列表里的 `English` 表示不替换任何文案。
|
|
114
|
+
|
|
115
|
+
**在终端里**(改的是同一个设置):
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
ckit locales # 看有哪些语言、各覆盖多少条、当前用的是哪一个(* 号标记)
|
|
119
|
+
ckit locales ja # 切换;正在打开的窗口约 4 秒内跟着变,不用重启
|
|
120
|
+
ckit locales none # 不再替换 Cline 自己的文案
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`ckit install` 每次都会打印当前语言和这条命令,第一次 `ckit start` 成功后也会提示一次。
|
|
124
|
+
`ckit config --dictionary=<语言>` 是等价的底层写法。选定非默认语言后,`ckit update` 热更新的就是那一份。
|
|
125
|
+
|
|
126
|
+
这一行是本工具加进去的,Cline 原生没有语言设置;不想要就 `ckit feature disable language-picker`。
|
|
127
|
+
|
|
106
128
|
## 功能
|
|
107
129
|
|
|
108
130
|
`ckit features` 看实时列表。
|
|
@@ -112,12 +134,14 @@ ckit doctor # 直接问运行中的窗口:增强层到底进没进
|
|
|
112
134
|
如果你亲手点了 Cline 的排序按钮,本会话内就以你的选择为准不再干预;否则增强层会持续保持分组模式。
|
|
113
135
|
两个同名项目(比如两块盘上都有 `LLM`)会带上上级目录名显示成 `LLM (workspace)`,仍然重名就加序号,
|
|
114
136
|
而鼠标悬停始终是完整路径。设计说明见 [`docs/features.zh-CN.md`](docs/features.zh-CN.md)。
|
|
137
|
+
* **`language-picker`**(默认开启)——在 Cline 自己的设置页里加一行「界面语言」,见
|
|
138
|
+
[选择语言](#选择语言)。
|
|
115
139
|
* **语言包**(`dictionaries/<locale>.json`)——只做**整串精确匹配**替换,因此不会误伤模型名、服务商名、
|
|
116
140
|
工具标识和代码。语料为 476 条词条 + 30 条规则,来源是「界面走查 + 从应用自身源码提取」两路合并。
|
|
117
141
|
现在随包附带 5 份词典:**zh-CN**(基准,逐条对着运行中的界面校对过)、**zh-TW**、**ja**、**ko**、
|
|
118
142
|
**vi** —— 后四份条目齐全但属于机器辅助翻译、未经母语者审校,术语有偏差欢迎提 PR 直接改。
|
|
119
|
-
|
|
120
|
-
`ckit update`
|
|
143
|
+
切换:Cline 设置页的「界面语言」,或 `ckit locales ja`,正在打开的窗口**不需要重启或刷新**就会跟着变;
|
|
144
|
+
`ckit update` 也只更新你选的那一份。详见[选择语言](#选择语言)。制作流程见
|
|
121
145
|
[`docs/dictionary-pipeline.zh-CN.md`](docs/dictionary-pipeline.zh-CN.md)。
|
|
122
146
|
|
|
123
147
|
## 已知限制
|
|
@@ -129,6 +153,9 @@ ckit doctor # 直接问运行中的窗口:增强层到底进没进
|
|
|
129
153
|
得到的是原版界面。
|
|
130
154
|
* **Cline 升级可能失效。** 文案改了,语言包会留英文;侧边栏结构改了,`sidebar-groups` 需要跟进。
|
|
131
155
|
跑一次 `ckit audit` 并开 issue。
|
|
156
|
+
* **语言那一行是本项目加的,不是 Cline 原生的。** 它插在设置页「深色模式」下面——位置符合直觉,
|
|
157
|
+
但那个位置并不是 Cline 公开约定的。那一页改版时需要更新 `language-picker` 的锚点
|
|
158
|
+
(`ckit doctor` 会报这一行在不在),不想要就 `ckit feature disable language-picker`。
|
|
132
159
|
* **模型下方的一句英文简介不覆盖**——来自云端目录的自由文本,条数随服务商变化。
|
|
133
160
|
* **同名项目。** Cline 自己的分组标题只有文件夹名、没有路径,所以登记表里出现两个不同盘符下的 `LLM`
|
|
134
161
|
且其中一个已有原生分组时,无法判断哪一个才是它。此时两行都列出(带上级目录区分),并且不在这种行里
|
package/dictionaries/ja.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 2,
|
|
3
3
|
"language": "ja",
|
|
4
4
|
"label": "Japanese",
|
|
5
5
|
"clineVersion": "0.0.32",
|
|
6
|
-
"updated": "2026-09-
|
|
6
|
+
"updated": "2026-09-21",
|
|
7
7
|
"note": "Whole-string exact matches only. Rules run before prefixes; $1..$9 come from rule capture groups. Provider, model and product names are intentionally left in English.",
|
|
8
8
|
"entries": {
|
|
9
9
|
"Session": "セッション",
|
|
@@ -617,6 +617,13 @@
|
|
|
617
617
|
"errNoSearch": "ワークスペース検索ボックスが見つかりません",
|
|
618
618
|
"errNotListed": "このプロジェクトは一覧にありません",
|
|
619
619
|
"errSwitch": "切り替えに失敗しました"
|
|
620
|
+
},
|
|
621
|
+
"language-picker": {
|
|
622
|
+
"title": "表示言語",
|
|
623
|
+
"hint": "cline-kit が追加。約4秒で反映され、再起動は不要です。選択は保存されます。",
|
|
624
|
+
"switching": "切り替え中…",
|
|
625
|
+
"offTip": "Cline 本来の表示に戻す",
|
|
626
|
+
"stringsWord": "件"
|
|
620
627
|
}
|
|
621
628
|
}
|
|
622
629
|
}
|
package/dictionaries/ko.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 2,
|
|
3
3
|
"language": "ko",
|
|
4
4
|
"label": "Korean",
|
|
5
5
|
"clineVersion": "0.0.32",
|
|
6
|
-
"updated": "2026-09-
|
|
6
|
+
"updated": "2026-09-21",
|
|
7
7
|
"note": "Whole-string exact matches only. Rules run before prefixes; $1..$9 come from rule capture groups. Provider, model and product names are intentionally left in English.",
|
|
8
8
|
"entries": {
|
|
9
9
|
"Session": "세션",
|
|
@@ -617,6 +617,13 @@
|
|
|
617
617
|
"errNoSearch": "작업 공간 검색 상자를 찾을 수 없습니다",
|
|
618
618
|
"errNotListed": "목록에 해당 프로젝트가 없습니다",
|
|
619
619
|
"errSwitch": "전환 실패"
|
|
620
|
+
},
|
|
621
|
+
"language-picker": {
|
|
622
|
+
"title": "인터페이스 언어",
|
|
623
|
+
"hint": "cline-kit이 추가한 항목입니다. 약 4초 후 적용되며 재시작이 필요 없고 설정은 저장됩니다.",
|
|
624
|
+
"switching": "변경 중…",
|
|
625
|
+
"offTip": "Cline 원문으로 되돌리기",
|
|
626
|
+
"stringsWord": "개"
|
|
620
627
|
}
|
|
621
628
|
}
|
|
622
629
|
}
|
package/dictionaries/vi.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 2,
|
|
3
3
|
"language": "vi",
|
|
4
4
|
"label": "Vietnamese",
|
|
5
5
|
"clineVersion": "0.0.32",
|
|
6
|
-
"updated": "2026-09-
|
|
6
|
+
"updated": "2026-09-21",
|
|
7
7
|
"note": "Whole-string exact matches only. Rules run before prefixes; $1..$9 come from rule capture groups. Provider, model and product names are intentionally left in English.",
|
|
8
8
|
"entries": {
|
|
9
9
|
"Session": "Phiên",
|
|
@@ -617,6 +617,13 @@
|
|
|
617
617
|
"errNoSearch": "Không tìm thấy ô tìm kiếm không gian làm việc",
|
|
618
618
|
"errNotListed": "Dự án đó không có trong danh sách",
|
|
619
619
|
"errSwitch": "Chuyển đổi thất bại"
|
|
620
|
+
},
|
|
621
|
+
"language-picker": {
|
|
622
|
+
"title": "Ngôn ngữ giao diện",
|
|
623
|
+
"hint": "Do cline-kit thêm vào. Áp dụng trong ~4 giây, không cần khởi động lại, lựa chọn được lưu.",
|
|
624
|
+
"switching": "Đang chuyển…",
|
|
625
|
+
"offTip": "Giữ nguyên văn bản gốc của Cline",
|
|
626
|
+
"stringsWord": "chuỗi"
|
|
620
627
|
}
|
|
621
628
|
}
|
|
622
629
|
}
|
package/dictionaries/zh-CN.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 6,
|
|
3
3
|
"language": "zh-CN",
|
|
4
4
|
"label": "简体中文",
|
|
5
5
|
"clineVersion": "0.0.32",
|
|
6
|
-
"updated": "2026-09-
|
|
6
|
+
"updated": "2026-09-21",
|
|
7
7
|
"note": "Whole-string exact matches only. Rules run before prefixes; $1..$9 come from rule capture groups. Provider, model and product names are intentionally left in English.",
|
|
8
8
|
"entries": {
|
|
9
9
|
"Session": "会话",
|
|
@@ -618,6 +618,13 @@
|
|
|
618
618
|
"errNoSearch": "未找到工作区搜索框",
|
|
619
619
|
"errNotListed": "列表里没有该项目",
|
|
620
620
|
"errSwitch": "切换失败"
|
|
621
|
+
},
|
|
622
|
+
"language-picker": {
|
|
623
|
+
"title": "界面语言",
|
|
624
|
+
"hint": "由 cline-kit 添加;约 4 秒生效,无需重启,选择会保存。",
|
|
625
|
+
"switching": "切换中…",
|
|
626
|
+
"offTip": "恢复 Cline 原始文案",
|
|
627
|
+
"stringsWord": "条"
|
|
621
628
|
}
|
|
622
629
|
}
|
|
623
630
|
}
|
package/dictionaries/zh-TW.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 2,
|
|
3
3
|
"language": "zh-TW",
|
|
4
4
|
"label": "繁體中文",
|
|
5
5
|
"clineVersion": "0.0.32",
|
|
6
|
-
"updated": "2026-09-
|
|
6
|
+
"updated": "2026-09-21",
|
|
7
7
|
"note": "Whole-string exact matches only. Rules run before prefixes; $1..$9 come from rule capture groups. Provider, model and product names are intentionally left in English. Same 476 strings as zh-CN, localised for Taiwan software usage (外掛程式 / 市集 / 智慧代理 / 重新整理 / 匯入) with character-sense review rather than a mechanical simplified-to-traditional swap.",
|
|
8
8
|
"entries": {
|
|
9
9
|
"Session": "會話",
|
|
@@ -618,6 +618,13 @@
|
|
|
618
618
|
"errNoSearch": "找不到工作區搜尋框",
|
|
619
619
|
"errNotListed": "清單中沒有這個專案",
|
|
620
620
|
"errSwitch": "切換失敗"
|
|
621
|
+
},
|
|
622
|
+
"language-picker": {
|
|
623
|
+
"title": "介面語言",
|
|
624
|
+
"hint": "由 cline-kit 新增;約 4 秒生效,不需重新啟動,設定會儲存。",
|
|
625
|
+
"switching": "切換中…",
|
|
626
|
+
"offTip": "還原 Cline 原始文字",
|
|
627
|
+
"stringsWord": "條"
|
|
621
628
|
}
|
|
622
629
|
}
|
|
623
630
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cline-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Desktop enhancement kit for the Cline desktop app (Windows): keeps every registered project visible in the sidebar, and adds
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Desktop enhancement kit for the Cline desktop app (Windows): keeps every registered project visible in the sidebar, and adds an interface-language picker inside Cline's own Settings (zh-CN, zh-TW, ja, ko, vi, or leave the text alone). Runtime overlay - no binary patching.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cline",
|
|
7
7
|
"desktop-app",
|
package/src/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
// Cline-kit - desktop enhancement kit for the Cline desktop app.
|
|
4
4
|
// Flagship feature: keep every registered project visible in the sidebar.
|
|
5
|
-
// Optional: UI locale packs (zh-CN
|
|
5
|
+
// Optional: UI locale packs (zh-CN reference; zh-TW / ja / ko / vi ship the same key set).
|
|
6
6
|
const cfg = require("./config");
|
|
7
7
|
|
|
8
8
|
function guard() {
|
|
@@ -26,6 +26,7 @@ Usage: ckit <command> [options]
|
|
|
26
26
|
uninstall Restore the original shortcuts
|
|
27
27
|
features List feature plugins and whether each is on
|
|
28
28
|
feature Toggle a plugin: ckit feature enable|disable <id>
|
|
29
|
+
locales List the UI languages that ship with the kit, and switch between them
|
|
29
30
|
update Fetch the latest locale dictionary from GitHub (skipped when offline)
|
|
30
31
|
audit Walk the UI and list strings still without a translation
|
|
31
32
|
dict Dictionary stats and the local override file path
|
|
@@ -40,13 +41,20 @@ Main feature
|
|
|
40
41
|
Optional feature
|
|
41
42
|
locale packs dictionaries/<locale>.json - whole-string replacement only, so provider names,
|
|
42
43
|
model names and code cannot be mangled. Bundled: zh-CN (reference, 476 entries),
|
|
43
|
-
zh-TW, ja, ko, vi
|
|
44
|
-
|
|
44
|
+
zh-TW, ja, ko, vi, plus 'none' to leave Cline's own text alone.
|
|
45
|
+
Pick it INSIDE Cline: Settings -> Interface language (that row is added by this
|
|
46
|
+
kit, and clicking it applies within ~4 s without a restart). Or from here:
|
|
47
|
+
'ckit locales' lists the choices, 'ckit locales ja' switches, and 'ckit update'
|
|
48
|
+
then follows whichever locale is selected.
|
|
49
|
+
Only zh-CN is proofread against the running app.
|
|
45
50
|
|
|
46
51
|
Examples:
|
|
47
52
|
ckit start
|
|
48
53
|
ckit install
|
|
49
54
|
ckit features
|
|
55
|
+
ckit locales
|
|
56
|
+
ckit locales ja
|
|
57
|
+
ckit locales none
|
|
50
58
|
ckit feature disable sidebar-groups
|
|
51
59
|
ckit config --cline-path "D:\\Programs\\Cline\\cline-app.exe"
|
|
52
60
|
`;
|
|
@@ -62,6 +70,33 @@ function parseFlags(argv) {
|
|
|
62
70
|
return { flags, rest };
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
// CJK and accented names are not one cell per code unit, and `padEnd` does not know that.
|
|
74
|
+
function cellWidth(s) {
|
|
75
|
+
let n = 0;
|
|
76
|
+
for (const ch of String(s)) n += /[㐀-䶿一-鿿-가-豈-]/.test(ch) ? 2 : 1;
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
function padCell(s, width) {
|
|
80
|
+
return String(s) + " ".repeat(Math.max(1, width - cellWidth(s)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Language choice is a per-install decision, so say where to change it once per
|
|
84
|
+
// setup path instead of hiding it in the README. Returns nothing; prints itself.
|
|
85
|
+
function languageTip(conf, showOnce) {
|
|
86
|
+
if (showOnce) {
|
|
87
|
+
if (conf.hintLanguageShown) return;
|
|
88
|
+
conf.hintLanguageShown = true;
|
|
89
|
+
cfg.write(conf);
|
|
90
|
+
}
|
|
91
|
+
const dict = require("./dict");
|
|
92
|
+
let codes = [];
|
|
93
|
+
try { codes = dict.choices().map((c) => c.code); } catch (e) { codes = []; }
|
|
94
|
+
const current = conf.dictionary || "zh-CN";
|
|
95
|
+
console.log(`Interface language: ${current} (choices: ${codes.join(", ") || current})`);
|
|
96
|
+
console.log(" pick it inside Cline: Settings -> Interface language");
|
|
97
|
+
console.log(" or here: ckit locales / ckit locales ja");
|
|
98
|
+
}
|
|
99
|
+
|
|
65
100
|
async function main() {
|
|
66
101
|
guard();
|
|
67
102
|
const argv = process.argv.slice(2);
|
|
@@ -80,6 +115,7 @@ async function main() {
|
|
|
80
115
|
console.log(out.debugPortAlive
|
|
81
116
|
? `Cline is up (port ${out.port}, path from ${out.source}) and the overlay is loaded.`
|
|
82
117
|
: `Warning: Cline started but port ${out.port} is not answering; the overlay may not be loaded.`);
|
|
118
|
+
if (out.debugPortAlive) languageTip(conf, true);
|
|
83
119
|
if (!out.debugPortAlive) process.exitCode = 1;
|
|
84
120
|
return;
|
|
85
121
|
}
|
|
@@ -136,6 +172,7 @@ async function main() {
|
|
|
136
172
|
r.shortcuts.forEach((s) => console.log(" " + s));
|
|
137
173
|
console.log("Launcher: " + r.launcher);
|
|
138
174
|
console.log("Open Cline from those shortcuts from now on; `ckit uninstall` restores them.");
|
|
175
|
+
languageTip(cfg.read(), false);
|
|
139
176
|
return;
|
|
140
177
|
}
|
|
141
178
|
|
|
@@ -192,6 +229,38 @@ async function main() {
|
|
|
192
229
|
return;
|
|
193
230
|
}
|
|
194
231
|
|
|
232
|
+
if (cmd === "locales" || cmd === "locale") {
|
|
233
|
+
const dict = require("./dict");
|
|
234
|
+
const conf2 = cfg.read();
|
|
235
|
+
const choices = dict.choices();
|
|
236
|
+
const target = argv.slice(1).filter((a) => !a.startsWith("--"))[0];
|
|
237
|
+
if (target) {
|
|
238
|
+
if (!dict.isKnown(target)) {
|
|
239
|
+
console.error(`Unknown locale "${target}". Bundled: ${choices.map((c) => c.code).join(", ")}`);
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
conf2.dictionary = target;
|
|
244
|
+
cfg.write(conf2);
|
|
245
|
+
console.log(`Language -> ${target} (the injector applies it within ~4 s, no restart needed)`);
|
|
246
|
+
console.log("Cline must be running through cline-kit for the open window to change; otherwise it");
|
|
247
|
+
console.log("applies the next time you start it with `ckit start`.");
|
|
248
|
+
console.log("The same choice is available inside Cline: Settings -> Interface language.");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
console.log("Bundled UI languages (switch with: ckit locales <code>):\n");
|
|
252
|
+
for (const c of choices) {
|
|
253
|
+
const active = (conf2.dictionary || "zh-CN") === c.code;
|
|
254
|
+
const tail = c.off ? " (original text, nothing replaced)"
|
|
255
|
+
: ` v${c.version}`;
|
|
256
|
+
console.log(` ${active ? "* " : " "}${c.code.padEnd(7)} ${padCell(c.native, 12)} ${String(c.strings).padStart(4)} strings${tail}`);
|
|
257
|
+
}
|
|
258
|
+
console.log("\nOnly zh-CN is proofread against the running app; the others are complete but not");
|
|
259
|
+
console.log("reviewed by native speakers - see dictionaries/ if you want to fix a term.");
|
|
260
|
+
console.log("You can also pick this inside Cline: Settings -> Interface language.");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
195
264
|
if (cmd === "dict") {
|
|
196
265
|
console.log(JSON.stringify(require("./payload").info(conf), null, 2));
|
|
197
266
|
console.log("Local override file (highest priority): " + cfg.localDictFile(conf.dictionary));
|
|
@@ -207,9 +276,9 @@ async function main() {
|
|
|
207
276
|
if (flags["storage-key"] !== undefined) { conf.storageKey = String(flags["storage-key"]); changed = true; }
|
|
208
277
|
if (flags.dictionary) {
|
|
209
278
|
const want = flags.dictionary;
|
|
210
|
-
const
|
|
211
|
-
if (!
|
|
212
|
-
console.error(`Unknown locale "${want}".
|
|
279
|
+
const dict = require("./dict");
|
|
280
|
+
if (!dict.isKnown(want)) {
|
|
281
|
+
console.error(`Unknown locale "${want}". Choices: ${dict.choices().map((c) => c.code).join(", ")}`);
|
|
213
282
|
process.exitCode = 1;
|
|
214
283
|
return;
|
|
215
284
|
}
|
package/src/config.js
CHANGED
|
@@ -73,7 +73,9 @@ const DEFAULTS = {
|
|
|
73
73
|
shortcutPath: null,
|
|
74
74
|
autostart: false,
|
|
75
75
|
lastUpdateCheck: 0,
|
|
76
|
-
remoteVersion: null
|
|
76
|
+
remoteVersion: null,
|
|
77
|
+
// one-off: `ckit start` points at `ckit locales` the first time it succeeds
|
|
78
|
+
hintLanguageShown: false
|
|
77
79
|
};
|
|
78
80
|
|
|
79
81
|
function read() {
|
package/src/dict.js
CHANGED
|
@@ -33,6 +33,23 @@ function bundledPath(name) {
|
|
|
33
33
|
return path.join(__dirname, "..", "dictionaries", name + ".json");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// "none" is a first-class choice, not an absent one: the overlay still runs (the sidebar feature
|
|
37
|
+
// needs it) but replaces no text, so the app reads as Cline shipped it. Both the in-app picker and
|
|
38
|
+
// `ckit locales none` land here.
|
|
39
|
+
const NONE = "none";
|
|
40
|
+
function noneDictionary() {
|
|
41
|
+
return {
|
|
42
|
+
version: 0,
|
|
43
|
+
language: NONE,
|
|
44
|
+
label: "English (no replacement)",
|
|
45
|
+
note: "synthetic - translation off",
|
|
46
|
+
entries: {},
|
|
47
|
+
prefixes: [],
|
|
48
|
+
rules: [],
|
|
49
|
+
sourceVersions: { bundled: 0, cached: 0, local: 0 }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
function validate(d) {
|
|
37
54
|
if (!d || typeof d !== "object") return false;
|
|
38
55
|
if (typeof d.version !== "number") return false;
|
|
@@ -96,6 +113,7 @@ function merge(base, extra) {
|
|
|
96
113
|
|
|
97
114
|
function load(cfgObj) {
|
|
98
115
|
const name = cfgObj.dictionary || "zh-CN";
|
|
116
|
+
if (name === NONE) return noneDictionary();
|
|
99
117
|
let dict = readJson(bundledPath(name));
|
|
100
118
|
if (!dict) throw new Error("bundled dictionary missing: " + name);
|
|
101
119
|
const cached = readJson(cfg.cachedDictFile(name));
|
|
@@ -120,6 +138,7 @@ async function fetchRemote(url, timeoutMs) {
|
|
|
120
138
|
// the file name rather than asking users to configure one URL per language.
|
|
121
139
|
function remoteUrlFor(cfgObj) {
|
|
122
140
|
const name = cfgObj.dictionary || "zh-CN";
|
|
141
|
+
if (name === NONE) return "";
|
|
123
142
|
const url = cfgObj.updateUrl || "";
|
|
124
143
|
return /CHANGE_ME/.test(url) ? url : url.replace(/zh-CN\.json(\?.*)?$/i, name + ".json$1");
|
|
125
144
|
}
|
|
@@ -129,6 +148,7 @@ async function update(cfgObj, opts) {
|
|
|
129
148
|
opts = opts || {};
|
|
130
149
|
const now = Date.now();
|
|
131
150
|
if (!opts.force && !cfgObj.autoUpdate) return { updated: false, reason: "autoUpdate disabled" };
|
|
151
|
+
if (cfgObj.dictionary === NONE) return { updated: false, reason: "translation is off" };
|
|
132
152
|
if (!opts.force && (now - (cfgObj.lastUpdateCheck || 0)) < (cfgObj.updateIntervalMs || 86400000)) {
|
|
133
153
|
return { updated: false, reason: "not due yet" };
|
|
134
154
|
}
|
|
@@ -168,4 +188,37 @@ function available() {
|
|
|
168
188
|
} catch (e) { return []; }
|
|
169
189
|
}
|
|
170
190
|
|
|
171
|
-
|
|
191
|
+
// Everything a picker (in-app or CLI) needs to render one row per choice. `none` leads because
|
|
192
|
+
// "leave the app alone" has to be reachable from the same place.
|
|
193
|
+
// Each language is listed in its own script - the one convention that stays readable no matter which
|
|
194
|
+
// language the interface is currently in. `label` inside a dictionary is the name in *that* language
|
|
195
|
+
// (and English for the machine-assisted ones), which would render a list like
|
|
196
|
+
// "简体中文 / Japanese / Korean" and read like a translation of the current UI rather than a choice.
|
|
197
|
+
const NATIVE_NAMES = {
|
|
198
|
+
none: "English",
|
|
199
|
+
"zh-CN": "简体中文",
|
|
200
|
+
"zh-TW": "繁體中文",
|
|
201
|
+
ja: "日本語",
|
|
202
|
+
ko: "한국어",
|
|
203
|
+
vi: "Tiếng Việt"
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function choices() {
|
|
207
|
+
const out = [{
|
|
208
|
+
code: NONE, native: NATIVE_NAMES[NONE], label: "English", strings: 0, version: 0, off: true
|
|
209
|
+
}];
|
|
210
|
+
for (const code of available().sort()) {
|
|
211
|
+
const d = readJson(bundledPath(code)) || {};
|
|
212
|
+
out.push({
|
|
213
|
+
code, native: NATIVE_NAMES[code] || d.label || code, label: d.label || code,
|
|
214
|
+
strings: Object.keys(d.entries || {}).length, version: d.version || 0, off: false
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isKnown(name) {
|
|
221
|
+
return name === NONE || available().includes(name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { load, update, validate, bundledPath, nestedQuantifier, remoteUrlFor, available, choices, isKnown, NONE };
|
package/src/doctor.js
CHANGED
|
@@ -30,6 +30,23 @@ function evaluate(api, expression) {
|
|
|
30
30
|
.then((r) => (r && r.result && r.result.value !== undefined ? r.result.value : null));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Each feature publishes its own shape under `__ckitFeatureState[<id>_stats]`, so the doctor line has
|
|
34
|
+
// to read the numbers instead of assuming the sidebar's. A feature that is only on screen some of the
|
|
35
|
+
// time (the Settings row) must not look like a failure when it is simply not mounted.
|
|
36
|
+
function summarize(stats) {
|
|
37
|
+
if (typeof stats.rows === "number") {
|
|
38
|
+
return stats.rows + " row(s) added of " + stats.registered + " registered, " + stats.nativeGroups + " native";
|
|
39
|
+
}
|
|
40
|
+
if (stats.row) {
|
|
41
|
+
const bits = [stats.row === "rendered" ? "row present" : "row not on screen (" + stats.row + ")"];
|
|
42
|
+
if (stats.current) bits.push("current " + stats.current);
|
|
43
|
+
if (stats.pending && stats.pending !== stats.current) bits.push("pending " + stats.pending);
|
|
44
|
+
if (stats.choices) bits.push(stats.choices + " choices");
|
|
45
|
+
return bits.join(", ");
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify(stats).slice(0, 120);
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
async function run() {
|
|
34
51
|
const conf = cfg.read();
|
|
35
52
|
const found = detect.detect(conf);
|
|
@@ -70,7 +87,7 @@ async function run() {
|
|
|
70
87
|
if (!f.enabled) continue;
|
|
71
88
|
const seen = live.map((p) => p.features[f.id]).filter(Boolean);
|
|
72
89
|
add("feature " + f.id, seen.length > 0, seen.length
|
|
73
|
-
? seen.map((s) => "build " + s.build + (s.stats ? ", " + s.stats
|
|
90
|
+
? seen.map((s) => "build " + s.build + (s.stats ? ", " + summarize(s.stats) : "")).join(" | ")
|
|
74
91
|
: "not present in any page - Cline's DOM may have changed");
|
|
75
92
|
}
|
|
76
93
|
|
package/src/engine.js
CHANGED
|
@@ -54,6 +54,18 @@
|
|
|
54
54
|
|
|
55
55
|
function norm(s) { return String(s || "").replace(/\s+/g, " ").trim(); }
|
|
56
56
|
|
|
57
|
+
// The kit's own controls (the in-app language row) are already rendered in the language the user
|
|
58
|
+
// picked, and translating them would let the observer chase its own output. The check is memoised
|
|
59
|
+
// on the node because this runs for every text node on every mutation.
|
|
60
|
+
function isKitUI(el) {
|
|
61
|
+
if (!el || el.nodeType !== 1) return false;
|
|
62
|
+
if (el.__ckitOwn == null) {
|
|
63
|
+
try { el.__ckitOwn = el.closest && el.closest("[data-ckit-ui]") ? 1 : 0; }
|
|
64
|
+
catch (e) { el.__ckitOwn = 0; }
|
|
65
|
+
}
|
|
66
|
+
return el.__ckitOwn === 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
// Text we replaced is remembered on the node, so switching locale (or pulling a newer dictionary)
|
|
58
70
|
// can start from the original English instead of from last run's output.
|
|
59
71
|
function translateText(node) {
|
|
@@ -61,6 +73,7 @@
|
|
|
61
73
|
if (!raw || raw.length > 800) return;
|
|
62
74
|
var el = node.parentElement;
|
|
63
75
|
if (el && SKIP[el.tagName]) return;
|
|
76
|
+
if (isKitUI(el)) return;
|
|
64
77
|
var src = raw;
|
|
65
78
|
if (node.__ckitOut != null) {
|
|
66
79
|
src = norm(raw) === norm(node.__ckitOut) ? node.__ckitSrc : raw; // the app rewrote it: stale
|
|
@@ -85,6 +98,7 @@
|
|
|
85
98
|
|
|
86
99
|
function translateEl(el) {
|
|
87
100
|
if (!el || el.nodeType !== 1 || !el.getAttribute) return;
|
|
101
|
+
if (isKitUI(el)) return;
|
|
88
102
|
var keep = el.__ckitAttrSrc || (el.__ckitAttrSrc = {});
|
|
89
103
|
for (var i = 0; i < ATTRS.length; i++) {
|
|
90
104
|
var a = ATTRS[i];
|
package/src/features/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Versions are read from the scripts themselves so the registry cannot drift.
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
+
const dict = require("../dict");
|
|
6
7
|
|
|
7
8
|
const DEFS = [
|
|
8
9
|
{
|
|
@@ -23,6 +24,25 @@ const DEFS = [
|
|
|
23
24
|
text: (ctx.featureText || {})[this.id] || {}
|
|
24
25
|
};
|
|
25
26
|
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "language-picker",
|
|
30
|
+
file: "language-picker.js",
|
|
31
|
+
title: "Interface language row inside Cline's own Settings / 在 Cline 设置页里选语言",
|
|
32
|
+
defaultOn: true,
|
|
33
|
+
build(ctx) {
|
|
34
|
+
const current = ctx.cfg.dictionary || "zh-CN";
|
|
35
|
+
// The row belongs under Dark mode. Anchor on the English key and on this locale's rendering of
|
|
36
|
+
// it, because the engine may already have replaced the label when we look.
|
|
37
|
+
const dark = (ctx.entries || {})["Dark mode"];
|
|
38
|
+
return {
|
|
39
|
+
current,
|
|
40
|
+
pendingKey: "cline-kit.language-pending",
|
|
41
|
+
choices: dict.choices(),
|
|
42
|
+
anchors: dark && dark !== "Dark mode" ? ["Dark mode", dark] : ["Dark mode"],
|
|
43
|
+
text: (ctx.featureText || {})[this.id] || {}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
26
46
|
}
|
|
27
47
|
];
|
|
28
48
|
|
|
@@ -63,7 +83,8 @@ function resolve(cfgObj, dictObj) {
|
|
|
63
83
|
const ctx = {
|
|
64
84
|
cfg: cfgObj,
|
|
65
85
|
installDir: cfgObj.clinePath ? path.dirname(cfgObj.clinePath) : "",
|
|
66
|
-
featureText: (dictObj && dictObj.featureText) || {}
|
|
86
|
+
featureText: (dictObj && dictObj.featureText) || {},
|
|
87
|
+
entries: (dictObj && dictObj.entries) || {}
|
|
67
88
|
};
|
|
68
89
|
const picked = [];
|
|
69
90
|
for (const d of DEFS) {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// feature: language-picker
|
|
2
|
+
// Puts Cline's own Settings page in charge of the interface language. Cline has no language setting,
|
|
3
|
+
// so this adds a row that looks like the neighbours it sits between (same structure as the Dark mode
|
|
4
|
+
// and App icon rows) and lists the dictionaries that ship with the kit, plus "English" to opt out.
|
|
5
|
+
//
|
|
6
|
+
// Clicking does not translate the page from inside the page - the payload is composed in Node. The
|
|
7
|
+
// click stores one pending intent in localStorage; the resident injector consumes it on its next
|
|
8
|
+
// cycle (<= 4 s), writes `dictionary` into %APPDATA%\cline-kit\config.json, and re-injects. The
|
|
9
|
+
// engine then re-translates from the original text, so no reload happens and the choice survives a
|
|
10
|
+
// restart. Because the injector clears the key after consuming it, `ckit locales <code>` from the
|
|
11
|
+
// terminal stays authoritative instead of being overwritten by a stale click.
|
|
12
|
+
//
|
|
13
|
+
// Runs inside the webview. Config arrives as window.__clineKitFeature["language-picker"].
|
|
14
|
+
(function () {
|
|
15
|
+
var ID = "language-picker";
|
|
16
|
+
var VER = 1; // human-readable; hot-swap keys off CFG.__build instead
|
|
17
|
+
var CFG = (window.__clineKitFeature && window.__clineKitFeature[ID]) || {};
|
|
18
|
+
var st = window.__clineKitFeatureState = window.__clineKitFeatureState || {};
|
|
19
|
+
var BUILD = String(CFG.__build || "v" + VER);
|
|
20
|
+
if (st[ID + "_build"] === BUILD) return;
|
|
21
|
+
if (st[ID + "_observer"]) { try { st[ID + "_observer"].disconnect(); } catch (e) { } }
|
|
22
|
+
if (st[ID + "_timer"]) clearInterval(st[ID + "_timer"]);
|
|
23
|
+
st[ID + "_build"] = BUILD;
|
|
24
|
+
st[ID] = VER;
|
|
25
|
+
|
|
26
|
+
var TEXT = CFG.text || {};
|
|
27
|
+
var CHOICES = CFG.choices || [];
|
|
28
|
+
var CURRENT = CFG.current || "zh-CN";
|
|
29
|
+
var PENDING = CFG.pendingKey || "cline-kit.language-pending";
|
|
30
|
+
// The row goes under the Dark mode switch. Its label is translated like everything else, so the
|
|
31
|
+
// registry hands over both the English key and this locale's rendering of it.
|
|
32
|
+
var ANCHORS = CFG.anchors || ["Dark mode"];
|
|
33
|
+
|
|
34
|
+
// Fallbacks are English on purpose: English is the app source language, so the row stays
|
|
35
|
+
// readable when a locale has no featureText block yet.
|
|
36
|
+
function t(key, fallback) { return (TEXT && TEXT[key]) || fallback; }
|
|
37
|
+
|
|
38
|
+
// Classes copied from Cline's own rows so the addition does not look bolted on.
|
|
39
|
+
var ROW_CLS = "flex items-center justify-between gap-5 border-b py-4 max-[720px]:flex-col max-[720px]:items-stretch";
|
|
40
|
+
var LEFT_CLS = "flex flex-col gap-1";
|
|
41
|
+
var TITLE_CLS = "text-base font-semibold text-foreground";
|
|
42
|
+
var HINT_CLS = "text-sm text-muted-foreground";
|
|
43
|
+
var GROUP_CLS = "flex shrink-0 items-center gap-2 max-[720px]:flex-wrap";
|
|
44
|
+
var BTN_CLS = "inline-flex h-8 cursor-pointer items-center justify-center whitespace-nowrap rounded-md border bg-background px-3 text-sm font-medium shadow-xs outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] hover:bg-surface-hover hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50";
|
|
45
|
+
var ON_CLS = " ring-2 ring-ring ring-offset-2 ring-offset-background text-foreground";
|
|
46
|
+
var OFF_CLS = " text-muted-foreground";
|
|
47
|
+
|
|
48
|
+
function txt(el) { return (el && (el.innerText || el.textContent) || "").replace(/\s+/g, " ").trim(); }
|
|
49
|
+
|
|
50
|
+
function el(tag, cls, text) {
|
|
51
|
+
var n = document.createElement(tag);
|
|
52
|
+
if (cls) n.className = cls;
|
|
53
|
+
if (text != null) n.textContent = text;
|
|
54
|
+
return n;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The Settings page is the container that holds several native switch rows. Anchoring on the
|
|
58
|
+
// switches instead of a class keeps this working when Tailwind reorders its utility list - but the
|
|
59
|
+
// page is not the only place with switches (a notification grid has eight of its own), so every
|
|
60
|
+
// candidate is scored and the densest one wins.
|
|
61
|
+
function settingsSection() {
|
|
62
|
+
var existing = document.querySelector("[data-ckit-lang]");
|
|
63
|
+
if (existing && existing.parentElement) return existing.parentElement;
|
|
64
|
+
var sws = document.querySelectorAll("[role=switch]");
|
|
65
|
+
var best = null, bestScore = 0;
|
|
66
|
+
for (var i = 0; i < sws.length; i++) {
|
|
67
|
+
var row = sws[i].closest("div.flex");
|
|
68
|
+
var box = row && row.parentElement;
|
|
69
|
+
if (!box || box === best) continue;
|
|
70
|
+
var switchRows = 0, bordered = 0;
|
|
71
|
+
for (var j = 0; j < box.children.length; j++) {
|
|
72
|
+
var c = box.children[j];
|
|
73
|
+
if (c.hasAttribute("data-ckit-lang")) continue;
|
|
74
|
+
if (c.querySelector("[role=switch]")) switchRows++;
|
|
75
|
+
if (/border/.test(String(c.className))) bordered++;
|
|
76
|
+
}
|
|
77
|
+
var score = switchRows * 2 + bordered;
|
|
78
|
+
if (switchRows >= 3 && score > bestScore) { best = box; bestScore = score; }
|
|
79
|
+
}
|
|
80
|
+
return best;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function anchorRow(box) {
|
|
84
|
+
for (var i = 0; i < box.children.length; i++) {
|
|
85
|
+
var c = box.children[i];
|
|
86
|
+
if (c.hasAttribute("data-ckit-lang")) continue;
|
|
87
|
+
var t2 = txt(c);
|
|
88
|
+
for (var k = 0; k < ANCHORS.length; k++) {
|
|
89
|
+
if (ANCHORS[k] && t2.indexOf(ANCHORS[k]) === 0) return c;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pendingCode() {
|
|
96
|
+
try { return localStorage.getItem(PENDING); } catch (e) { return null; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function choose(code) {
|
|
100
|
+
try { localStorage.setItem(PENDING, code); } catch (e) { return; }
|
|
101
|
+
render(); // show "switching" immediately, the injector applies it
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildRow() {
|
|
105
|
+
var row = el("div", ROW_CLS);
|
|
106
|
+
row.setAttribute("data-ckit-lang", String(VER));
|
|
107
|
+
row.setAttribute("data-ckit-ui", ""); // the engine must not re-translate our own labels
|
|
108
|
+
var left = el("div", LEFT_CLS);
|
|
109
|
+
left.appendChild(el("p", TITLE_CLS, t("title", "Interface language")));
|
|
110
|
+
var p = pendingCode();
|
|
111
|
+
var waiting = p && p !== CURRENT;
|
|
112
|
+
left.appendChild(el("p", HINT_CLS, waiting ? t("switching", "Switching...") : t("hint", "Added by cline-kit. Applies in a few seconds, no restart.")));
|
|
113
|
+
row.appendChild(left);
|
|
114
|
+
|
|
115
|
+
var group = el("div", GROUP_CLS);
|
|
116
|
+
group.setAttribute("role", "group");
|
|
117
|
+
group.setAttribute("aria-label", t("title", "Interface language"));
|
|
118
|
+
for (var i = 0; i < CHOICES.length; i++) {
|
|
119
|
+
(function (c) {
|
|
120
|
+
var on = c.code === CURRENT;
|
|
121
|
+
var b = el("button", BTN_CLS + (on ? ON_CLS : OFF_CLS), c.native);
|
|
122
|
+
b.type = "button";
|
|
123
|
+
b.setAttribute("data-ckit-code", c.code); // stable handle for tests and for `ckit doctor`
|
|
124
|
+
b.setAttribute("aria-pressed", on ? "true" : "false");
|
|
125
|
+
b.setAttribute("title", c.off
|
|
126
|
+
? t("offTip", "Show Cline's original text")
|
|
127
|
+
: c.code + " - " + c.strings + " " + t("stringsWord", "strings") + " v" + c.version);
|
|
128
|
+
if (waiting && c.code === p) b.className += ON_CLS;
|
|
129
|
+
b.addEventListener("click", function () { if (c.code !== CURRENT) choose(c.code); });
|
|
130
|
+
group.appendChild(b);
|
|
131
|
+
})(CHOICES[i]);
|
|
132
|
+
}
|
|
133
|
+
row.appendChild(group);
|
|
134
|
+
return row;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function removeRow(box) {
|
|
138
|
+
var olds = box.querySelectorAll(":scope > [data-ckit-lang]");
|
|
139
|
+
for (var i = 0; i < olds.length; i++) olds[i].remove();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function render() {
|
|
143
|
+
var box = settingsSection();
|
|
144
|
+
if (!box) { st[ID + "_stats"] = { version: VER, build: BUILD, row: "no-settings-page", choices: CHOICES.length }; return; }
|
|
145
|
+
var prev = box.querySelector(":scope > [data-ckit-lang]");
|
|
146
|
+
if (prev) {
|
|
147
|
+
// Text-only refresh keeps focus on a button the user may be tabbing through.
|
|
148
|
+
var fresh = buildRow();
|
|
149
|
+
box.replaceChild(fresh, prev);
|
|
150
|
+
} else {
|
|
151
|
+
var after = anchorRow(box);
|
|
152
|
+
if (after && after.nextSibling) box.insertBefore(buildRow(), after.nextSibling);
|
|
153
|
+
else if (after) box.appendChild(buildRow());
|
|
154
|
+
else box.appendChild(buildRow());
|
|
155
|
+
}
|
|
156
|
+
st[ID + "_stats"] = {
|
|
157
|
+
version: VER, build: BUILD, row: "rendered", current: CURRENT,
|
|
158
|
+
pending: pendingCode(), choices: CHOICES.length
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var queued = false;
|
|
163
|
+
function schedule() {
|
|
164
|
+
if (queued) return;
|
|
165
|
+
queued = true;
|
|
166
|
+
(window.requestAnimationFrame || setTimeout)(function () { queued = false; render(); }, 16);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!document.body) return;
|
|
170
|
+
render();
|
|
171
|
+
// The settings page mounts and unmounts as the user navigates; an observer keeps the row present
|
|
172
|
+
// without polling, and the interval is the safety net for subtrees that report no mutation.
|
|
173
|
+
st[ID + "_observer"] = new MutationObserver(function (muts) {
|
|
174
|
+
for (var i = 0; i < muts.length; i++) {
|
|
175
|
+
var n = muts[i].target && muts[i].target.nodeType === 1 ? muts[i].target : muts[i].target && muts[i].target.parentElement;
|
|
176
|
+
if (n && n.closest && n.closest("[data-ckit-lang]")) return; // our own click feedback
|
|
177
|
+
}
|
|
178
|
+
schedule();
|
|
179
|
+
});
|
|
180
|
+
st[ID + "_observer"].observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
181
|
+
st[ID + "_timer"] = setInterval(render, 1500);
|
|
182
|
+
})();
|
package/src/injector.js
CHANGED
|
@@ -6,11 +6,31 @@ const path = require("path");
|
|
|
6
6
|
const { execFileSync } = require("child_process");
|
|
7
7
|
const cfg = require("./config");
|
|
8
8
|
const cdp = require("./cdp");
|
|
9
|
+
const dict = require("./dict");
|
|
9
10
|
const payload = require("./payload");
|
|
10
11
|
|
|
11
12
|
const INTERVAL_MS = 4000;
|
|
12
13
|
const registered = new Map(); // targetId -> { scriptId, dictVersion }
|
|
13
14
|
|
|
15
|
+
// The in-app language row cannot swap the dictionary by itself - the payload is composed here. It
|
|
16
|
+
// writes one pending intent instead, and this consumes it (read + clear in one expression so a
|
|
17
|
+
// second page cannot consume it twice). Clearing is what keeps `ckit locales <code>` authoritative:
|
|
18
|
+
// the key is a request, not the stored setting.
|
|
19
|
+
const PENDING_KEY = "cline-kit.language-pending";
|
|
20
|
+
const CONSUME = "(function(){try{var k=" + JSON.stringify(PENDING_KEY) +
|
|
21
|
+
";var v=localStorage.getItem(k);if(v)localStorage.removeItem(k);return v||\"\"}catch(e){return\"\"}})()";
|
|
22
|
+
|
|
23
|
+
async function consumeLanguageIntent(port) {
|
|
24
|
+
let wanted = "";
|
|
25
|
+
const results = await cdp.eachPage(port, async (api) => {
|
|
26
|
+
const r = await api.rpc("Runtime.evaluate", { expression: CONSUME, returnByValue: true });
|
|
27
|
+
if (r && r.exceptionDetails) return "";
|
|
28
|
+
return r && r.result && typeof r.result.value === "string" ? r.result.value : "";
|
|
29
|
+
});
|
|
30
|
+
for (const item of results) if (item && !item.error && item.result) wanted = item.result;
|
|
31
|
+
return wanted;
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
// Surface injection failures instead of swallowing them; a silent no-op is the worst outcome.
|
|
15
35
|
function log(msg) {
|
|
16
36
|
try {
|
|
@@ -78,6 +98,16 @@ async function main() {
|
|
|
78
98
|
try {
|
|
79
99
|
// re-read every cycle so feature toggles, dictionary updates and path changes apply live
|
|
80
100
|
const conf = cfg.read();
|
|
101
|
+
try {
|
|
102
|
+
const wanted = await consumeLanguageIntent(conf.port || port);
|
|
103
|
+
if (wanted && dict.isKnown(wanted) && wanted !== conf.dictionary) {
|
|
104
|
+
conf.dictionary = wanted;
|
|
105
|
+
cfg.write(conf);
|
|
106
|
+
log("language chosen inside Cline -> " + wanted);
|
|
107
|
+
} else if (wanted && !dict.isKnown(wanted)) {
|
|
108
|
+
log("ignored unknown language request: " + wanted);
|
|
109
|
+
}
|
|
110
|
+
} catch (e) { log("language intent failed: " + e.message); }
|
|
81
111
|
const composed = payload.compose(conf);
|
|
82
112
|
await installOnce(conf.port || port, composed.source, composed.version);
|
|
83
113
|
writeActiveVersion(composed.version);
|
|
@@ -93,4 +123,4 @@ async function main() {
|
|
|
93
123
|
}
|
|
94
124
|
|
|
95
125
|
if (require.main === module) main().catch((e) => { console.error(e); process.exit(1); });
|
|
96
|
-
module.exports = { installOnce, readActiveVersion, activeVersionFile };
|
|
126
|
+
module.exports = { installOnce, readActiveVersion, activeVersionFile, consumeLanguageIntent, PENDING_KEY };
|
package/src/payload.js
CHANGED
|
@@ -26,9 +26,11 @@ function compose(cfgObj) {
|
|
|
26
26
|
const parts = ["const DICT = " + json(d) + ";", ENGINE];
|
|
27
27
|
|
|
28
28
|
for (const f of picked) {
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
29
|
+
// Per-feature build hash over source *and* config: editing the script hot-swaps it, and so does a
|
|
30
|
+
// config change (a different `current` language, a new hide path) - keying on source alone left a
|
|
31
|
+
// feature running with stale config until a full reload.
|
|
32
|
+
const plain = Object.assign({}, f.config);
|
|
33
|
+
const cfgObjForFeature = Object.assign({}, plain, { __build: hash12(f.source + "\u0000" + json(plain)) });
|
|
32
34
|
parts.push(
|
|
33
35
|
"window.__clineKitFeature = window.__clineKitFeature || {};",
|
|
34
36
|
"window.__clineKitFeature[" + json(f.id) + "] = " + json(cfgObjForFeature) + ";",
|