136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
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 MessageKey> =
|
||
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<Record<string, MessageKey>>);
|
||
|
||
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<Record<string, string | number>>,
|
||
): 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<Key extends MessageKey>(
|
||
locale: string,
|
||
key: Key,
|
||
...args: MessageArguments<Key>
|
||
): string {
|
||
return resolveMessage(
|
||
locale,
|
||
key,
|
||
args[0] as Readonly<Record<string, string | number>> | undefined,
|
||
);
|
||
}
|
||
|
||
function pseudoLocalize(value: string): string {
|
||
const expanded = value.replace(/[A-Za-z]/g, (character) => {
|
||
const replacements: Readonly<Record<string, string>> = {
|
||
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"];
|
||
}
|