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.
@@ -0,0 +1,293 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import { mkdir, writeFile } from "node:fs/promises"
3
+ import {
4
+ CONFIG_PATH,
5
+ STATE_PATH,
6
+ STATE_ROOT,
7
+ localeInfo,
8
+ readConfig,
9
+ readState,
10
+ resolveLocaleInput,
11
+ type I18nState,
12
+ type LocaleCode,
13
+ type LocaleInfo,
14
+ } from "../i18n/lib.ts"
15
+
16
+ const ACTIONS = ["status", "set", "toggle", "locale", "locales"] as const
17
+
18
+ type Action = (typeof ACTIONS)[number]
19
+
20
+ type Messages = {
21
+ unset: string
22
+ noLocalesInline: string
23
+ noLocales: string
24
+ currentLanguage: string
25
+ localization: string
26
+ enabled: string
27
+ disabled: string
28
+ availableLanguages: string
29
+ current: string
30
+ default: string
31
+ statusTitle: string
32
+ lastUpdated: string
33
+ languagePack: string
34
+ stateFile: string
35
+ availableCommands: string
36
+ question: string
37
+ commandOn: string
38
+ commandOff: string
39
+ commandToggle: string
40
+ commandChoose: string
41
+ refreshHint: string
42
+ written: string
43
+ setNeedsEnabled: string
44
+ localeNeedsSelection: string
45
+ unknownLanguage: string
46
+ unknownAction: string
47
+ }
48
+
49
+ const MESSAGES: Record<string, Messages> = {
50
+ en: {
51
+ unset: "not set",
52
+ noLocalesInline: "Available languages: no language packs found",
53
+ noLocales: "No language packs found.",
54
+ currentLanguage: "Current language",
55
+ localization: "Localization",
56
+ enabled: "enabled",
57
+ disabled: "disabled",
58
+ availableLanguages: "Available languages",
59
+ current: "current",
60
+ default: "default",
61
+ statusTitle: "OpenCode interface localization",
62
+ lastUpdated: "Last changed",
63
+ languagePack: "Language pack",
64
+ stateFile: "State file",
65
+ availableCommands: "Available commands",
66
+ question: "Choose OpenCode interface language",
67
+ commandOn: "/i18n on - enable localized titles",
68
+ commandOff: "/i18n off - disable localized titles",
69
+ commandToggle: "/i18n toggle - toggle localization",
70
+ commandChoose: "/i18n - choose language",
71
+ refreshHint: "Tip: restart OpenCode if the interface does not refresh immediately.",
72
+ written: "Written",
73
+ setNeedsEnabled: "Provide enabled: true or enabled: false when setting the switch.",
74
+ localeNeedsSelection: "Provide the language selected from question when switching language.",
75
+ unknownLanguage: "Unknown language",
76
+ unknownAction: "Unknown action",
77
+ },
78
+ "zh-Hans": {
79
+ unset: "未设置",
80
+ noLocalesInline: "可用语言: 未找到语言包",
81
+ noLocales: "未找到语言包。",
82
+ currentLanguage: "当前语言",
83
+ localization: "本地化",
84
+ enabled: "已开启",
85
+ disabled: "已关闭",
86
+ availableLanguages: "可用语言",
87
+ current: "当前",
88
+ default: "默认",
89
+ statusTitle: "OpenCode 界面本地化",
90
+ lastUpdated: "最后切换时间",
91
+ languagePack: "语言包",
92
+ stateFile: "状态文件",
93
+ availableCommands: "可用命令",
94
+ question: "选择 OpenCode 界面语言",
95
+ commandOn: "/i18n on 或 /i18n 开 - 开启本地化标题",
96
+ commandOff: "/i18n off 或 /i18n 关 - 关闭本地化标题",
97
+ commandToggle: "/i18n toggle 或 /i18n 切换 - 切换开关",
98
+ commandChoose: "/i18n - 选择语言",
99
+ refreshHint: "提示: 如果界面没有立即刷新,请重启 OpenCode。",
100
+ written: "已写入",
101
+ setNeedsEnabled: "设置开关时必须提供 enabled: true 或 enabled: false。",
102
+ localeNeedsSelection: "切换语言时必须提供 question 选择的语言名称。",
103
+ unknownLanguage: "未知语言",
104
+ unknownAction: "未知操作",
105
+ },
106
+ "zh-Hant": {
107
+ unset: "未設定",
108
+ noLocalesInline: "可用語言: 未找到語言包",
109
+ noLocales: "未找到語言包。",
110
+ currentLanguage: "目前語言",
111
+ localization: "本地化",
112
+ enabled: "已開啟",
113
+ disabled: "已關閉",
114
+ availableLanguages: "可用語言",
115
+ current: "目前",
116
+ default: "預設",
117
+ statusTitle: "OpenCode 介面本地化",
118
+ lastUpdated: "最後切換時間",
119
+ languagePack: "語言包",
120
+ stateFile: "狀態檔案",
121
+ availableCommands: "可用命令",
122
+ question: "選擇 OpenCode 介面語言",
123
+ commandOn: "/i18n on 或 /i18n 開 - 開啟本地化標題",
124
+ commandOff: "/i18n off 或 /i18n 關 - 關閉本地化標題",
125
+ commandToggle: "/i18n toggle 或 /i18n 切換 - 切換開關",
126
+ commandChoose: "/i18n - 選擇語言",
127
+ refreshHint: "提示: 如果介面沒有立即重新整理,請重啟 OpenCode。",
128
+ written: "已寫入",
129
+ setNeedsEnabled: "設定開關時必須提供 enabled: true 或 enabled: false。",
130
+ localeNeedsSelection: "切換語言時必須提供 question 選擇的語言名稱。",
131
+ unknownLanguage: "未知語言",
132
+ unknownAction: "未知操作",
133
+ },
134
+ }
135
+
136
+ function messages(locale: LocaleCode | undefined) {
137
+ return MESSAGES[locale ?? ""] ?? MESSAGES.en
138
+ }
139
+
140
+ async function readLocaleInfo(state: I18nState): Promise<LocaleInfo> {
141
+ return localeInfo(await readConfig(), state)
142
+ }
143
+
144
+ function localeConfig(info: LocaleInfo, locale: LocaleCode | undefined) {
145
+ return locale ? info.config?.locales[locale] : undefined
146
+ }
147
+
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) {
163
+ if (!locale) return text.unset
164
+ const label = info.labels.get(locale)
165
+ return label && label !== locale ? `${locale} (${label})` : locale
166
+ }
167
+
168
+ function formatAvailableLocales(info: LocaleInfo, text: Messages) {
169
+ if (info.available.length === 0) return text.noLocalesInline
170
+
171
+ return `${text.availableLanguages}: ${info.available.map((locale) => formatLocale(locale, info, text)).join(", ")}`
172
+ }
173
+
174
+ function localesMessage(state: I18nState, info: LocaleInfo) {
175
+ const text = messages(info.activeLocale)
176
+ const activeConfig = localeConfig(info, info.activeLocale)
177
+ const fallbackConfig = localeConfig(info, "en")
178
+ if (info.available.length === 0) return text.noLocales
179
+ const current = formatLocale(info.activeLocale, info, text)
180
+ const questionData = {
181
+ locale: info.activeLocale ?? "",
182
+ question: `${activeConfig?.language_picker.question ?? text.question} (${text.currentLanguage}: ${current})`,
183
+ options: info.available.map((locale) => ({
184
+ label: info.labels.get(locale) ?? locale,
185
+ locale,
186
+ description:
187
+ activeConfig?.language_picker.option_descriptions[locale] ??
188
+ fallbackConfig?.language_picker.option_descriptions[locale] ??
189
+ `Switch to ${info.labels.get(locale) ?? locale}`,
190
+ })),
191
+ }
192
+
193
+ return [
194
+ `${text.currentLanguage}: ${current}`,
195
+ `${text.localization}: ${state.enabled ? text.enabled : text.disabled}`,
196
+ `${text.availableLanguages}:`,
197
+ ...info.available.map((locale) => {
198
+ const label = info.labels.get(locale) ?? locale
199
+ const markers = [
200
+ locale === info.activeLocale ? text.current : "",
201
+ locale === info.defaultLocale ? text.default : "",
202
+ ].filter(Boolean)
203
+ const suffix = markers.length > 0 ? ` (${markers.join(", ")})` : ""
204
+ return `- ${label} => ${locale}${suffix}`
205
+ }),
206
+ "",
207
+ "QUESTION_DATA:",
208
+ JSON.stringify(questionData),
209
+ ].join("\n")
210
+ }
211
+
212
+ function statusMessage(state: I18nState, info: LocaleInfo) {
213
+ const text = messages(info.activeLocale)
214
+ const status = state.enabled ? text.enabled : text.disabled
215
+ const updated = state.updatedAt ? `\n${text.lastUpdated}: ${state.updatedAt}` : ""
216
+
217
+ return [
218
+ `${text.statusTitle}: ${status}${updated}`,
219
+ `${text.currentLanguage}: ${formatLocale(info.activeLocale, info, text)}`,
220
+ `${text.languagePack}: ${CONFIG_PATH}`,
221
+ `${text.stateFile}: ${STATE_PATH}`,
222
+ formatAvailableLocales(info, text),
223
+ "",
224
+ `${text.availableCommands}:`,
225
+ text.commandOn,
226
+ text.commandOff,
227
+ text.commandToggle,
228
+ text.commandChoose,
229
+ "",
230
+ text.refreshHint,
231
+ ].join("\n")
232
+ }
233
+
234
+ export default tool({
235
+ description: "Manage OpenCode interface localization state and language.",
236
+ args: {
237
+ action: tool.schema.enum(ACTIONS).describe("Action: status, set, toggle, locale, locales"),
238
+ enabled: tool.schema.boolean().optional().describe("Use with action=set; true enables localization, false disables it"),
239
+ locale: tool.schema.string().optional().describe("Use with action=locale; pass the language name selected from question"),
240
+ },
241
+ async execute(args) {
242
+ const action = args.action as Action
243
+
244
+ if (action === "status") {
245
+ const state = await readState()
246
+ return statusMessage(state, await readLocaleInfo(state))
247
+ }
248
+
249
+ if (action === "locales") {
250
+ const state = await readState()
251
+ return localesMessage(state, await readLocaleInfo(state))
252
+ }
253
+
254
+ if (action === "set") {
255
+ const current = await readState()
256
+ const info = await readLocaleInfo(current)
257
+ const text = messages(info.activeLocale)
258
+ if (typeof args.enabled !== "boolean") return text.setNeedsEnabled
259
+ const state = await writeState({ enabled: args.enabled })
260
+ return `${statusMessage(state, await readLocaleInfo(state))}\n\n${text.written}: ${STATE_PATH}`
261
+ }
262
+
263
+ if (action === "toggle") {
264
+ const current = await readState()
265
+ const info = await readLocaleInfo(current)
266
+ const text = messages(info.activeLocale)
267
+ const state = await writeState({ enabled: !current.enabled })
268
+ return `${statusMessage(state, await readLocaleInfo(state))}\n\n${text.written}: ${STATE_PATH}`
269
+ }
270
+
271
+ if (action === "locale") {
272
+ const rawLocale = args.locale?.trim()
273
+ const current = await readState()
274
+ const info = await readLocaleInfo(current)
275
+ const currentText = messages(info.activeLocale)
276
+ if (!rawLocale) return currentText.localeNeedsSelection
277
+
278
+ const locale = resolveLocaleInput(rawLocale, info)
279
+ if (info.available.length > 0 && !info.available.includes(locale)) {
280
+ return [`${currentText.unknownLanguage}: ${locale}`, formatAvailableLocales(info, currentText)].join("\n")
281
+ }
282
+
283
+ const state = await writeState({ locale, enabled: locale !== "en" })
284
+ const nextInfo = await readLocaleInfo(state)
285
+ const nextText = messages(nextInfo.activeLocale)
286
+ return `${statusMessage(state, nextInfo)}\n\n${nextText.written}: ${STATE_PATH}`
287
+ }
288
+
289
+ const state = await readState()
290
+ const info = await readLocaleInfo(state)
291
+ return `${messages(info.activeLocale).unknownAction}: ${action}`
292
+ },
293
+ })