Download packages/app/src/context/language.tsx from SaylorTwift/opencode: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/context/language.tsx
- Command line
-
hf download hf://SaylorTwift/opencode/packages/app/src/context/language.tsx
-
curl -L -o language.tsx https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/context/language.tsx
10.5 kB
| 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 { pluralCategory, type UiI18nPluralKey } from "@opencode-ai/ui/context/i18n" | |
| import { Persist, persisted } from "@/utils/persist" | |
| import { dict as en } from "@/i18n/en" | |
| import { dict as uiEn } from "@opencode-ai/ui/i18n/en" | |
| import { | |
| createDesktopNativeBundle, | |
| detectDesktopNativeLocale, | |
| DESKTOP_NATIVE_ENGLISH, | |
| DESKTOP_NATIVE_LABELS, | |
| DESKTOP_NATIVE_LOCALES, | |
| DESKTOP_NATIVE_LOCALE_TAGS, | |
| type DesktopNativeBundle, | |
| type DesktopNativeLocale, | |
| } from "@/i18n/desktop-native" | |
| export type Locale = DesktopNativeLocale | |
| export type Direction = "ltr" | "rtl" | |
| const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa", "fa", "dv"]) | |
| function localeDirection(locale: Locale): Direction { | |
| return RTL_LOCALES.has(locale) ? "rtl" : "ltr" | |
| } | |
| type RawDictionary = typeof en & typeof uiEn | |
| type Dictionary = i18n.Flatten<RawDictionary> | |
| type PluralKey = | |
| | UiI18nPluralKey | |
| | "session.question.pending" | |
| | "session.followupDock.summary" | |
| | "session.revertDock.summary" | |
| type Source = { dict: Record<string, string> } | |
| function cookie(locale: Locale) { | |
| return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` | |
| } | |
| const LOCALES: readonly Locale[] = DESKTOP_NATIVE_LOCALES | |
| const INTL = DESKTOP_NATIVE_LOCALE_TAGS | |
| const base = i18n.flatten({ ...en, ...uiEn }) | |
| const dicts = new Map<Locale, Dictionary>([["en", base]]) | |
| const merge = (app: Promise<Source>, ui: Promise<Source>) => | |
| Promise.all([app, ui]).then(([a, b]) => ({ ...base, ...i18n.flatten({ ...a.dict, ...b.dict }) }) as Dictionary) | |
| const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = { | |
| 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")), | |
| uk: () => merge(import("@/i18n/uk"), import("@opencode-ai/ui/i18n/uk")), | |
| 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")), | |
| hi: () => merge(import("@/i18n/hi"), import("@opencode-ai/ui/i18n/hi")), | |
| nl: () => merge(import("@/i18n/nl"), import("@opencode-ai/ui/i18n/nl")), | |
| id: () => merge(import("@/i18n/id"), import("@opencode-ai/ui/i18n/id")), | |
| vi: () => merge(import("@/i18n/vi"), import("@opencode-ai/ui/i18n/vi")), | |
| it: () => merge(import("@/i18n/it"), import("@opencode-ai/ui/i18n/it")), | |
| ur: () => merge(import("@/i18n/ur"), import("@opencode-ai/ui/i18n/ur")), | |
| pa: () => merge(import("@/i18n/pa"), import("@opencode-ai/ui/i18n/pa")), | |
| az: () => merge(import("@/i18n/az"), import("@opencode-ai/ui/i18n/az")), | |
| fi: () => merge(import("@/i18n/fi"), import("@opencode-ai/ui/i18n/fi")), | |
| sv: () => merge(import("@/i18n/sv"), import("@opencode-ai/ui/i18n/sv")), | |
| am: () => merge(import("@/i18n/am"), import("@opencode-ai/ui/i18n/am")), | |
| bg: () => merge(import("@/i18n/bg"), import("@opencode-ai/ui/i18n/bg")), | |
| bn: () => merge(import("@/i18n/bn"), import("@opencode-ai/ui/i18n/bn")), | |
| ca: () => merge(import("@/i18n/ca"), import("@opencode-ai/ui/i18n/ca")), | |
| cs: () => merge(import("@/i18n/cs"), import("@opencode-ai/ui/i18n/cs")), | |
| dv: () => merge(import("@/i18n/dv"), import("@opencode-ai/ui/i18n/dv")), | |
| dz: () => merge(import("@/i18n/dz"), import("@opencode-ai/ui/i18n/dz")), | |
| el: () => merge(import("@/i18n/el"), import("@opencode-ai/ui/i18n/el")), | |
| et: () => merge(import("@/i18n/et"), import("@opencode-ai/ui/i18n/et")), | |
| fa: () => merge(import("@/i18n/fa"), import("@opencode-ai/ui/i18n/fa")), | |
| fo: () => merge(import("@/i18n/fo"), import("@opencode-ai/ui/i18n/fo")), | |
| hr: () => merge(import("@/i18n/hr"), import("@opencode-ai/ui/i18n/hr")), | |
| hu: () => merge(import("@/i18n/hu"), import("@opencode-ai/ui/i18n/hu")), | |
| hy: () => merge(import("@/i18n/hy"), import("@opencode-ai/ui/i18n/hy")), | |
| is: () => merge(import("@/i18n/is"), import("@opencode-ai/ui/i18n/is")), | |
| ka: () => merge(import("@/i18n/ka"), import("@opencode-ai/ui/i18n/ka")), | |
| km: () => merge(import("@/i18n/km"), import("@opencode-ai/ui/i18n/km")), | |
| lo: () => merge(import("@/i18n/lo"), import("@opencode-ai/ui/i18n/lo")), | |
| lt: () => merge(import("@/i18n/lt"), import("@opencode-ai/ui/i18n/lt")), | |
| lv: () => merge(import("@/i18n/lv"), import("@opencode-ai/ui/i18n/lv")), | |
| mk: () => merge(import("@/i18n/mk"), import("@opencode-ai/ui/i18n/mk")), | |
| mn: () => merge(import("@/i18n/mn"), import("@opencode-ai/ui/i18n/mn")), | |
| ms: () => merge(import("@/i18n/ms"), import("@opencode-ai/ui/i18n/ms")), | |
| my: () => merge(import("@/i18n/my"), import("@opencode-ai/ui/i18n/my")), | |
| ne: () => merge(import("@/i18n/ne"), import("@opencode-ai/ui/i18n/ne")), | |
| ro: () => merge(import("@/i18n/ro"), import("@opencode-ai/ui/i18n/ro")), | |
| si: () => merge(import("@/i18n/si"), import("@opencode-ai/ui/i18n/si")), | |
| sk: () => merge(import("@/i18n/sk"), import("@opencode-ai/ui/i18n/sk")), | |
| sl: () => merge(import("@/i18n/sl"), import("@opencode-ai/ui/i18n/sl")), | |
| sq: () => merge(import("@/i18n/sq"), import("@opencode-ai/ui/i18n/sq")), | |
| sr: () => merge(import("@/i18n/sr"), import("@opencode-ai/ui/i18n/sr")), | |
| tg: () => merge(import("@/i18n/tg"), import("@opencode-ai/ui/i18n/tg")), | |
| tk: () => merge(import("@/i18n/tk"), import("@opencode-ai/ui/i18n/tk")), | |
| uz: () => merge(import("@/i18n/uz"), import("@opencode-ai/ui/i18n/uz")), | |
| } | |
| 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) | |
| } | |
| function detectLocale(): Locale { | |
| if (typeof navigator !== "object") return "en" | |
| return detectDesktopNativeLocale(navigator.languages?.length ? navigator.languages : [navigator.language]) | |
| } | |
| 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() | |
| const initialLocale = | |
| warm === "en" | |
| ? Promise.resolve(warm) | |
| : loadDict(warm).then( | |
| () => warm, | |
| () => "en" as const, | |
| ) | |
| export function loadInitialLocale() { | |
| return initialLocale | |
| } | |
| export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({ | |
| name: "Language", | |
| gate: false, | |
| init: (props: { locale?: Locale; onNativeTranslations?: (bundle: DesktopNativeBundle) => void }) => { | |
| const initial = props.locale ?? readStoredLocale() ?? detectLocale() | |
| const [store, setStore, _, ready] = persisted( | |
| Persist.global("language", ["language.v1"]), | |
| createStore({ | |
| locale: initial, | |
| }), | |
| ) | |
| const locale = createMemo<Locale>(() => normalizeLocale(store.locale)) | |
| const intl = createMemo(() => INTL[locale()]) | |
| const [layout, setLayout] = createStore({ direction: undefined as Direction | undefined }) | |
| const direction = createMemo(() => layout.direction ?? localeDirection(locale())) | |
| const layoutLocale = createMemo(() => { | |
| if (!layout.direction) return intl() | |
| // Kobalte derives menu direction from locale rather than accepting a direction override. | |
| return layout.direction === "rtl" ? "ar" : "en" | |
| }) | |
| 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, string | number | boolean>, | |
| ) => string | |
| const plural = (key: PluralKey, count: number, params?: Record<string, string | number | boolean>) => { | |
| const category = pluralCategory(intl(), count) | |
| const current = (dict.loading ? base : (dict() ?? base)) as Record<string, string> | |
| const candidate = `${key}.${category}` | |
| const fallback = `${key}.other` | |
| return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, { ...params, count }) | |
| } | |
| const label = (value: Locale) => DESKTOP_NATIVE_LABELS[value] | |
| createEffect(() => { | |
| if (typeof document !== "object") return | |
| const value = locale() | |
| document.documentElement.lang = intl() | |
| document.documentElement.dir = direction() | |
| document.cookie = cookie(value) | |
| }) | |
| createEffect(() => { | |
| if (!props.onNativeTranslations || dict.loading) return | |
| const current = dict() | |
| if (!current) return | |
| props.onNativeTranslations( | |
| createDesktopNativeBundle(locale(), (key) => current[key] ?? DESKTOP_NATIVE_ENGLISH[key]), | |
| ) | |
| }) | |
| return { | |
| ready, | |
| locale, | |
| intl, | |
| direction, | |
| layoutLocale, | |
| locales: LOCALES, | |
| label, | |
| t, | |
| plural, | |
| setLocale(next: Locale) { | |
| setStore("locale", normalizeLocale(next)) | |
| }, | |
| setDirection(next: Direction) { | |
| setLayout("direction", next === localeDirection(locale()) ? undefined : next) | |
| }, | |
| } | |
| }, | |
| }) | |