import { EN_MESSAGES, KO_MESSAGES, MESSAGE_CATALOGS, type MessageKey, } from "./catalog.js"; export const SUPPORTED_LOCALES = Object.freeze([ "ko-KR", "en-US", "en-XA", "ar-EG", ] as const); export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number]; export type TextDirection = "ltr" | "rtl"; export type MessageParameters = Readonly<{ "action.closeNamed": { title: string }; "action.alertCloseNamed": { title: string }; "route.loadingNamed": { title: string }; "route.documentTitle": { title: string; appName: string }; "template.supportReference": { reference: string }; "form.remaining": { count: number }; "boot.supportReference": { reference: string }; }>; export type ParameterlessMessageKey = Exclude< MessageKey, keyof MessageParameters >; export type MessageArguments = Key extends keyof MessageParameters ? [parameters: MessageParameters[Key]] : [parameters?: never]; const PLACEHOLDER_PATTERN = /\{([a-zA-Z][a-zA-Z0-9]*)\}/g; const FALLBACK_LOCALE = "ko-KR" as const; export const MESSAGE_KEY_ALIASES = Object.freeze({ "action.login": "action.reauth", "async.pending": "common.processing", } as const satisfies Readonly>); export function normalizeLocale(locale: string): SupportedLocale { if ((SUPPORTED_LOCALES as readonly string[]).includes(locale)) { return locale as SupportedLocale; } const language = locale.split("-")[0]?.toLowerCase(); if (language === "ko") return "ko-KR"; if (language === "ar") return "ar-EG"; if (language === "en") return "en-US"; return FALLBACK_LOCALE; } export function localeDirection(locale: SupportedLocale): TextDirection { return locale === "ar-EG" ? "rtl" : "ltr"; } export function messagePlaceholders(template: string): readonly string[] { return Object.freeze( [...template.matchAll(PLACEHOLDER_PATTERN)].map((match) => match[1] ?? ""), ); } export function resolveMessage( locale: string, key: string, parameters?: Readonly>, ): string { const normalized = normalizeLocale(locale); const sourceLocale: keyof typeof MESSAGE_CATALOGS = normalized === "en-XA" ? "en-US" : normalized; const catalog = MESSAGE_CATALOGS[sourceLocale] ?? MESSAGE_CATALOGS[FALLBACK_LOCALE]; const fallback = KO_MESSAGES["common.unavailable"]; const canonicalKey = MESSAGE_KEY_ALIASES[key as keyof typeof MESSAGE_KEY_ALIASES] ?? key; const template = catalog[canonicalKey as MessageKey] ?? fallback; const placeholders = messagePlaceholders(template); if ( placeholders.some( (placeholder) => parameters?.[placeholder] === undefined || parameters?.[placeholder] === null, ) ) { return fallback; } const formatted = template.replace( PLACEHOLDER_PATTERN, (_, placeholder: string) => String(parameters?.[placeholder] ?? ""), ); return normalized === "en-XA" ? pseudoLocalize(formatted) : formatted; } export function formatMessage( locale: string, key: Key, ...args: MessageArguments ): string { return resolveMessage( locale, key, args[0] as Readonly> | undefined, ); } function pseudoLocalize(value: string): string { const expanded = value.replace(/[A-Za-z]/g, (character) => { const replacements: Readonly> = { a: "á", e: "é", i: "í", o: "ó", u: "ú", A: "Á", E: "É", I: "Í", O: "Ó", U: "Ú", }; return replacements[character] ?? character; }); return `[${expanded} ···]`; } export function catalogKeys( locale: "ko-KR" | "en-US" | "ar-EG", ): MessageKey[] { return Object.keys(MESSAGE_CATALOGS[locale]).sort() as MessageKey[]; } export function fallbackMessage() { return EN_MESSAGES["common.unavailable"]; }