import { setupI18n, type I18n, type Messages } from "@lingui/core"; import { messages as enMessages } from "../../locales/en/messages.po"; export const SUPPORTED_LOCALES = ["en", "es", "de", "ja"] as const; export type AppLocale = (typeof SUPPORTED_LOCALES)[number]; export const DEFAULT_LOCALE: AppLocale = "en"; // es is Mexican Spanish (translations + dayjs es-mx). export const LOCALE_LABELS: Record = { en: "English", es: "Español", de: "Deutsch", ja: "日本語", }; // Adding a language: add it to SUPPORTED_LOCALES + LOCALE_LABELS, add the // locale to lingui.config.ts, run `bunx lingui extract`, and translate the // generated src/locales//messages.po. const catalogLoaders = import.meta.glob<{ messages: Messages }>( "/src/locales/*/messages.po" ); const messageCache: Partial> = { [DEFAULT_LOCALE]: enMessages, }; export function resolveLocale(candidate: string | null | undefined): AppLocale { if (!candidate) return DEFAULT_LOCALE; const base = candidate.toLowerCase().split("-")[0]; return (SUPPORTED_LOCALES as readonly string[]).includes(base) ? (base as AppLocale) : DEFAULT_LOCALE; } const inflightLoads: Partial>> = {}; export async function ensureMessages(locale: AppLocale): Promise { const cached = messageCache[locale]; if (cached) return cached; const loader = catalogLoaders[`/src/locales/${locale}/messages.po`]; if (!loader) return messageCache[DEFAULT_LOCALE]!; // Dedupe concurrent loads; a rejected load is dropped so a retry can succeed. const pending = (inflightLoads[locale] ??= loader() .then(({ messages }) => { messageCache[locale] = messages; return messages; }) .finally(() => { delete inflightLoads[locale]; })); return pending; } export function primeMessages(locale: AppLocale, messages: Messages) { messageCache[locale] ??= messages; } // Seeded into the SSR-dehydrated query cache so hydration renders the active // locale without depending on a client-side catalog chunk load. export const i18nMessagesQueryKey = (locale: AppLocale) => ["i18n-messages", locale] as const; export function getCachedMessages(locale: AppLocale): Messages { return messageCache[locale] ?? messageCache[DEFAULT_LOCALE]!; } export function hasCachedMessages(locale: AppLocale): boolean { return messageCache[locale] !== undefined; } export function createI18n(locale: AppLocale): I18n { const i18n = setupI18n(); i18n.load(locale, getCachedMessages(locale)); i18n.activate(locale); return i18n; } export function parseAcceptLanguage(header: string | null): AppLocale { if (!header) return DEFAULT_LOCALE; for (const part of header.split(",")) { const tag = part.split(";")[0]?.trim(); if (!tag) continue; const base = tag.toLowerCase().split("-")[0]; if ((SUPPORTED_LOCALES as readonly string[]).includes(base)) { return base as AppLocale; } } return DEFAULT_LOCALE; }