Files
flxn-app/src/lib/i18n/index.ts
T

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;
}