// The loaded catalogs as the host uses them per request: resolve the locale, then hand out a // translator for it. A plugin's translator looks in its own catalogs first, so a plugin may shadow // any core string; both fall back to en-US per key, so an untranslated corner is English, never blank. // Translators are memoised — catalogs are immutable after boot, so there is one per locale+plugin. import { DEFAULT_LOCALE, type Catalog } from "./catalog.ts"; import type { LoadedI18n } from "./load.ts"; import { resolveLocale, type ResolvedLocale } from "./locale.ts"; import { createTranslator, type Translate } from "./translate.ts"; export interface ResolveRequest { acceptLanguage?: string | undefined; param?: string | null | undefined; } export interface I18n { available: string[]; resolve(request: ResolveRequest): ResolvedLocale; translator(locale: string, pluginId?: string): Translate; } export function createI18n(loaded: LoadedI18n): I18n { const memo = new Map(); return { available: loaded.available, resolve: ({ acceptLanguage, param }) => resolveLocale({ acceptLanguage, available: loaded.available, param }), translator: (locale, pluginId) => { const key = `${locale} ${pluginId ?? ""}`; let translate = memo.get(key); if (!translate) { translate = createTranslator({ catalogs: chain(loaded, locale, pluginId), locale }); memo.set(key, translate); } return translate; }, }; } function chain(loaded: LoadedI18n, locale: string, pluginId: string | undefined): Catalog[] { const plugin = pluginId === undefined ? undefined : loaded.plugins.get(pluginId); const catalogs = [plugin?.get(locale), plugin?.get(DEFAULT_LOCALE), loaded.core.get(locale), loaded.core.get(DEFAULT_LOCALE)]; return [...new Set(catalogs.filter((catalog): catalog is Catalog => catalog !== undefined))]; }