opencode-i18n 0.1.0 → 0.1.2
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/README.md +19 -10
- package/i18n/lib.ts +15 -1
- package/package.json +1 -1
- package/plugins/i18n/index.ts +66 -1
- package/plugins/i18n/server.ts +30 -0
- package/tools/i18n-state.ts +2 -17
package/README.md
CHANGED
|
@@ -10,20 +10,29 @@ OpenCode TUI 界面本地化插件。
|
|
|
10
10
|
|
|
11
11
|
安装后运行 `/i18n`,用选项切换语言。选择中文会自动开启本地化;选择 `English` 会回到原始英文。
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 一键安装(命令面板)
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
在 OpenCode 中:
|
|
16
|
+
|
|
17
|
+
1. 按 **`Ctrl + P`** 打开命令面板
|
|
18
|
+
2. 搜索 **`install plugin`**
|
|
19
|
+
3. 输入 **`opencode-i18n`**,回车
|
|
18
20
|
|
|
19
21
|
安装后重启 OpenCode,运行 `/i18n` 选择 English、简体中文或繁體中文。
|
|
20
22
|
|
|
21
23
|
说明:
|
|
22
24
|
|
|
23
|
-
-
|
|
25
|
+
- 插件已发布到 npm,安装方式和 `opencode-visual-cache` 等插件完全一致。
|
|
26
|
+
- 安装会自动写入 `opencode.json`(server 端)和 `tui.json`(TUI 端)的 `plugin` 列表。
|
|
24
27
|
- 插件自带默认语言包(`i18n/locales/*.json`),开箱即用。
|
|
25
28
|
- 如需自定义语言包,把文件放到 `~/.config/opencode/i18n/locales/` 即可覆盖默认值。
|
|
26
29
|
|
|
30
|
+
也可以用命令行安装:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
opencode plugin opencode-i18n
|
|
34
|
+
```
|
|
35
|
+
|
|
27
36
|
## 一句提示词安装
|
|
28
37
|
|
|
29
38
|
把下面这一句发给 OpenCode:
|
|
@@ -82,7 +91,7 @@ cd opencode-i18n
|
|
|
82
91
|
/i18n
|
|
83
92
|
```
|
|
84
93
|
|
|
85
|
-
|
|
94
|
+
打开语言选择对话框(由 TUI 插件注册,npm 安装后直接可用)。选择语言即切换,也可在对话框里开关本地化。
|
|
86
95
|
|
|
87
96
|
```text
|
|
88
97
|
/i18n status
|
|
@@ -91,16 +100,16 @@ cd opencode-i18n
|
|
|
91
100
|
/i18n toggle
|
|
92
101
|
```
|
|
93
102
|
|
|
94
|
-
|
|
103
|
+
管理开关状态(这些带参数形式走服务端命令;npm 安装时 server 插件会自动把 `commands/i18n.md` 写入 `~/.config/opencode/commands/`,无需手动操作)。
|
|
95
104
|
|
|
96
105
|
如果界面没有立即刷新,请重启 OpenCode。
|
|
97
106
|
|
|
98
107
|
## 文件说明
|
|
99
108
|
|
|
100
|
-
- `plugins/i18n/index.ts`:TUI
|
|
101
|
-
- `plugins/i18n/server.ts`:server 插件,向 OpenCode 注册 `i18n-state` 工具(npm 安装模式下工具不会自动从 `tools/`
|
|
109
|
+
- `plugins/i18n/index.ts`:TUI 插件,读取语言包和状态,改写界面标题/已有描述;并注册 `/i18n` 命令(语言选择对话框)。
|
|
110
|
+
- `plugins/i18n/server.ts`:server 插件,向 OpenCode 注册 `i18n-state` 工具(npm 安装模式下工具不会自动从 `tools/` 加载),并在首次启动时自动安装 `commands/i18n.md`(已存在则不覆盖)。
|
|
102
111
|
- `tools/i18n-state.ts`:状态工具,负责开关和语言选择。
|
|
103
112
|
- `i18n/lib.ts`:共享路径、状态读取和语言解析逻辑。配置优先读用户目录 `~/.config/opencode/i18n/`,缺失时回退到包内默认数据。
|
|
104
|
-
- `commands/i18n.md`:OpenCode 自定义 command(复制式安装时随脚本复制;npm
|
|
113
|
+
- `commands/i18n.md`:OpenCode 自定义 command(复制式安装时随脚本复制;npm 安装时由 server 插件自动安装)。
|
|
105
114
|
- `i18n/config.json`:默认语言和内置语言排序。
|
|
106
115
|
- `i18n/locales/*.json`:独立语言包。新增语言时只要添加一个 locale JSON 即可被自动识别;如果想调整显示顺序,可把语言代码加入 `config.json` 的 `locales` 列表。
|
package/i18n/lib.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdirSync, readFileSync } from "node:fs"
|
|
2
|
-
import { readdir, readFile } from "node:fs/promises"
|
|
2
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"
|
|
3
3
|
import os from "node:os"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { fileURLToPath } from "node:url"
|
|
@@ -236,6 +236,20 @@ export async function readState(): Promise<I18nState> {
|
|
|
236
236
|
return normalizeState(await readJsonFile<unknown>(STATE_PATH))
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
export async function writeState(patch: Partial<Pick<I18nState, "enabled" | "locale">>): Promise<I18nState> {
|
|
240
|
+
const current = await readState()
|
|
241
|
+
const state: I18nState = {
|
|
242
|
+
version: 1,
|
|
243
|
+
enabled: patch.enabled ?? current.enabled,
|
|
244
|
+
locale: patch.locale ?? current.locale,
|
|
245
|
+
updatedAt: new Date().toISOString(),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await mkdir(STATE_ROOT, { recursive: true })
|
|
249
|
+
await writeFile(STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
250
|
+
return state
|
|
251
|
+
}
|
|
252
|
+
|
|
239
253
|
export function readConfigSync(): I18nConfig | undefined {
|
|
240
254
|
const index = normalizeIndexConfig(readJsonFileSync<unknown>(CONFIG_PATH) ?? readJsonFileSync<unknown>(PACKAGE_CONFIG_PATH))
|
|
241
255
|
return buildConfig(index, discoverLocalesSync(), readLocaleFileSync)
|
package/package.json
CHANGED
package/plugins/i18n/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
localeInfo,
|
|
4
|
+
readConfigSync,
|
|
5
|
+
readStateSync,
|
|
6
|
+
resolveLocale,
|
|
7
|
+
writeState,
|
|
8
|
+
type I18nLocaleConfig,
|
|
9
|
+
} from "../../i18n/lib.ts"
|
|
3
10
|
|
|
4
11
|
type TranslationSnapshot = {
|
|
5
12
|
enabled: boolean
|
|
@@ -229,8 +236,66 @@ function patchKeymap(api: TuiPluginApi) {
|
|
|
229
236
|
})
|
|
230
237
|
}
|
|
231
238
|
|
|
239
|
+
function registerI18nCommand(api: TuiPluginApi) {
|
|
240
|
+
api.keymap.registerLayer({
|
|
241
|
+
commands: [
|
|
242
|
+
{
|
|
243
|
+
name: "i18n.open",
|
|
244
|
+
title: "Open i18n language picker",
|
|
245
|
+
desc: "Choose the OpenCode interface language",
|
|
246
|
+
category: "System",
|
|
247
|
+
namespace: "palette",
|
|
248
|
+
slashName: "i18n",
|
|
249
|
+
run: () => openLanguagePicker(api),
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function openLanguagePicker(api: TuiPluginApi) {
|
|
256
|
+
const config = readConfigSync()
|
|
257
|
+
const state = readStateSync()
|
|
258
|
+
const info = localeInfo(config, state)
|
|
259
|
+
const active = info.activeLocale
|
|
260
|
+
const enabled = state.enabled
|
|
261
|
+
|
|
262
|
+
const toggleLabel = enabled ? "Disable localization" : "Enable localization"
|
|
263
|
+
const options = [
|
|
264
|
+
{
|
|
265
|
+
title: `${toggleLabel} ${enabled ? "ON" : "OFF"}`,
|
|
266
|
+
value: "__toggle__",
|
|
267
|
+
description: enabled ? "Turn off localized titles" : "Turn on localized titles",
|
|
268
|
+
},
|
|
269
|
+
...info.available.map((locale) => ({
|
|
270
|
+
title: `${info.labels.get(locale) ?? locale}${locale === active ? " ✓" : ""}`,
|
|
271
|
+
value: locale,
|
|
272
|
+
description: `Switch to ${info.labels.get(locale) ?? locale}`,
|
|
273
|
+
})),
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
api.ui.dialog.setSize("medium")
|
|
277
|
+
api.ui.dialog.replace(() =>
|
|
278
|
+
api.ui.DialogSelect({
|
|
279
|
+
title: "OpenCode i18n",
|
|
280
|
+
placeholder: "Search languages...",
|
|
281
|
+
options,
|
|
282
|
+
onSelect(opt: { value: string }) {
|
|
283
|
+
api.ui.dialog.clear()
|
|
284
|
+
if (opt.value === "__toggle__") {
|
|
285
|
+
void writeState({ enabled: !enabled })
|
|
286
|
+
api.ui.toast({ message: enabled ? "Localization disabled" : "Localization enabled" })
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
void writeState({ locale: opt.value, enabled: opt.value !== "en" })
|
|
290
|
+
api.ui.toast({ message: `Switched to ${info.labels.get(opt.value) ?? opt.value}` })
|
|
291
|
+
},
|
|
292
|
+
}),
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
232
296
|
const tui: TuiPlugin = async (api) => {
|
|
233
297
|
patchKeymap(api)
|
|
298
|
+
registerI18nCommand(api)
|
|
234
299
|
}
|
|
235
300
|
|
|
236
301
|
const plugin = {
|
package/plugins/i18n/server.ts
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
import type { Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import { CONFIG_ROOT } from "../../i18n/lib.ts"
|
|
2
6
|
import i18nStateTool from "../../tools/i18n-state.ts"
|
|
3
7
|
|
|
8
|
+
const MODULE_ROOT = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const COMMAND_SOURCE = path.resolve(MODULE_ROOT, "../../commands/i18n.md")
|
|
10
|
+
const COMMAND_TARGET = path.join(CONFIG_ROOT, "commands", "i18n.md")
|
|
11
|
+
|
|
12
|
+
async function exists(file: string) {
|
|
13
|
+
try {
|
|
14
|
+
await access(file)
|
|
15
|
+
return true
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function installCommandFile() {
|
|
22
|
+
try {
|
|
23
|
+
if (await exists(COMMAND_TARGET)) return
|
|
24
|
+
const content = await readFile(COMMAND_SOURCE, "utf8")
|
|
25
|
+
await mkdir(path.dirname(COMMAND_TARGET), { recursive: true })
|
|
26
|
+
await writeFile(COMMAND_TARGET, content, "utf8")
|
|
27
|
+
} catch {
|
|
28
|
+
// Read failure (file missing in copy-based installs) or write failure are
|
|
29
|
+
// non-fatal: the command file is optional.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
4
33
|
const server: Plugin = async () => {
|
|
34
|
+
await installCommandFile()
|
|
5
35
|
return {
|
|
6
36
|
tool: {
|
|
7
37
|
"i18n-state": i18nStateTool,
|
package/tools/i18n-state.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises"
|
|
3
2
|
import {
|
|
4
3
|
CONFIG_PATH,
|
|
5
4
|
STATE_PATH,
|
|
6
|
-
STATE_ROOT,
|
|
7
5
|
localeInfo,
|
|
8
6
|
readConfig,
|
|
9
7
|
readState,
|
|
10
8
|
resolveLocaleInput,
|
|
9
|
+
writeState,
|
|
11
10
|
type I18nState,
|
|
12
11
|
type LocaleCode,
|
|
13
12
|
type LocaleInfo,
|
|
@@ -145,21 +144,7 @@ function localeConfig(info: LocaleInfo, locale: LocaleCode | undefined) {
|
|
|
145
144
|
return locale ? info.config?.locales[locale] : undefined
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
|
|
149
|
-
const current = await readState()
|
|
150
|
-
const state: I18nState = {
|
|
151
|
-
version: 1,
|
|
152
|
-
enabled: patch.enabled ?? current.enabled,
|
|
153
|
-
locale: patch.locale ?? current.locale,
|
|
154
|
-
updatedAt: new Date().toISOString(),
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
await mkdir(STATE_ROOT, { recursive: true })
|
|
158
|
-
await writeFile(STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
159
|
-
return state
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function formatLocale(locale: string | undefined, info: LocaleInfo, text: Messages) {
|
|
147
|
+
function formatLocale(locale: string | undefined, info: LocaleInfo, text: Messages) {
|
|
163
148
|
if (!locale) return text.unset
|
|
164
149
|
const label = info.labels.get(locale)
|
|
165
150
|
return label && label !== locale ? `${locale} (${label})` : locale
|