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,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
|
+
})
|