feat: add internationalization message platform
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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"];
|
||||
}
|
||||
Reference in New Issue
Block a user