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 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 gated on
64
- an `NPM_TOKEN` secret.
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 (简体中文 today; 繁體中文 / 日本語 / 한국어 / Tiếng Việt use the
17
- same format). Locales exist because the same injection channel can carry them they are not what the
18
- project is for.
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 # 22 checks, no dependencies
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. Switch with `ckit config --dictionary=ja`; the change
132
- applies to the open window without a reload, and `ckit update` follows the locale you picked.
133
- Authoring guide: [`docs/dictionary-pipeline.zh-CN.md`](docs/dictionary-pipeline.zh-CN.md).
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
- **附属功能:** 界面语言包(目前简体中文;繁體中文 / 日本語 / 한국어 / Tiếng Việt 用同一套格式)。
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 # 22 项无依赖自检
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
- 切换:`ckit config --dictionary=ja`,正在打开的窗口**不需要重启或刷新**就会跟着变;
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
  且其中一个已有原生分组时,无法判断哪一个才是它。此时两行都列出(带上级目录区分),并且不在这种行里
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "language": "ja",
4
4
  "label": "Japanese",
5
5
  "clineVersion": "0.0.32",
6
- "updated": "2026-09-20",
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
  }
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "language": "ko",
4
4
  "label": "Korean",
5
5
  "clineVersion": "0.0.32",
6
- "updated": "2026-09-20",
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
  }
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "language": "vi",
4
4
  "label": "Vietnamese",
5
5
  "clineVersion": "0.0.32",
6
- "updated": "2026-09-20",
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
  }
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": 5,
2
+ "version": 6,
3
3
  "language": "zh-CN",
4
4
  "label": "简体中文",
5
5
  "clineVersion": "0.0.32",
6
- "updated": "2026-09-20",
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
  }
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "language": "zh-TW",
4
4
  "label": "繁體中文",
5
5
  "clineVersion": "0.0.32",
6
- "updated": "2026-09-20",
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.1.1",
4
- "description": "Desktop enhancement kit for the Cline desktop app (Windows): keeps every registered project visible in the sidebar, and adds optional UI locale packs (zh-CN, zh-TW, ja, ko, vi). Runtime overlay - no binary patching.",
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 today; zh-TW / ja / ko / vi follow the same data format).
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. Choose one: ckit config --dictionary=ja (ckit update then
44
- follows that locale). Only zh-CN is proofread against the running app.
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 have = require("./dict").available();
211
- if (!have.includes(want)) {
212
- console.error(`Unknown locale "${want}". Bundled dictionaries: ${have.join(", ")}`);
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
- module.exports = { load, update, validate, bundledPath, nestedQuantifier, remoteUrlFor, available };
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.rows + " row(s) added of " + s.stats.registered + " registered, " + s.stats.nativeGroups + " native" : "")).join(" | ")
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];
@@ -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
- // per-feature build hash: the feature's own guard compares against it, so editing the script
30
- // hot-swaps it without anyone remembering to bump a VER constant
31
- const cfgObjForFeature = Object.assign({}, f.config, { __build: hash12(f.source) });
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) + ";",