import * as i18n from "@solid-primitives/i18n" import { createEffect, createMemo, createResource } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { Persist, persisted } from "@/utils/persist" import { dict as en } from "@/i18n/en" import { dict as uiEn } from "@opencode-ai/ui/i18n/en" export type Locale = | "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br" | "th" | "bs" | "tr" type RawDictionary = typeof en & typeof uiEn type Dictionary = i18n.Flatten type Source = { dict: Record } function cookie(locale: Locale) { return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` } const LOCALES: readonly Locale[] = [ "en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "bs", "ar", "no", "br", "th", "tr", ] const INTL: Record = { en: "en", zh: "zh-Hans", zht: "zh-Hant", ko: "ko", de: "de", es: "es", fr: "fr", da: "da", ja: "ja", pl: "pl", ru: "ru", ar: "ar", no: "nb-NO", br: "pt-BR", th: "th", bs: "bs", tr: "tr", } const LABEL_KEY: Record = { en: "language.en", zh: "language.zh", zht: "language.zht", ko: "language.ko", de: "language.de", es: "language.es", fr: "language.fr", da: "language.da", ja: "language.ja", pl: "language.pl", ru: "language.ru", ar: "language.ar", no: "language.no", br: "language.br", th: "language.th", bs: "language.bs", tr: "language.tr", } const base = i18n.flatten({ ...en, ...uiEn }) const dicts = new Map([["en", base]]) const merge = (app: Promise, ui: Promise) => Promise.all([app, ui]).then(([a, b]) => ({ ...base, ...i18n.flatten({ ...a.dict, ...b.dict }) }) as Dictionary) const loaders: Record, () => Promise> = { zh: () => merge(import("@/i18n/zh"), import("@opencode-ai/ui/i18n/zh")), zht: () => merge(import("@/i18n/zht"), import("@opencode-ai/ui/i18n/zht")), ko: () => merge(import("@/i18n/ko"), import("@opencode-ai/ui/i18n/ko")), de: () => merge(import("@/i18n/de"), import("@opencode-ai/ui/i18n/de")), es: () => merge(import("@/i18n/es"), import("@opencode-ai/ui/i18n/es")), fr: () => merge(import("@/i18n/fr"), import("@opencode-ai/ui/i18n/fr")), da: () => merge(import("@/i18n/da"), import("@opencode-ai/ui/i18n/da")), ja: () => merge(import("@/i18n/ja"), import("@opencode-ai/ui/i18n/ja")), pl: () => merge(import("@/i18n/pl"), import("@opencode-ai/ui/i18n/pl")), ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")), ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")), no: () => merge(import("@/i18n/no"), import("@opencode-ai/ui/i18n/no")), br: () => merge(import("@/i18n/br"), import("@opencode-ai/ui/i18n/br")), th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")), bs: () => merge(import("@/i18n/bs"), import("@opencode-ai/ui/i18n/bs")), tr: () => merge(import("@/i18n/tr"), import("@opencode-ai/ui/i18n/tr")), } function loadDict(locale: Locale) { const hit = dicts.get(locale) if (hit) return Promise.resolve(hit) if (locale === "en") return Promise.resolve(base) const load = loaders[locale] return load().then((next: Dictionary) => { dicts.set(locale, next) return next }) } export function loadLocaleDict(locale: Locale) { return loadDict(locale).then(() => undefined) } const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [ { locale: "en", match: (language) => language.startsWith("en") }, { locale: "zht", match: (language) => language.startsWith("zh") && language.includes("hant") }, { locale: "zh", match: (language) => language.startsWith("zh") }, { locale: "ko", match: (language) => language.startsWith("ko") }, { locale: "de", match: (language) => language.startsWith("de") }, { locale: "es", match: (language) => language.startsWith("es") }, { locale: "fr", match: (language) => language.startsWith("fr") }, { locale: "da", match: (language) => language.startsWith("da") }, { locale: "ja", match: (language) => language.startsWith("ja") }, { locale: "pl", match: (language) => language.startsWith("pl") }, { locale: "ru", match: (language) => language.startsWith("ru") }, { locale: "ar", match: (language) => language.startsWith("ar") }, { locale: "no", match: (language) => language.startsWith("no") || language.startsWith("nb") || language.startsWith("nn"), }, { locale: "br", match: (language) => language.startsWith("pt") }, { locale: "th", match: (language) => language.startsWith("th") }, { locale: "bs", match: (language) => language.startsWith("bs") }, { locale: "tr", match: (language) => language.startsWith("tr") }, ] 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 const normalized = language.toLowerCase() const match = localeMatchers.find((entry) => entry.match(normalized)) if (match) return match.locale } return "en" } export function normalizeLocale(value: string): Locale { return LOCALES.includes(value as Locale) ? (value as Locale) : "en" } function readStoredLocale() { if (typeof localStorage !== "object") return try { const raw = localStorage.getItem("opencode.global.dat:language") if (!raw) return const next = JSON.parse(raw) as { locale?: string } if (typeof next?.locale !== "string") return return normalizeLocale(next.locale) } catch { return } } const warm = readStoredLocale() ?? detectLocale() if (warm !== "en") void loadDict(warm) export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({ name: "Language", init: (props: { locale?: Locale }) => { const initial = props.locale ?? readStoredLocale() ?? detectLocale() const [store, setStore, _, ready] = persisted( Persist.global("language", ["language.v1"]), createStore({ locale: initial, }), ) const locale = createMemo(() => normalizeLocale(store.locale)) const intl = createMemo(() => INTL[locale()]) const [dict] = createResource(locale, loadDict, { initialValue: dicts.get(initial) ?? base, }) const t = i18n.translator(() => dict() ?? base, i18n.resolveTemplate) as ( key: keyof Dictionary, params?: Record, ) => string const label = (value: Locale) => t(LABEL_KEY[value]) createEffect(() => { if (typeof document !== "object") return document.documentElement.lang = locale() document.cookie = cookie(locale()) }) return { ready, locale, intl, locales: LOCALES, label, t, setLocale(next: Locale) { setStore("locale", normalizeLocale(next)) }, } }, })