opencode-i18n 0.1.0 → 0.1.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/README.md CHANGED
@@ -82,7 +82,7 @@ cd opencode-i18n
82
82
  /i18n
83
83
  ```
84
84
 
85
- 打开语言选择。
85
+ 打开语言选择对话框(由 TUI 插件注册,npm 安装后直接可用)。选择语言即切换,也可在对话框里开关本地化。
86
86
 
87
87
  ```text
88
88
  /i18n status
@@ -91,16 +91,16 @@ cd opencode-i18n
91
91
  /i18n toggle
92
92
  ```
93
93
 
94
- 管理开关状态。
94
+ 管理开关状态(这些带参数形式走服务端命令;npm 安装时 server 插件会自动把 `commands/i18n.md` 写入 `~/.config/opencode/commands/`,无需手动操作)。
95
95
 
96
96
  如果界面没有立即刷新,请重启 OpenCode。
97
97
 
98
98
  ## 文件说明
99
99
 
100
- - `plugins/i18n/index.ts`:TUI 插件,读取语言包和状态,改写界面标题/已有描述。
101
- - `plugins/i18n/server.ts`:server 插件,向 OpenCode 注册 `i18n-state` 工具(npm 安装模式下工具不会自动从 `tools/` 加载)。
100
+ - `plugins/i18n/index.ts`:TUI 插件,读取语言包和状态,改写界面标题/已有描述;并注册 `/i18n` 命令(语言选择对话框)。
101
+ - `plugins/i18n/server.ts`:server 插件,向 OpenCode 注册 `i18n-state` 工具(npm 安装模式下工具不会自动从 `tools/` 加载),并在首次启动时自动安装 `commands/i18n.md`(已存在则不覆盖)。
102
102
  - `tools/i18n-state.ts`:状态工具,负责开关和语言选择。
103
103
  - `i18n/lib.ts`:共享路径、状态读取和语言解析逻辑。配置优先读用户目录 `~/.config/opencode/i18n/`,缺失时回退到包内默认数据。
104
- - `commands/i18n.md`:OpenCode 自定义 command(复制式安装时随脚本复制;npm 安装时请手动放到 `~/.config/opencode/commands/` 或直接调用 `i18n-state` 工具)。
104
+ - `commands/i18n.md`:OpenCode 自定义 command(复制式安装时随脚本复制;npm 安装时由 server 插件自动安装)。
105
105
  - `i18n/config.json`:默认语言和内置语言排序。
106
106
  - `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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-i18n",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "OpenCode TUI i18n plugin — localize the interface into 简体中文 / 繁體中文 and more, with one-command install.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,5 +1,12 @@
1
1
  import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
2
- import { readConfigSync, readStateSync, resolveLocale, type I18nLocaleConfig } from "../../i18n/lib.ts"
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 = {
@@ -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,
@@ -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
- async function writeState(patch: Partial<Pick<I18nState, "enabled" | "locale">>): Promise<I18nState> {
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