cline-kit 0.1.2 → 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 +31 -0
- package/README.md +24 -13
- package/README.zh-CN.md +18 -9
- 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 +34 -18
- 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,37 @@ 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
|
+
|
|
7
38
|
## [0.1.2] - 2026-09-21
|
|
8
39
|
|
|
9
40
|
### Added
|
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,7 +82,7 @@ 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:
|
|
@@ -117,17 +118,24 @@ then gives you the enhanced sidebar. If you would rather keep your own launcher,
|
|
|
117
118
|
|
|
118
119
|
### Choosing a language
|
|
119
120
|
|
|
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:
|
|
121
126
|
|
|
122
127
|
```bash
|
|
123
128
|
ckit locales # what ships, how many strings each covers, which one is active
|
|
124
129
|
ckit locales ja # switch; the open window changes within ~4 s, no restart
|
|
125
|
-
ckit locales
|
|
130
|
+
ckit locales none # stop replacing Cline's own text
|
|
126
131
|
```
|
|
127
132
|
|
|
128
133
|
`ckit install` prints the current language and this command, and the first `ckit start` says it once.
|
|
129
|
-
`ckit config --dictionary=<code>` is the
|
|
130
|
-
|
|
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`.
|
|
131
139
|
|
|
132
140
|
## Features
|
|
133
141
|
|
|
@@ -140,13 +148,15 @@ non-default locale is selected, `ckit update` hot-updates *that* dictionary.
|
|
|
140
148
|
session; otherwise the kit keeps project grouping on. Two projects that share a folder name are
|
|
141
149
|
labelled with their parent folder - `LLM (workspace)` - and every row carries the full path as its
|
|
142
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).
|
|
143
153
|
* **locale packs** (`dictionaries/<locale>.json`) — whole-string text replacement only, so model
|
|
144
154
|
names, provider names, tool identifiers and code cannot be mangled. The corpus is 476 strings plus
|
|
145
155
|
30 pattern rules, built by combining a UI walk with extraction from the app's own source. Five
|
|
146
156
|
dictionaries ship: **zh-CN** (reference, proofread against the running app), **zh-TW**, **ja**,
|
|
147
157
|
**ko**, **vi** - complete but machine-assisted and *not* reviewed by native speakers, so a pull
|
|
148
|
-
request fixing a term is genuinely welcome.
|
|
149
|
-
[Choosing a language](#choosing-a-language). Authoring guide:
|
|
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:
|
|
150
160
|
[`docs/dictionary-pipeline.zh-CN.md`](docs/dictionary-pipeline.zh-CN.md).
|
|
151
161
|
|
|
152
162
|
## Limitations
|
|
@@ -160,9 +170,10 @@ non-default locale is selected, `ckit update` hot-updates *that* dictionary.
|
|
|
160
170
|
repointed) has no debug port to attach to, so you get plain Cline.
|
|
161
171
|
* **Cline updates can break things.** If the app renames a label, the locale leaves it in English; if
|
|
162
172
|
it restructures the sidebar, `sidebar-groups` needs updating. Run `ckit audit` and open an issue.
|
|
163
|
-
* **The language
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
166
177
|
* **Per-model description blurbs stay English** — free-form text from a remote provider catalogue.
|
|
167
178
|
* **Same-named projects.** Cline's own group headers expose only the folder name, never the path, so if
|
|
168
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,8 +10,9 @@
|
|
|
10
10
|
在界面上根本不存在。cline-kit 把所有已登记项目常驻列出,样式与原生分组一致,并且可以直接从侧边栏
|
|
11
11
|
切进一个还没有会话的项目。
|
|
12
12
|
|
|
13
|
-
**附属功能:** 界面语言包,随包 5 份:**zh-CN**(基准,逐条对着界面校过)、繁體中文 / 日本語 /
|
|
14
|
-
|
|
13
|
+
**附属功能:** 界面语言包,随包 5 份:**zh-CN**(基准,逐条对着界面校过)、繁體中文 / 日本語 / 한국어 /
|
|
14
|
+
Tiếng Việt,另加一项 `English` 表示不替换。选择入口在 Cline 里面:设置页会多出一行**界面语言**。
|
|
15
|
+
语言包之所以在这里,是因为同一条注入通道顺带能承载它——它不是这个项目存在的理由。
|
|
15
16
|
|
|
16
17
|
## 要解决的问题
|
|
17
18
|
|
|
@@ -73,7 +74,7 @@ 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
|
装完统一确认一次:
|
|
@@ -108,16 +109,21 @@ ckit locales # 可选:看有哪些语言、换语言
|
|
|
108
109
|
|
|
109
110
|
### 选择语言
|
|
110
111
|
|
|
111
|
-
Cline
|
|
112
|
+
**在软件里**:点开 Cline 侧边栏底部的齿轮进「设置」,用**界面语言**那一行。它排在「深色模式」下面,
|
|
113
|
+
样式和邻居一致,点完约 4 秒生效——不用重启,选择会记住。列表里的 `English` 表示不替换任何文案。
|
|
114
|
+
|
|
115
|
+
**在终端里**(改的是同一个设置):
|
|
112
116
|
|
|
113
117
|
```bash
|
|
114
118
|
ckit locales # 看有哪些语言、各覆盖多少条、当前用的是哪一个(* 号标记)
|
|
115
119
|
ckit locales ja # 切换;正在打开的窗口约 4 秒内跟着变,不用重启
|
|
116
|
-
ckit locales
|
|
120
|
+
ckit locales none # 不再替换 Cline 自己的文案
|
|
117
121
|
```
|
|
118
122
|
|
|
119
123
|
`ckit install` 每次都会打印当前语言和这条命令,第一次 `ckit start` 成功后也会提示一次。
|
|
120
|
-
`ckit config --dictionary=<语言>`
|
|
124
|
+
`ckit config --dictionary=<语言>` 是等价的底层写法。选定非默认语言后,`ckit update` 热更新的就是那一份。
|
|
125
|
+
|
|
126
|
+
这一行是本工具加进去的,Cline 原生没有语言设置;不想要就 `ckit feature disable language-picker`。
|
|
121
127
|
|
|
122
128
|
## 功能
|
|
123
129
|
|
|
@@ -128,11 +134,13 @@ ckit locales zh-CN # 换回基准词典
|
|
|
128
134
|
如果你亲手点了 Cline 的排序按钮,本会话内就以你的选择为准不再干预;否则增强层会持续保持分组模式。
|
|
129
135
|
两个同名项目(比如两块盘上都有 `LLM`)会带上上级目录名显示成 `LLM (workspace)`,仍然重名就加序号,
|
|
130
136
|
而鼠标悬停始终是完整路径。设计说明见 [`docs/features.zh-CN.md`](docs/features.zh-CN.md)。
|
|
137
|
+
* **`language-picker`**(默认开启)——在 Cline 自己的设置页里加一行「界面语言」,见
|
|
138
|
+
[选择语言](#选择语言)。
|
|
131
139
|
* **语言包**(`dictionaries/<locale>.json`)——只做**整串精确匹配**替换,因此不会误伤模型名、服务商名、
|
|
132
140
|
工具标识和代码。语料为 476 条词条 + 30 条规则,来源是「界面走查 + 从应用自身源码提取」两路合并。
|
|
133
141
|
现在随包附带 5 份词典:**zh-CN**(基准,逐条对着运行中的界面校对过)、**zh-TW**、**ja**、**ko**、
|
|
134
142
|
**vi** —— 后四份条目齐全但属于机器辅助翻译、未经母语者审校,术语有偏差欢迎提 PR 直接改。
|
|
135
|
-
|
|
143
|
+
切换:Cline 设置页的「界面语言」,或 `ckit locales ja`,正在打开的窗口**不需要重启或刷新**就会跟着变;
|
|
136
144
|
`ckit update` 也只更新你选的那一份。详见[选择语言](#选择语言)。制作流程见
|
|
137
145
|
[`docs/dictionary-pipeline.zh-CN.md`](docs/dictionary-pipeline.zh-CN.md)。
|
|
138
146
|
|
|
@@ -145,8 +153,9 @@ ckit locales zh-CN # 换回基准词典
|
|
|
145
153
|
得到的是原版界面。
|
|
146
154
|
* **Cline 升级可能失效。** 文案改了,语言包会留英文;侧边栏结构改了,`sidebar-groups` 需要跟进。
|
|
147
155
|
跑一次 `ckit audit` 并开 issue。
|
|
148
|
-
*
|
|
149
|
-
|
|
156
|
+
* **语言那一行是本项目加的,不是 Cline 原生的。** 它插在设置页「深色模式」下面——位置符合直觉,
|
|
157
|
+
但那个位置并不是 Cline 公开约定的。那一页改版时需要更新 `language-picker` 的锚点
|
|
158
|
+
(`ckit doctor` 会报这一行在不在),不想要就 `ckit feature disable language-picker`。
|
|
150
159
|
* **模型下方的一句英文简介不覆盖**——来自云端目录的自由文本,条数随服务商变化。
|
|
151
160
|
* **同名项目。** Cline 自己的分组标题只有文件夹名、没有路径,所以登记表里出现两个不同盘符下的 `LLM`
|
|
152
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
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// Flagship feature: keep every registered project visible in the sidebar.
|
|
5
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
|
-
const fs = require("fs");
|
|
8
7
|
|
|
9
8
|
function guard() {
|
|
10
9
|
if (typeof WebSocket !== "function") {
|
|
@@ -42,9 +41,12 @@ Main feature
|
|
|
42
41
|
Optional feature
|
|
43
42
|
locale packs dictionaries/<locale>.json - whole-string replacement only, so provider names,
|
|
44
43
|
model names and code cannot be mangled. Bundled: zh-CN (reference, 476 entries),
|
|
45
|
-
zh-TW, ja, ko, vi
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
48
50
|
|
|
49
51
|
Examples:
|
|
50
52
|
ckit start
|
|
@@ -52,6 +54,7 @@ Examples:
|
|
|
52
54
|
ckit features
|
|
53
55
|
ckit locales
|
|
54
56
|
ckit locales ja
|
|
57
|
+
ckit locales none
|
|
55
58
|
ckit feature disable sidebar-groups
|
|
56
59
|
ckit config --cline-path "D:\\Programs\\Cline\\cline-app.exe"
|
|
57
60
|
`;
|
|
@@ -67,6 +70,16 @@ function parseFlags(argv) {
|
|
|
67
70
|
return { flags, rest };
|
|
68
71
|
}
|
|
69
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
|
+
|
|
70
83
|
// Language choice is a per-install decision, so say where to change it once per
|
|
71
84
|
// setup path instead of hiding it in the README. Returns nothing; prints itself.
|
|
72
85
|
function languageTip(conf, showOnce) {
|
|
@@ -77,10 +90,11 @@ function languageTip(conf, showOnce) {
|
|
|
77
90
|
}
|
|
78
91
|
const dict = require("./dict");
|
|
79
92
|
let codes = [];
|
|
80
|
-
try { codes = dict.
|
|
93
|
+
try { codes = dict.choices().map((c) => c.code); } catch (e) { codes = []; }
|
|
81
94
|
const current = conf.dictionary || "zh-CN";
|
|
82
|
-
console.log(`Interface language: ${current} (
|
|
83
|
-
console.log("
|
|
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");
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
async function main() {
|
|
@@ -218,11 +232,11 @@ async function main() {
|
|
|
218
232
|
if (cmd === "locales" || cmd === "locale") {
|
|
219
233
|
const dict = require("./dict");
|
|
220
234
|
const conf2 = cfg.read();
|
|
221
|
-
const
|
|
235
|
+
const choices = dict.choices();
|
|
222
236
|
const target = argv.slice(1).filter((a) => !a.startsWith("--"))[0];
|
|
223
237
|
if (target) {
|
|
224
|
-
if (!
|
|
225
|
-
console.error(`Unknown locale "${target}". Bundled: ${
|
|
238
|
+
if (!dict.isKnown(target)) {
|
|
239
|
+
console.error(`Unknown locale "${target}". Bundled: ${choices.map((c) => c.code).join(", ")}`);
|
|
226
240
|
process.exitCode = 1;
|
|
227
241
|
return;
|
|
228
242
|
}
|
|
@@ -231,17 +245,19 @@ async function main() {
|
|
|
231
245
|
console.log(`Language -> ${target} (the injector applies it within ~4 s, no restart needed)`);
|
|
232
246
|
console.log("Cline must be running through cline-kit for the open window to change; otherwise it");
|
|
233
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.");
|
|
234
249
|
return;
|
|
235
250
|
}
|
|
236
251
|
console.log("Bundled UI languages (switch with: ckit locales <code>):\n");
|
|
237
|
-
for (const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
console.log(` ${active ? "* " : " "}${code.padEnd(7)} ${(
|
|
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}`);
|
|
242
257
|
}
|
|
243
258
|
console.log("\nOnly zh-CN is proofread against the running app; the others are complete but not");
|
|
244
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.");
|
|
245
261
|
return;
|
|
246
262
|
}
|
|
247
263
|
|
|
@@ -260,9 +276,9 @@ async function main() {
|
|
|
260
276
|
if (flags["storage-key"] !== undefined) { conf.storageKey = String(flags["storage-key"]); changed = true; }
|
|
261
277
|
if (flags.dictionary) {
|
|
262
278
|
const want = flags.dictionary;
|
|
263
|
-
const
|
|
264
|
-
if (!
|
|
265
|
-
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(", ")}`);
|
|
266
282
|
process.exitCode = 1;
|
|
267
283
|
return;
|
|
268
284
|
}
|
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) + ";",
|