opencode-i18n 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/commands/i18n.md +18 -0
- package/i18n/config.json +23 -0
- package/i18n/lib.ts +307 -0
- package/i18n/locales/ar.json +277 -0
- package/i18n/locales/bs.json +277 -0
- package/i18n/locales/da.json +277 -0
- package/i18n/locales/de.json +277 -0
- package/i18n/locales/en.json +29 -0
- package/i18n/locales/es.json +277 -0
- package/i18n/locales/fr.json +277 -0
- package/i18n/locales/ja.json +277 -0
- package/i18n/locales/ko.json +277 -0
- package/i18n/locales/no.json +277 -0
- package/i18n/locales/pl.json +277 -0
- package/i18n/locales/pt-BR.json +277 -0
- package/i18n/locales/ru.json +277 -0
- package/i18n/locales/th.json +277 -0
- package/i18n/locales/tr.json +277 -0
- package/i18n/locales/uk.json +277 -0
- package/i18n/locales/zh-Hans.json +279 -0
- package/i18n/locales/zh-Hant.json +277 -0
- package/package.json +29 -0
- package/plugins/i18n/index.ts +241 -0
- package/plugins/i18n/server.ts +17 -0
- package/tools/i18n-state.ts +293 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "繁體中文",
|
|
3
|
+
"language_picker": {
|
|
4
|
+
"question": "選擇 OpenCode 介面語言",
|
|
5
|
+
"option_descriptions": {
|
|
6
|
+
"en": "切換到英文介面",
|
|
7
|
+
"zh-Hans": "切換到簡體中文介面",
|
|
8
|
+
"zh-Hant": "切換到繁體中文介面",
|
|
9
|
+
"ko": "切換到韓語介面",
|
|
10
|
+
"de": "切換到德語介面",
|
|
11
|
+
"es": "切換到西班牙語介面",
|
|
12
|
+
"fr": "切換到法語介面",
|
|
13
|
+
"da": "切換到丹麥語介面",
|
|
14
|
+
"ja": "切換到日語介面",
|
|
15
|
+
"pl": "切換到波蘭語介面",
|
|
16
|
+
"ru": "切換到俄語介面",
|
|
17
|
+
"uk": "切換到烏克蘭語介面",
|
|
18
|
+
"bs": "切換到波士尼亞語介面",
|
|
19
|
+
"ar": "切換到阿拉伯語介面",
|
|
20
|
+
"no": "切換到挪威語介面",
|
|
21
|
+
"pt-BR": "切換到巴西葡萄牙語介面",
|
|
22
|
+
"th": "切換到泰語介面",
|
|
23
|
+
"tr": "切換到土耳其語介面"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"commands": {
|
|
27
|
+
"Suggested": {
|
|
28
|
+
"Share session": "分享會話",
|
|
29
|
+
"Switch session": "切換會話",
|
|
30
|
+
"New session": "新增會話",
|
|
31
|
+
"Switch model": "切換模型",
|
|
32
|
+
"Connect provider": "連結模型提供者"
|
|
33
|
+
},
|
|
34
|
+
"Session": {
|
|
35
|
+
"Share session": "分享會話",
|
|
36
|
+
"Copy share link": "複製分享連結",
|
|
37
|
+
"Rename session": "重新命名會話",
|
|
38
|
+
"Jump to message": "跳至訊息",
|
|
39
|
+
"Fork session": "分叉會話",
|
|
40
|
+
"Compact session": "壓縮會話上下文",
|
|
41
|
+
"Unshare session": "取消分享會話",
|
|
42
|
+
"Undo previous message": "復原上一則訊息",
|
|
43
|
+
"Redo": "重做",
|
|
44
|
+
"Hide sidebar": "隱藏側邊欄",
|
|
45
|
+
"Show sidebar": "顯示側邊欄",
|
|
46
|
+
"Disable code concealment": "停用程式碼隱藏",
|
|
47
|
+
"Enable code concealment": "啟用程式碼隱藏",
|
|
48
|
+
"Hide timestamps": "隱藏時間戳記",
|
|
49
|
+
"Show timestamps": "顯示時間戳記",
|
|
50
|
+
"Collapse thinking": "折疊思考過程",
|
|
51
|
+
"Expand thinking": "展開思考過程",
|
|
52
|
+
"Hide tool details": "隱藏工具詳細資訊",
|
|
53
|
+
"Show tool details": "顯示工具詳細資訊",
|
|
54
|
+
"Toggle session scrollbar": "切換會話捲動條",
|
|
55
|
+
"Hide generic tool output": "隱藏一般工具輸出",
|
|
56
|
+
"Show generic tool output": "顯示一般工具輸出",
|
|
57
|
+
"Copy last assistant message": "複製最後一則助理回覆",
|
|
58
|
+
"Copy session transcript": "複製會話記錄",
|
|
59
|
+
"Export session transcript": "匯出會話記錄",
|
|
60
|
+
"Switch session": "切換會話",
|
|
61
|
+
"New session": "新增會話",
|
|
62
|
+
"Page up": "向上翻頁",
|
|
63
|
+
"Page down": "向下翻頁",
|
|
64
|
+
"Line up": "向上移動一行",
|
|
65
|
+
"Line down": "向下移動一行",
|
|
66
|
+
"Half page up": "向上捲動半頁",
|
|
67
|
+
"Half page down": "向下捲動半頁",
|
|
68
|
+
"First message": "第一則訊息",
|
|
69
|
+
"Last message": "最後一則訊息",
|
|
70
|
+
"Jump to last user message": "跳至最後一則使用者訊息",
|
|
71
|
+
"Next message": "下一則訊息",
|
|
72
|
+
"Previous message": "上一則訊息",
|
|
73
|
+
"Background subagents": "背景子代理",
|
|
74
|
+
"Go to child session": "進入子會話",
|
|
75
|
+
"Go to parent session": "返回父會話",
|
|
76
|
+
"Next child session": "下一個子會話",
|
|
77
|
+
"Previous child session": "上一個子會話",
|
|
78
|
+
"Switch to session in quick slot 1": "切換到快速槽位 1 的會話",
|
|
79
|
+
"Switch to session in quick slot 2": "切換到快速槽位 2 的會話",
|
|
80
|
+
"Switch to session in quick slot 3": "切換到快速槽位 3 的會話",
|
|
81
|
+
"Switch to session in quick slot 4": "切換到快速槽位 4 的會話",
|
|
82
|
+
"Switch to session in quick slot 5": "切換到快速槽位 5 的會話",
|
|
83
|
+
"Switch to session in quick slot 6": "切換到快速槽位 6 的會話",
|
|
84
|
+
"Switch to session in quick slot 7": "切換到快速槽位 7 的會話",
|
|
85
|
+
"Switch to session in quick slot 8": "切換到快速槽位 8 的會話",
|
|
86
|
+
"Switch to session in quick slot 9": "切換到快速槽位 9 的會話",
|
|
87
|
+
"Open editor": "開啟編輯器",
|
|
88
|
+
"Move session": "移動會話",
|
|
89
|
+
"Interrupt session": "中斷會話",
|
|
90
|
+
"View subagents": "檢視子代理",
|
|
91
|
+
"Manage queued prompts": "管理佇列中的提示詞",
|
|
92
|
+
"Warp": "切換工作區"
|
|
93
|
+
},
|
|
94
|
+
"Agent": {
|
|
95
|
+
"Switch model": "切換模型",
|
|
96
|
+
"Switch agent": "切換 AI 代理",
|
|
97
|
+
"Toggle MCPs": "切換 MCP",
|
|
98
|
+
"Variant cycle": "下一個變體",
|
|
99
|
+
"Switch model variant": "切換模型變體",
|
|
100
|
+
"Model cycle": "下一個模型",
|
|
101
|
+
"Model cycle reverse": "上一個模型",
|
|
102
|
+
"Favorite cycle": "下一個收藏項",
|
|
103
|
+
"Favorite cycle reverse": "上一個收藏項",
|
|
104
|
+
"Agent cycle": "下一個 AI 代理",
|
|
105
|
+
"Agent cycle reverse": "上一個 AI 代理"
|
|
106
|
+
},
|
|
107
|
+
"Provider": {
|
|
108
|
+
"Connect provider": "連結模型提供者",
|
|
109
|
+
"Switch org": "切換組織"
|
|
110
|
+
},
|
|
111
|
+
"System": {
|
|
112
|
+
"Show command palette": "顯示指令面板",
|
|
113
|
+
"View status": "檢視狀態",
|
|
114
|
+
"Switch theme": "切換主題",
|
|
115
|
+
"Switch to light mode": "切換到淺色模式",
|
|
116
|
+
"Switch to dark mode": "切換到深色模式",
|
|
117
|
+
"Lock theme mode": "鎖定主題模式",
|
|
118
|
+
"Unlock theme mode": "解鎖主題模式",
|
|
119
|
+
"Help": "說明",
|
|
120
|
+
"Open docs": "開啟說明文件",
|
|
121
|
+
"Exit the app": "結束應用程式",
|
|
122
|
+
"Toggle debug panel": "切換除錯面板",
|
|
123
|
+
"Toggle console": "切換主控台",
|
|
124
|
+
"Write heap snapshot": "寫入堆積快照",
|
|
125
|
+
"Suspend terminal": "暫停終端機",
|
|
126
|
+
"Disable terminal title": "停用終端機標題",
|
|
127
|
+
"Enable terminal title": "啟用終端機標題",
|
|
128
|
+
"Disable animations": "停用動畫",
|
|
129
|
+
"Enable animations": "啟用動畫",
|
|
130
|
+
"Disable file context": "停用檔案上下文",
|
|
131
|
+
"Enable file context": "啟用檔案上下文",
|
|
132
|
+
"Disable diff wrapping": "停用差異換行",
|
|
133
|
+
"Enable diff wrapping": "啟用差異換行",
|
|
134
|
+
"Disable paste summary": "停用貼上摘要",
|
|
135
|
+
"Enable paste summary": "啟用貼上摘要",
|
|
136
|
+
"Disable session directory filtering": "停用會話目錄篩選",
|
|
137
|
+
"Enable session directory filtering": "啟用會話目錄篩選",
|
|
138
|
+
"Disable auto-approve permissions": "停用自動核准權限",
|
|
139
|
+
"Enable auto-approve permissions": "啟用自動核准權限",
|
|
140
|
+
"Open command palette": "開啟指令面板",
|
|
141
|
+
"Plugins": "外掛",
|
|
142
|
+
"Install plugin": "安裝外掛",
|
|
143
|
+
"Show tips": "顯示提示",
|
|
144
|
+
"Hide tips": "隱藏提示",
|
|
145
|
+
"Show key bindings": "顯示快捷鍵",
|
|
146
|
+
"First key binding": "第一個快捷鍵",
|
|
147
|
+
"Last key binding": "最後一個快捷鍵",
|
|
148
|
+
"Previous key binding group": "上一組快捷鍵",
|
|
149
|
+
"Next key binding group": "下一組快捷鍵",
|
|
150
|
+
"Page key bindings up": "快捷鍵列表向上翻頁",
|
|
151
|
+
"Page key bindings down": "快捷鍵列表向下翻頁",
|
|
152
|
+
"Scroll key bindings up": "快捷鍵列表向上捲動",
|
|
153
|
+
"Scroll key bindings down": "快捷鍵列表向下捲動",
|
|
154
|
+
"Toggle key bindings layout": "切換快捷鍵面板版面",
|
|
155
|
+
"Toggle pending key preview": "切換待輸入按鍵預覽"
|
|
156
|
+
},
|
|
157
|
+
"Workspace": {
|
|
158
|
+
"Copy worktree path": "複製工作樹路徑",
|
|
159
|
+
"Manage workspaces": "管理工作區"
|
|
160
|
+
},
|
|
161
|
+
"Autocomplete": {
|
|
162
|
+
"Previous autocomplete item": "上一個補全項",
|
|
163
|
+
"Next autocomplete item": "下一個補全項",
|
|
164
|
+
"Hide autocomplete": "隱藏補全選單",
|
|
165
|
+
"Select autocomplete item": "選取目前補全項",
|
|
166
|
+
"Complete autocomplete item": "補全目前項目"
|
|
167
|
+
},
|
|
168
|
+
"Prompt": {
|
|
169
|
+
"Skills": "技能",
|
|
170
|
+
"Clear prompt": "清空提示詞",
|
|
171
|
+
"Clear prompt or exit": "清空提示詞或結束",
|
|
172
|
+
"Submit prompt": "送出提示詞",
|
|
173
|
+
"Remove editor context": "移除編輯器上下文",
|
|
174
|
+
"Paste": "貼上",
|
|
175
|
+
"Stash prompt": "暫存提示詞",
|
|
176
|
+
"Stash pop": "還原暫存的提示詞",
|
|
177
|
+
"Stash list": "暫存提示詞列表",
|
|
178
|
+
"Previous prompt history": "上一則提示詞歷史",
|
|
179
|
+
"Next prompt history": "下一則提示詞歷史"
|
|
180
|
+
},
|
|
181
|
+
"VCS": {
|
|
182
|
+
"Open diff viewer": "開啟差異視圖",
|
|
183
|
+
"Close diff viewer": "關閉差異視圖",
|
|
184
|
+
"Move diff viewer up": "向上移動差異視圖",
|
|
185
|
+
"Move diff viewer down": "向下移動差異視圖",
|
|
186
|
+
"Page diff viewer up": "向上翻頁差異視圖",
|
|
187
|
+
"Page diff viewer down": "向下翻頁差異視圖",
|
|
188
|
+
"Toggle diff viewer item": "切換差異項目",
|
|
189
|
+
"Expand diff viewer item": "展開差異項目",
|
|
190
|
+
"Expand all diff viewer folders": "展開所有差異視圖資料夾",
|
|
191
|
+
"Collapse diff viewer item": "折疊差異項目",
|
|
192
|
+
"Jump to next diff hunk": "跳至下一個差異區塊",
|
|
193
|
+
"Jump to previous diff hunk": "跳至上一個差異區塊",
|
|
194
|
+
"Jump to next diff file": "跳至下一個差異檔案",
|
|
195
|
+
"Jump to previous diff file": "跳至上一個差異檔案",
|
|
196
|
+
"Toggle selected diff file reviewed": "切換所選差異檔案已審閱狀態",
|
|
197
|
+
"Switch diff viewer focus": "切換差異視圖焦點",
|
|
198
|
+
"Toggle diff viewer file tree": "切換差異視圖檔案樹",
|
|
199
|
+
"Toggle single patch view": "切換單一 Patch 視圖",
|
|
200
|
+
"Switch diff viewer source": "切換差異來源",
|
|
201
|
+
"Toggle diff viewer split or unified view": "切換分欄/統一視圖",
|
|
202
|
+
"Show more diff viewer shortcuts": "顯示更多差異視圖快捷鍵"
|
|
203
|
+
},
|
|
204
|
+
"Dialog": {
|
|
205
|
+
"Previous item": "上一項",
|
|
206
|
+
"Next item": "下一項",
|
|
207
|
+
"First item": "第一項",
|
|
208
|
+
"Last item": "最後一項",
|
|
209
|
+
"Select item": "選取項目",
|
|
210
|
+
"Submit dialog prompt": "送出對話框輸入"
|
|
211
|
+
},
|
|
212
|
+
"Permission": {
|
|
213
|
+
"Cancel permission rejection": "取消拒絕權限",
|
|
214
|
+
"Reject permission": "拒絕權限",
|
|
215
|
+
"Toggle permission fullscreen": "切換權限全螢幕"
|
|
216
|
+
},
|
|
217
|
+
"Question": {
|
|
218
|
+
"Clear answer edit": "清空答案編輯內容",
|
|
219
|
+
"Reject question": "拒絕回答"
|
|
220
|
+
},
|
|
221
|
+
"Model": {
|
|
222
|
+
"Cycle model variant": "下一個模型變體"
|
|
223
|
+
},
|
|
224
|
+
"Providers": {
|
|
225
|
+
"Other": "其他"
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
"slash_commands": {
|
|
229
|
+
"/connect": "連結模型提供者",
|
|
230
|
+
"/compact": "壓縮會話上下文(別名: /summarize)",
|
|
231
|
+
"/details": "切換工具詳細資訊顯示",
|
|
232
|
+
"/diff": "開啟差異視圖",
|
|
233
|
+
"/editor": "開啟外部編輯器",
|
|
234
|
+
"/exit": "結束應用程式(別名: /quit, /q)",
|
|
235
|
+
"/export": "匯出會話記錄",
|
|
236
|
+
"/help": "顯示說明",
|
|
237
|
+
"/init": "建立或更新 AGENTS.md",
|
|
238
|
+
"/models": "列出可用模型(別名: /mo)",
|
|
239
|
+
"/new": "新增會話(別名: /clear)",
|
|
240
|
+
"/redo": "重做",
|
|
241
|
+
"/sessions": "切換會話(別名: /resume, /continue)",
|
|
242
|
+
"/share": "分享會話",
|
|
243
|
+
"/themes": "切換主題",
|
|
244
|
+
"/thinking": "切換思考過程可見性(別名: /toggle-thinking)",
|
|
245
|
+
"/undo": "復原上一則訊息",
|
|
246
|
+
"/unshare": "取消分享",
|
|
247
|
+
"/rename": "重新命名會話",
|
|
248
|
+
"/timeline": "跳至訊息",
|
|
249
|
+
"/fork": "分叉會話",
|
|
250
|
+
"/copy": "複製會話記錄",
|
|
251
|
+
"/agents": "切換 AI 代理",
|
|
252
|
+
"/mcps": "切換 MCP",
|
|
253
|
+
"/skills": "選擇技能",
|
|
254
|
+
"/variants": "切換模型變體",
|
|
255
|
+
"/status": "檢視狀態",
|
|
256
|
+
"/org": "切換組織(別名: /orgs, /switch-org)",
|
|
257
|
+
"/warp": "切換目前會話使用的工作區",
|
|
258
|
+
"/move": "將會話移動到另一個專案目錄",
|
|
259
|
+
"/workspaces": "管理工作區"
|
|
260
|
+
},
|
|
261
|
+
"descriptions": {
|
|
262
|
+
"Move session": "移動到另一個專案目錄",
|
|
263
|
+
"Warp": "切換目前會話使用的工作區",
|
|
264
|
+
"Previous key binding group": "顯示上一組快捷鍵提示",
|
|
265
|
+
"Next key binding group": "顯示下一組快捷鍵提示",
|
|
266
|
+
"Scroll key bindings up": "向上捲動快捷鍵提示面板",
|
|
267
|
+
"Scroll key bindings down": "向下捲動快捷鍵提示面板",
|
|
268
|
+
"Page key bindings up": "向上翻頁快捷鍵提示面板",
|
|
269
|
+
"Page key bindings down": "向下翻頁快捷鍵提示面板",
|
|
270
|
+
"First key binding": "跳至第一個快捷鍵",
|
|
271
|
+
"Last key binding": "跳至最後一個快捷鍵",
|
|
272
|
+
"Show tips": "切換提示層",
|
|
273
|
+
"Show key bindings": "切換快捷鍵提示層",
|
|
274
|
+
"Toggle key bindings layout": "在停駐和覆蓋模式之間切換快捷鍵提示",
|
|
275
|
+
"Toggle pending key preview": "在覆蓋模式下自動顯示待輸入按鍵序列的快捷鍵提示"
|
|
276
|
+
}
|
|
277
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-i18n",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode TUI i18n plugin — localize the interface into 简体中文 / 繁體中文 and more, with one-command install.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./tui": "./plugins/i18n/index.ts",
|
|
9
|
+
"./server": "./plugins/i18n/server.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"plugins",
|
|
13
|
+
"i18n",
|
|
14
|
+
"tools",
|
|
15
|
+
"commands",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"opencode": ">=1.18.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@opencode-ai/plugin": ">=1.14.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^26.0.1",
|
|
27
|
+
"typescript": "^6.0.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import { readConfigSync, readStateSync, resolveLocale, type I18nLocaleConfig } from "../../i18n/lib.ts"
|
|
3
|
+
|
|
4
|
+
type TranslationSnapshot = {
|
|
5
|
+
enabled: boolean
|
|
6
|
+
translations: Map<string, string>
|
|
7
|
+
descriptions: Map<string, string>
|
|
8
|
+
slashDescriptions: Map<string, string>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type KeymapCommand = {
|
|
12
|
+
name: string
|
|
13
|
+
title?: unknown
|
|
14
|
+
desc?: unknown
|
|
15
|
+
[key: string]: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type CommandEntry = {
|
|
19
|
+
command: KeymapCommand
|
|
20
|
+
[key: string]: unknown
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type CommandQuery = {
|
|
24
|
+
search?: unknown
|
|
25
|
+
searchIn?: unknown
|
|
26
|
+
limit?: unknown
|
|
27
|
+
filter?: unknown
|
|
28
|
+
[key: string]: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const KEYMAP_PATCHED = "__opencodeI18nPatched"
|
|
32
|
+
|
|
33
|
+
type PatchableKeymap = {
|
|
34
|
+
getCommands(query?: CommandQuery): readonly KeymapCommand[]
|
|
35
|
+
getCommandEntries(query?: CommandQuery): readonly CommandEntry[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type PatchedKeymap = PatchableKeymap & {
|
|
39
|
+
[KEYMAP_PATCHED]?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readEnabled() {
|
|
43
|
+
return readStateSync().enabled
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readLocaleConfig() {
|
|
47
|
+
const config = readConfigSync()
|
|
48
|
+
const state = readStateSync()
|
|
49
|
+
const locale = resolveLocale(config, state)
|
|
50
|
+
return locale ? config?.locales?.[locale] : undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readTranslations(localeConfig: I18nLocaleConfig | undefined) {
|
|
54
|
+
const translations = new Map<string, string>()
|
|
55
|
+
|
|
56
|
+
for (const [category, commands] of Object.entries(localeConfig?.commands ?? {})) {
|
|
57
|
+
for (const [english, chinese] of Object.entries(commands)) {
|
|
58
|
+
if (!english || english.startsWith("_")) continue
|
|
59
|
+
if (!chinese.trim()) continue
|
|
60
|
+
|
|
61
|
+
if (translations.has(english) && category !== "Suggested") continue
|
|
62
|
+
|
|
63
|
+
translations.set(english, chinese.trim())
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return translations
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readStringMap(values: Record<string, string> | undefined) {
|
|
71
|
+
const result = new Map<string, string>()
|
|
72
|
+
|
|
73
|
+
for (const [key, value] of Object.entries(values ?? {})) {
|
|
74
|
+
if (!key || key.startsWith("_")) continue
|
|
75
|
+
if (!value.trim()) continue
|
|
76
|
+
|
|
77
|
+
result.set(key, value.trim())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeSlashName(name: string) {
|
|
84
|
+
const trimmed = name.trim()
|
|
85
|
+
if (!trimmed) return ""
|
|
86
|
+
|
|
87
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readSlashDescriptions(localeConfig: I18nLocaleConfig | undefined) {
|
|
91
|
+
const descriptions = new Map<string, string>()
|
|
92
|
+
|
|
93
|
+
for (const [slash, description] of Object.entries(localeConfig?.slash_commands ?? {})) {
|
|
94
|
+
if (!slash || slash.startsWith("_")) continue
|
|
95
|
+
if (!description.trim()) continue
|
|
96
|
+
|
|
97
|
+
const normalized = normalizeSlashName(slash)
|
|
98
|
+
if (normalized) descriptions.set(normalized, description.trim())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return descriptions
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readSnapshot(): TranslationSnapshot {
|
|
105
|
+
if (!readEnabled()) {
|
|
106
|
+
return {
|
|
107
|
+
enabled: false,
|
|
108
|
+
translations: new Map(),
|
|
109
|
+
descriptions: new Map(),
|
|
110
|
+
slashDescriptions: new Map(),
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const localeConfig = readLocaleConfig()
|
|
115
|
+
const translations = readTranslations(localeConfig)
|
|
116
|
+
const descriptions = readStringMap(localeConfig?.descriptions)
|
|
117
|
+
const slashDescriptions = readSlashDescriptions(localeConfig)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
enabled: true,
|
|
121
|
+
translations,
|
|
122
|
+
descriptions,
|
|
123
|
+
slashDescriptions,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function commandSlashNames(command: KeymapCommand) {
|
|
128
|
+
const names: string[] = []
|
|
129
|
+
|
|
130
|
+
if (typeof command.slashName === "string") names.push(command.slashName)
|
|
131
|
+
|
|
132
|
+
if (Array.isArray(command.slashAliases)) {
|
|
133
|
+
for (const alias of command.slashAliases) {
|
|
134
|
+
if (typeof alias === "string") names.push(alias)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const slash = command.slash
|
|
139
|
+
if (slash && typeof slash === "object") {
|
|
140
|
+
const legacy = slash as { name?: unknown; aliases?: unknown }
|
|
141
|
+
if (typeof legacy.name === "string") names.push(legacy.name)
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(legacy.aliases)) {
|
|
144
|
+
for (const alias of legacy.aliases) {
|
|
145
|
+
if (typeof alias === "string") names.push(alias)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return names.map(normalizeSlashName).filter(Boolean)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function slashDescription(command: KeymapCommand, snapshot: TranslationSnapshot) {
|
|
154
|
+
for (const name of commandSlashNames(command)) {
|
|
155
|
+
const description = snapshot.slashDescriptions.get(name)
|
|
156
|
+
if (description) return description
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function titleDescription(command: KeymapCommand, snapshot: TranslationSnapshot) {
|
|
163
|
+
const english = typeof command.title === "string" ? command.title : command.name
|
|
164
|
+
return snapshot.descriptions.get(english)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function translateCommand(command: KeymapCommand, snapshot: TranslationSnapshot) {
|
|
168
|
+
if (!snapshot.enabled) return command
|
|
169
|
+
|
|
170
|
+
const english = typeof command.title === "string" ? command.title : command.name
|
|
171
|
+
const entry = snapshot.translations.get(english)
|
|
172
|
+
const originalDescription = typeof command.desc === "string" && command.desc.trim() ? command.desc : undefined
|
|
173
|
+
const description = originalDescription ? titleDescription(command, snapshot) ?? slashDescription(command, snapshot) : undefined
|
|
174
|
+
if (!entry && !description) return command
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...command,
|
|
178
|
+
...(entry
|
|
179
|
+
? {
|
|
180
|
+
title: entry,
|
|
181
|
+
i18nOriginalTitle: english,
|
|
182
|
+
}
|
|
183
|
+
: {}),
|
|
184
|
+
...(description
|
|
185
|
+
? {
|
|
186
|
+
desc: description,
|
|
187
|
+
i18nOriginalDesc: originalDescription,
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function translateEntries(entries: readonly CommandEntry[], snapshot: TranslationSnapshot) {
|
|
194
|
+
if (!snapshot.enabled) return entries
|
|
195
|
+
|
|
196
|
+
return entries.map((entry) => ({
|
|
197
|
+
...entry,
|
|
198
|
+
command: translateCommand(entry.command, snapshot),
|
|
199
|
+
}))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function patchKeymap(api: TuiPluginApi) {
|
|
203
|
+
const keymap = api.keymap as unknown as PatchedKeymap
|
|
204
|
+
if (keymap[KEYMAP_PATCHED]) return
|
|
205
|
+
|
|
206
|
+
const getCommands = keymap.getCommands
|
|
207
|
+
const getCommandEntries = keymap.getCommandEntries
|
|
208
|
+
|
|
209
|
+
keymap.getCommands = (query?: CommandQuery) => {
|
|
210
|
+
const snapshot = readSnapshot()
|
|
211
|
+
if (!snapshot.enabled) return getCommands(query)
|
|
212
|
+
|
|
213
|
+
return getCommands(query).map((command) => translateCommand(command, snapshot))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
keymap.getCommandEntries = (query?: CommandQuery) => {
|
|
217
|
+
const snapshot = readSnapshot()
|
|
218
|
+
if (!snapshot.enabled) return getCommandEntries(query)
|
|
219
|
+
|
|
220
|
+
return translateEntries(getCommandEntries(query), snapshot)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
keymap[KEYMAP_PATCHED] = true
|
|
224
|
+
|
|
225
|
+
api.lifecycle.onDispose(() => {
|
|
226
|
+
keymap.getCommands = getCommands
|
|
227
|
+
keymap.getCommandEntries = getCommandEntries
|
|
228
|
+
keymap[KEYMAP_PATCHED] = false
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tui: TuiPlugin = async (api) => {
|
|
233
|
+
patchKeymap(api)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const plugin = {
|
|
237
|
+
id: "opencode-i18n",
|
|
238
|
+
tui,
|
|
239
|
+
} satisfies TuiPluginModule
|
|
240
|
+
|
|
241
|
+
export default plugin
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
+
import i18nStateTool from "../../tools/i18n-state.ts"
|
|
3
|
+
|
|
4
|
+
const server: Plugin = async () => {
|
|
5
|
+
return {
|
|
6
|
+
tool: {
|
|
7
|
+
"i18n-state": i18nStateTool,
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const plugin: PluginModule & { id: string } = {
|
|
13
|
+
id: "opencode-i18n",
|
|
14
|
+
server,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default plugin
|