feat(desktop): i18n for tauri side

This commit is contained in:
adamelmore
2026-01-27 15:00:17 -06:00
parent acf0df1e98
commit 51edf68606
8 changed files with 204 additions and 28 deletions

View File

@@ -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

View File

@@ -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
}