diff options
| author | Adam <[email protected]> | 2026-02-06 08:54:51 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-06 08:54:51 -0600 |
| commit | 812597bb8b101896a8988493d37261ff851ae502 (patch) | |
| tree | 3b713c13f49c65665e6a7296ed483a3f6f8befbf /packages/console/app/src/lib/language.ts | |
| parent | 0ec5f6608bdfea5be62dbbdc4c04a61de6d3e67c (diff) | |
| download | opencode-812597bb8b101896a8988493d37261ff851ae502.tar.gz opencode-812597bb8b101896a8988493d37261ff851ae502.zip | |
feat(web): i18n (#12471)
Diffstat (limited to 'packages/console/app/src/lib/language.ts')
| -rw-r--r-- | packages/console/app/src/lib/language.ts | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/packages/console/app/src/lib/language.ts b/packages/console/app/src/lib/language.ts new file mode 100644 index 000000000..e1e62d8fd --- /dev/null +++ b/packages/console/app/src/lib/language.ts @@ -0,0 +1,175 @@ +export const LOCALES = [ + "en", + "zh", + "zht", + "ko", + "de", + "es", + "fr", + "it", + "da", + "ja", + "pl", + "ru", + "ar", + "no", + "br", + "th", + "tr", +] as const + +export type Locale = (typeof LOCALES)[number] + +export const LOCALE_COOKIE = "oc_locale" as const + +const LABEL = { + en: "English", + zh: "简体中文", + zht: "繁體中文", + ko: "한국어", + de: "Deutsch", + es: "Español", + fr: "Français", + it: "Italiano", + da: "Dansk", + ja: "日本語", + pl: "Polski", + ru: "Русский", + ar: "العربية", + no: "Norsk", + br: "Português (Brasil)", + th: "ไทย", + tr: "Türkçe", +} satisfies Record<Locale, string> + +const TAG = { + en: "en", + zh: "zh-Hans", + zht: "zh-Hant", + ko: "ko", + de: "de", + es: "es", + fr: "fr", + it: "it", + da: "da", + ja: "ja", + pl: "pl", + ru: "ru", + ar: "ar", + no: "no", + br: "pt-BR", + th: "th", + tr: "tr", +} satisfies Record<Locale, string> + +export function parseLocale(value: unknown): Locale | null { + if (typeof value !== "string") return null + if ((LOCALES as readonly string[]).includes(value)) return value as Locale + return null +} + +export function label(locale: Locale) { + return LABEL[locale] +} + +export function tag(locale: Locale) { + return TAG[locale] +} + +export function dir(locale: Locale) { + if (locale === "ar") return "rtl" + return "ltr" +} + +function match(input: string): Locale | null { + const value = input.trim().toLowerCase() + if (!value) return null + + if (value.startsWith("zh")) { + if (value.includes("hant") || value.includes("-tw") || value.includes("-hk") || value.includes("-mo")) return "zht" + return "zh" + } + + if (value.startsWith("ko")) return "ko" + if (value.startsWith("de")) return "de" + if (value.startsWith("es")) return "es" + if (value.startsWith("fr")) return "fr" + if (value.startsWith("it")) return "it" + if (value.startsWith("da")) return "da" + if (value.startsWith("ja")) return "ja" + if (value.startsWith("pl")) return "pl" + if (value.startsWith("ru")) return "ru" + if (value.startsWith("ar")) return "ar" + if (value.startsWith("tr")) return "tr" + if (value.startsWith("th")) return "th" + if (value.startsWith("pt")) return "br" + if (value.startsWith("no") || value.startsWith("nb") || value.startsWith("nn")) return "no" + if (value.startsWith("en")) return "en" + return null +} + +export function detectFromLanguages(languages: readonly string[]) { + for (const language of languages) { + const locale = match(language) + if (locale) return locale + } + return "en" satisfies Locale +} + +export function detectFromAcceptLanguage(header: string | null) { + if (!header) return "en" satisfies Locale + + const items = header + .split(",") + .map((raw) => raw.trim()) + .filter(Boolean) + .map((raw) => { + const parts = raw.split(";").map((x) => x.trim()) + const lang = parts[0] ?? "" + const q = parts + .slice(1) + .find((x) => x.startsWith("q=")) + ?.slice(2) + return { + lang, + q: q ? Number.parseFloat(q) : 1, + } + }) + .sort((a, b) => b.q - a.q) + + for (const item of items) { + if (!item.lang || item.lang === "*") continue + const locale = match(item.lang) + if (locale) return locale + } + + return "en" satisfies Locale +} + +export function localeFromCookieHeader(header: string | null) { + if (!header) return null + + const raw = header + .split(";") + .map((x) => x.trim()) + .find((x) => x.startsWith(`${LOCALE_COOKIE}=`)) + ?.slice(`${LOCALE_COOKIE}=`.length) + + if (!raw) return null + return parseLocale(decodeURIComponent(raw)) +} + +export function localeFromRequest(request: Request) { + return ( + localeFromCookieHeader(request.headers.get("cookie")) ?? + detectFromAcceptLanguage(request.headers.get("accept-language")) + ) +} + +export function cookie(locale: Locale) { + return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` +} + +export function clearCookie() { + return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax` +} |
