Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0189ASmkMee4F5aJ3jnKeKQv
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
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<AppLocale, string> = {
|
|
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/<locale>/messages.po.
|
|
const catalogLoaders = import.meta.glob<{ messages: Messages }>(
|
|
"/src/locales/*/messages.po"
|
|
);
|
|
|
|
const messageCache: Partial<Record<AppLocale, Messages>> = {
|
|
[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<Record<AppLocale, Promise<Messages>>> = {};
|
|
|
|
export async function ensureMessages(locale: AppLocale): Promise<Messages> {
|
|
const cached = messageCache[locale];
|
|
if (cached) return cached;
|
|
|
|
const loader = catalogLoaders[`/src/locales/${locale}/messages.po`];
|
|
if (!loader) return messageCache[DEFAULT_LOCALE]!;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|