cline-kit 0.1.2 → 0.2.1

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,56 @@ 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.1] - 2026-09-21
8
+
9
+ ### Changed
10
+
11
+ - **zh-TW dictionary v3**: the send action was rendered two different ways in the same locale
12
+ (發送 in four strings, 送出 in four others). It is now 傳送, which is both what a zh-TW reader sees
13
+ in Microsoft and Google products for that button and, more importantly, self-consistent.
14
+ - Locale terminology was cross-checked against professional human localizations - the MIT-licensed
15
+ `microsoft/vscode-loc` packs (zh-hant/ja/ko) and the MPL-2.0 Firefox localizations (vi, since no
16
+ VS Code Vietnamese pack exists). `scripts/terminology-crosscheck.js` reports divergences and
17
+ changes nothing; [`docs/terminology.md`](docs/terminology.md) records the sources, the coverage
18
+ limits, and every divergence that was reviewed and deliberately kept (會話 for a chat session
19
+ rather than Microsoft's 工作階段, 작업 공간 over 작업 영역, 스킬 for a product noun). One item is
20
+ marked genuinely arguable rather than settled: 智慧代理 for "agent".
21
+ - NOTICE no longer claims nothing was taken from third-party translation projects, which stopped
22
+ being true the moment a term was adopted from one.
23
+
24
+
25
+ ## [0.2.0] - 2026-09-21
26
+
27
+
28
+ ### Added
29
+
30
+ - **`language-picker` feature - the interface language is now chosen inside Cline.** Settings gains an
31
+ "Interface language" row under Dark mode, built from the same markup as the rows around it, listing
32
+ each language in its own script (English / 日本語 / 한국어 / Tiếng Việt / 简体中文 / 繁體中文).
33
+ Clicking writes one pending intent into `localStorage`; the resident injector consumes it on its next
34
+ cycle, stores it as the configured dictionary, and re-injects, so the open window follows in ~4 s with
35
+ no reload and the choice survives a restart. `ckit feature disable language-picker` removes the row.
36
+ - `none` / `English` as a first-class language: run the overlay for the sidebar only and leave every
37
+ string Cline shipped. Available in the app row, `ckit locales none` and `--dictionary=none`; with it
38
+ selected `ckit update` reports "translation is off" instead of fetching.
39
+
40
+ ### Changed
41
+
42
+ - The terminal is now the secondary route, not the only one: `ckit locales`, `--help` and both READMEs
43
+ point at Settings first, and `ckit install` / the first `ckit start` say "pick it inside Cline".
44
+ - Language names are listed in the language they name. `label` inside a dictionary is that language's
45
+ own wording (English for the machine-assisted packs), so a list built from it read as
46
+ "简体中文 / Japanese / Korean" - a translation of whatever is on screen instead of a choice.
47
+ - Per-feature build hashes now cover the feature's **config** as well as its source. Keyed on source
48
+ alone, a locale switch left `language-picker` running with the previous `current` until a full reload.
49
+ - `ckit doctor` summarises each feature with its own numbers instead of assuming the sidebar's, and a
50
+ feature that is only mounted some of the time (a Settings row) no longer reads as a failure.
51
+ - The overlay leaves its own UI alone: nodes marked `data-ckit-ui` are skipped by the translator
52
+ (memoised per node), which is what keeps the language row from being re-translated by itself.
53
+ - `npm test` is 28 checks: feature text is compared against **every** dictionary and every feature,
54
+ the picker's choice list and native names are pinned, `none` is verified as a real setting, and a
55
+ config-only change is required to bump the payload version.
56
+
7
57
  ## [0.1.2] - 2026-09-21
8
58
 
9
59
  ### Added
package/NOTICE CHANGED
@@ -54,7 +54,15 @@ app and out of `apps/examples/desktop-app` in the Apache-2.0 `cline/cline` repos
54
54
  are original work of this project.
55
55
 
56
56
  `zh-TW.json`, `ja.json`, `ko.json` and `vi.json` hold the same 476 keys, translated with machine
57
- assistance and then terminology-checked inside this project. zh-TW additionally went through a
58
- character-sense review rather than a simplified-to-traditional swap. None of the four has been reviewed
59
- by a native speaker of the target language, and each file says so in its own `note` field. They contain
60
- no code, no assets and no text taken from any third-party translation project.
57
+ assistance. Their terminology was then cross-checked against professional human localizations of the
58
+ same software vocabulary - the MIT-licensed `microsoft/vscode-loc` packs (zh-hant, ja, ko) and the
59
+ MPL-2.0 Firefox localizations (vi) - using `scripts/terminology-crosscheck.js`. Where the reference
60
+ term was simply the standard one we adopted it; where we kept a different rendering the reason is
61
+ recorded in `docs/terminology.md`. zh-TW additionally went through a character-sense review rather
62
+ than a simplified-to-traditional swap.
63
+
64
+ None of the four has been reviewed by a native speaker of the target language, and each file says so
65
+ in its own `note` field. The cross-check compares terminology; it cannot judge fluency or register.
66
+ No translation memory, string table or file from a third-party localization project is redistributed
67
+ in this repository - the reference packs are downloaded at check time into a local cache and are
68
+ never committed.
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 — five ship: 简体中文 (reference, proofread), plus 繁體中文 /
17
- 日本語 / 한국어 / Tiếng Việt. Locales exist because the same injection channel can carry them — they
18
- are not what the 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,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 # 25 checks, no dependencies
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
- Language is a per-install choice, and Cline has no language setting of its own, so the kit exposes it:
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 zh-CN # back to the reference dictionary
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 same write if you prefer the generic config command. Once a
130
- non-default locale is selected, `ckit update` hot-updates *that* dictionary.
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,17 @@ 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. Pick one with `ckit locales` see
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). Terminology was cross-checked against
160
+ professional human localizations and the deliberate differences are listed in
161
+ [`docs/terminology.md`](docs/terminology.md); authoring guide:
150
162
  [`docs/dictionary-pipeline.zh-CN.md`](docs/dictionary-pipeline.zh-CN.md).
151
163
 
152
164
  ## Limitations
@@ -160,9 +172,10 @@ non-default locale is selected, `ckit update` hot-updates *that* dictionary.
160
172
  repointed) has no debug port to attach to, so you get plain Cline.
161
173
  * **Cline updates can break things.** If the app renames a label, the locale leaves it in English; if
162
174
  it restructures the sidebar, `sidebar-groups` needs updating. Run `ckit audit` and open an issue.
163
- * **The language switch is in the kit, not in Cline.** Cline has no language setting, so there is no
164
- gear icon to find it under: `ckit locales` is the picker (and `ckit install` / the first `ckit start`
165
- point at it).
175
+ * **The language row is ours, not Cline's.** It is inserted into the Settings page next to Dark mode,
176
+ which is the right place to look but not a location Cline documents. If that page is restructured,
177
+ `language-picker` needs its anchor updated (`ckit doctor` reports whether the row is present);
178
+ `ckit feature disable language-picker` removes it.
166
179
  * **Per-model description blurbs stay English** — free-form text from a remote provider catalogue.
167
180
  * **Same-named projects.** Cline's own group headers expose only the folder name, never the path, so if
168
181
  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
- 한국어 / Tiếng Việt。语言包之所以在这里,是因为同一条注入通道顺带能承载它——它不是这个项目存在的理由。
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 # 25 项无依赖自检
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 zh-CN # 换回基准词典
120
+ ckit locales none # 不再替换 Cline 自己的文案
117
121
  ```
118
122
 
119
123
  `ckit install` 每次都会打印当前语言和这条命令,第一次 `ckit start` 成功后也会提示一次。
120
- `ckit config --dictionary=<语言>` 是等价的写法。选定非默认语言后,`ckit update` 热更新的就是那一份。
124
+ `ckit config --dictionary=<语言>` 是等价的底层写法。选定非默认语言后,`ckit update` 热更新的就是那一份。
125
+
126
+ 这一行是本工具加进去的,Cline 原生没有语言设置;不想要就 `ckit feature disable language-picker`。
121
127
 
122
128
  ## 功能
123
129
 
@@ -128,12 +134,15 @@ 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
- 切换:`ckit locales`(列出并切换)或 `ckit locales ja`,正在打开的窗口**不需要重启或刷新**就会跟着变;
136
- `ckit update` 也只更新你选的那一份。详见[选择语言](#选择语言)。制作流程见
143
+ 切换:Cline 设置页的「界面语言」,或 `ckit locales ja`,正在打开的窗口**不需要重启或刷新**就会跟着变;
144
+ `ckit update` 也只更新你选的那一份。详见[选择语言](#选择语言)。术语已对照官方人工本地化校验,
145
+ 有意保留的差异见 [`docs/terminology.md`](docs/terminology.md);制作流程见
137
146
  [`docs/dictionary-pipeline.zh-CN.md`](docs/dictionary-pipeline.zh-CN.md)。
138
147
 
139
148
  ## 已知限制
@@ -145,8 +154,9 @@ ckit locales zh-CN # 换回基准词典
145
154
  得到的是原版界面。
146
155
  * **Cline 升级可能失效。** 文案改了,语言包会留英文;侧边栏结构改了,`sidebar-groups` 需要跟进。
147
156
  跑一次 `ckit audit` 并开 issue。
148
- * **语言开关在工具这一侧,不在 Cline 里。** Cline 没有语言设置,所以界面里找不到那个齿轮:
149
- `ckit locales` 就是选择器(`ckit install` 和第一次 `ckit start` 都会提示它)。
157
+ * **语言那一行是本项目加的,不是 Cline 原生的。** 它插在设置页「深色模式」下面——位置符合直觉,
158
+ 但那个位置并不是 Cline 公开约定的。那一页改版时需要更新 `language-picker` 的锚点
159
+ (`ckit doctor` 会报这一行在不在),不想要就 `ckit feature disable language-picker`。
150
160
  * **模型下方的一句英文简介不覆盖**——来自云端目录的自由文本,条数随服务商变化。
151
161
  * **同名项目。** Cline 自己的分组标题只有文件夹名、没有路径,所以登记表里出现两个不同盘符下的 `LLM`
152
162
  且其中一个已有原生分组时,无法判断哪一个才是它。此时两行都列出(带上级目录区分),并且不在这种行里
@@ -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,10 +1,10 @@
1
1
  {
2
- "version": 1,
2
+ "version": 3,
3
3
  "language": "zh-TW",
4
4
  "label": "繁體中文",
5
5
  "clineVersion": "0.0.32",
6
- "updated": "2026-09-20",
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.",
6
+ "updated": "2026-09-21",
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. Terminology cross-checked against the official VS Code zh-hant language pack (MIT) - see docs/terminology.md.",
8
8
  "entries": {
9
9
  "Session": "會話",
10
10
  "Sessions": "會話清單",
@@ -54,8 +54,8 @@
54
54
  "What would you like to build?": "您想建立什麼?",
55
55
  "Ask to make changes, @mention files, reference #PRs, or run /commands.": "直接提出需求,也能 @提及檔案、引用 #PR 或執行 /命令。",
56
56
  "Enter your question or type / for commands or @ for context": "輸入問題,或輸入 / 叫出命令、@ 加入上下文",
57
- "Send message": "發送訊息",
58
- "Send (Enter)": "發送 (Enter)",
57
+ "Send message": "傳送訊息",
58
+ "Send (Enter)": "傳送 (Enter)",
59
59
  "Attach files": "新增附件",
60
60
  "Model and provider": "模型與供應商",
61
61
  "Thinking level": "思考程度",
@@ -105,7 +105,7 @@
105
105
  "Desktop": "桌面端",
106
106
  "Manage desktop preferences for this browser and CLI environment.": "管理此瀏覽器與 CLI 環境的桌面偏好設定。",
107
107
  "Desktop notifications": "桌面通知",
108
- "Notify only while the Cline window is in the background. Clicking a notification opens its session.": "僅在 Cline 視窗於背景執行時發送通知;點擊通知會開啟對應的會話。",
108
+ "Notify only while the Cline window is in the background. Clicking a notification opens its session.": "僅在 Cline 視窗於背景執行時傳送通知;點擊通知會開啟對應的會話。",
109
109
  "Notifications are blocked in system settings.": "通知已在系統設定中被封鎖。",
110
110
  "Check permission": "檢查權限",
111
111
  "Event": "事件",
@@ -430,7 +430,7 @@
430
430
  "Pick your repositories": "選取您的程式庫",
431
431
  "Previous generated image": "上一張產生的圖片",
432
432
  "Pull request status": "拉取請求狀態",
433
- "Read, search, draft, and send email from your Gmail account.": "從您的 Gmail 帳戶讀取、搜尋、起草並發送郵件。",
433
+ "Read, search, draft, and send email from your Gmail account.": "從您的 Gmail 帳戶讀取、搜尋、起草並傳送郵件。",
434
434
  "Ready to merge": "可以合併",
435
435
  "Refresh channels": "重新整理頻道",
436
436
  "Refresh pull request status": "重新整理拉取請求狀態",
@@ -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.2",
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.1",
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. Pick one with 'ckit locales' (lists + switches); the older
46
- 'ckit config --dictionary=ja' does the same. 'ckit update' then follows that
47
- 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.
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.available(); } catch (e) { codes = []; }
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} (bundled: ${codes.join(", ") || current})`);
83
- console.log(" list / switch with: ckit locales e.g. ckit locales ja");
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 have = dict.available();
235
+ const choices = dict.choices();
222
236
  const target = argv.slice(1).filter((a) => !a.startsWith("--"))[0];
223
237
  if (target) {
224
- if (!have.includes(target)) {
225
- console.error(`Unknown locale "${target}". Bundled: ${have.join(", ")}`);
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 code of have) {
238
- const d = JSON.parse(fs.readFileSync(dict.bundledPath(code), "utf8"));
239
- const n = Object.keys(d.entries || {}).length;
240
- const active = (conf2.dictionary || "zh-CN") === code;
241
- console.log(` ${active ? "* " : " "}${code.padEnd(7)} ${(d.label || "").padEnd(12)} ${String(n).padStart(4)} strings v${d.version}`);
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 have = require("./dict").available();
264
- if (!have.includes(want)) {
265
- 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(", ")}`);
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
- 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) + ";",