summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/i18n
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-27 15:00:17 -0600
committeradamelmore <[email protected]>2026-01-27 15:25:07 -0600
commit51edf68606618505ee65c53d085ebf384df10a0c (patch)
tree077cffc591b08ab22d3972a3e6727927a6a2dc6e /packages/desktop/src/i18n
parentacf0df1e985b7a740aeef7a9ba2309e308be0d5d (diff)
downloadopencode-51edf68606618505ee65c53d085ebf384df10a0c.tar.gz
opencode-51edf68606618505ee65c53d085ebf384df10a0c.zip
feat(desktop): i18n for tauri side
Diffstat (limited to 'packages/desktop/src/i18n')
-rw-r--r--packages/desktop/src/i18n/en.ts31
-rw-r--r--packages/desktop/src/i18n/index.ts134
2 files changed, 165 insertions, 0 deletions
diff --git a/packages/desktop/src/i18n/en.ts b/packages/desktop/src/i18n/en.ts
new file mode 100644
index 000000000..4008efca5
--- /dev/null
+++ b/packages/desktop/src/i18n/en.ts
@@ -0,0 +1,31 @@
+export const dict = {
+ "desktop.menu.checkForUpdates": "Check for Updates...",
+ "desktop.menu.installCli": "Install CLI...",
+ "desktop.menu.reloadWebview": "Reload Webview",
+ "desktop.menu.restart": "Restart",
+
+ "desktop.dialog.chooseFolder": "Choose a folder",
+ "desktop.dialog.chooseFile": "Choose a file",
+ "desktop.dialog.saveFile": "Save file",
+
+ "desktop.updater.checkFailed.title": "Update Check Failed",
+ "desktop.updater.checkFailed.message": "Failed to check for updates",
+ "desktop.updater.none.title": "No Update Available",
+ "desktop.updater.none.message": "You are already using the latest version of OpenCode",
+ "desktop.updater.downloadFailed.title": "Update Failed",
+ "desktop.updater.downloadFailed.message": "Failed to download update",
+ "desktop.updater.downloaded.title": "Update Downloaded",
+ "desktop.updater.downloaded.prompt":
+ "Version {{version}} of OpenCode has been downloaded, would you like to install it and relaunch?",
+ "desktop.updater.installFailed.title": "Update Failed",
+ "desktop.updater.installFailed.message": "Failed to install update",
+
+ "desktop.cli.installed.title": "CLI Installed",
+ "desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode' command.",
+ "desktop.cli.failed.title": "Installation Failed",
+ "desktop.cli.failed.message": "Failed to install CLI: {{error}}",
+
+ "desktop.error.serverStartFailed.title": "OpenCode failed to start",
+ "desktop.error.serverStartFailed.description":
+ "The local OpenCode server could not be started. Restart the app, or check your network settings (VPN/proxy) and try again.",
+} as const
diff --git a/packages/desktop/src/i18n/index.ts b/packages/desktop/src/i18n/index.ts
new file mode 100644
index 000000000..34a427c7a
--- /dev/null
+++ b/packages/desktop/src/i18n/index.ts
@@ -0,0 +1,134 @@
+import * as i18n from "@solid-primitives/i18n"
+import { Store } from "@tauri-apps/plugin-store"
+
+import { dict as desktopEn } from "./en"
+
+import { dict as appEn } from "../../../app/src/i18n/en"
+import { dict as appZh } from "../../../app/src/i18n/zh"
+import { dict as appZht } from "../../../app/src/i18n/zht"
+import { dict as appKo } from "../../../app/src/i18n/ko"
+import { dict as appDe } from "../../../app/src/i18n/de"
+import { dict as appEs } from "../../../app/src/i18n/es"
+import { dict as appFr } from "../../../app/src/i18n/fr"
+import { dict as appDa } from "../../../app/src/i18n/da"
+import { dict as appJa } from "../../../app/src/i18n/ja"
+import { dict as appPl } from "../../../app/src/i18n/pl"
+import { dict as appRu } from "../../../app/src/i18n/ru"
+import { dict as appAr } from "../../../app/src/i18n/ar"
+import { dict as appNo } from "../../../app/src/i18n/no"
+import { dict as appBr } from "../../../app/src/i18n/br"
+
+export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
+
+type RawDictionary = typeof appEn & typeof desktopEn
+type Dictionary = i18n.Flatten<RawDictionary>
+
+const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
+
+function detectLocale(): Locale {
+ if (typeof navigator !== "object") return "en"
+
+ const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
+ for (const language of languages) {
+ if (!language) continue
+ if (language.toLowerCase().startsWith("zh")) {
+ if (language.toLowerCase().includes("hant")) return "zht"
+ return "zh"
+ }
+ if (language.toLowerCase().startsWith("ko")) return "ko"
+ if (language.toLowerCase().startsWith("de")) return "de"
+ if (language.toLowerCase().startsWith("es")) return "es"
+ if (language.toLowerCase().startsWith("fr")) return "fr"
+ if (language.toLowerCase().startsWith("da")) return "da"
+ if (language.toLowerCase().startsWith("ja")) return "ja"
+ if (language.toLowerCase().startsWith("pl")) return "pl"
+ if (language.toLowerCase().startsWith("ru")) return "ru"
+ if (language.toLowerCase().startsWith("ar")) return "ar"
+ if (
+ language.toLowerCase().startsWith("no") ||
+ language.toLowerCase().startsWith("nb") ||
+ language.toLowerCase().startsWith("nn")
+ )
+ return "no"
+ if (language.toLowerCase().startsWith("pt")) return "br"
+ }
+
+ return "en"
+}
+
+function parseLocale(value: unknown): Locale | null {
+ if (!value) return null
+ if (typeof value !== "string") return null
+ if ((LOCALES as readonly string[]).includes(value)) return value as Locale
+ return null
+}
+
+function parseRecord(value: unknown) {
+ if (!value || typeof value !== "object") return null
+ if (Array.isArray(value)) return null
+ return value as Record<string, unknown>
+}
+
+function pickLocale(value: unknown): Locale | null {
+ const direct = parseLocale(value)
+ if (direct) return direct
+
+ const record = parseRecord(value)
+ if (!record) return null
+
+ return parseLocale(record.locale)
+}
+
+const base = i18n.flatten({ ...appEn, ...desktopEn })
+
+function build(locale: Locale): Dictionary {
+ if (locale === "en") return base
+ if (locale === "zh") return { ...base, ...i18n.flatten(appZh) }
+ if (locale === "zht") return { ...base, ...i18n.flatten(appZht) }
+ if (locale === "de") return { ...base, ...i18n.flatten(appDe) }
+ if (locale === "es") return { ...base, ...i18n.flatten(appEs) }
+ if (locale === "fr") return { ...base, ...i18n.flatten(appFr) }
+ if (locale === "da") return { ...base, ...i18n.flatten(appDa) }
+ if (locale === "ja") return { ...base, ...i18n.flatten(appJa) }
+ if (locale === "pl") return { ...base, ...i18n.flatten(appPl) }
+ if (locale === "ru") return { ...base, ...i18n.flatten(appRu) }
+ if (locale === "ar") return { ...base, ...i18n.flatten(appAr) }
+ if (locale === "no") return { ...base, ...i18n.flatten(appNo) }
+ if (locale === "br") return { ...base, ...i18n.flatten(appBr) }
+ return { ...base, ...i18n.flatten(appKo) }
+}
+
+const state = {
+ locale: detectLocale(),
+ dict: base as Dictionary,
+ init: undefined as Promise<Locale> | undefined,
+}
+
+state.dict = build(state.locale)
+
+const translate = i18n.translator(() => state.dict, i18n.resolveTemplate)
+
+export function t(key: keyof Dictionary, params?: Record<string, string | number>) {
+ return translate(key, params)
+}
+
+export function initI18n(): Promise<Locale> {
+ const cached = state.init
+ if (cached) return cached
+
+ const promise = (async () => {
+ const store = await Store.load("opencode.global.dat").catch(() => null)
+ if (!store) return state.locale
+
+ const raw = await store.get("language").catch(() => null)
+ const value = typeof raw === "string" ? JSON.parse(raw) : raw
+ const next = pickLocale(value) ?? state.locale
+
+ state.locale = next
+ state.dict = build(next)
+ return next
+ })().catch(() => state.locale)
+
+ state.init = promise
+ return promise
+}