116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
|
||
import {
|
||
catalogKeys,
|
||
FORMAT_FALLBACK,
|
||
formatDate,
|
||
formatList,
|
||
formatMessage,
|
||
formatNumber,
|
||
formatRelativeTime,
|
||
localeDirection,
|
||
messagePlaceholders,
|
||
normalizeLocale,
|
||
resolveMessage,
|
||
selectMessage,
|
||
selectPlural,
|
||
} from "../../src/presentation/i18n/index.js";
|
||
import {
|
||
EN_MESSAGES,
|
||
KO_MESSAGES,
|
||
MESSAGE_CATALOGS,
|
||
} from "../../src/presentation/i18n/catalog.js";
|
||
|
||
describe("internationalization message contract", () => {
|
||
it("keeps every registered catalog and interpolation contract in parity", () => {
|
||
expect(catalogKeys("en-US")).toEqual(catalogKeys("ko-KR"));
|
||
expect(catalogKeys("ar-EG")).toEqual(catalogKeys("ko-KR"));
|
||
|
||
for (const key of catalogKeys("ko-KR")) {
|
||
expect(messagePlaceholders(EN_MESSAGES[key])).toEqual(
|
||
messagePlaceholders(KO_MESSAGES[key]),
|
||
);
|
||
}
|
||
expect(MESSAGE_CATALOGS["ar-EG"]).toBe(EN_MESSAGES);
|
||
});
|
||
|
||
it("formats typed parameters and expands the pseudo locale", () => {
|
||
expect(
|
||
formatMessage("en-US", "route.documentTitle", {
|
||
title: "Settings",
|
||
appName: "Skeleton",
|
||
}),
|
||
).toBe("Settings · Skeleton");
|
||
const pseudo = formatMessage("en-XA", "action.closeNamed", {
|
||
title: "Account",
|
||
});
|
||
expect(pseudo).toMatch(/^[.+ ···]$/);
|
||
expect(pseudo.length).toBeGreaterThan("Close Account".length);
|
||
});
|
||
|
||
it("falls back safely for unknown locale, key and missing interpolation", () => {
|
||
expect(normalizeLocale("fr-FR")).toBe("ko-KR");
|
||
expect(resolveMessage("fr-FR", "action.retry")).toBe("다시 시도");
|
||
expect(resolveMessage("en-US", "action.login")).toBe("Sign in");
|
||
expect(resolveMessage("en-US", "backend.stack.trace")).toBe(
|
||
"요청한 문구를 표시할 수 없습니다.",
|
||
);
|
||
expect(resolveMessage("en-US", "action.closeNamed")).toBe(
|
||
"요청한 문구를 표시할 수 없습니다.",
|
||
);
|
||
expect(resolveMessage("en-US", "backend.stack.trace")).not.toContain(
|
||
"backend.stack.trace",
|
||
);
|
||
});
|
||
|
||
it("defines direction without inferring it in components", () => {
|
||
expect(localeDirection("ko-KR")).toBe("ltr");
|
||
expect(localeDirection("en-XA")).toBe("ltr");
|
||
expect(localeDirection("ar-EG")).toBe("rtl");
|
||
});
|
||
});
|
||
|
||
describe("internationalization formatter contract", () => {
|
||
const instant = new Date("2026-07-26T23:30:00.000Z");
|
||
|
||
it("uses an explicit UTC default and deterministic locale formatting", () => {
|
||
expect(formatDate("en-US", instant, { dateStyle: "short" })).toBe(
|
||
"7/26/26",
|
||
);
|
||
expect(
|
||
formatDate("en-US", instant, {
|
||
dateStyle: "short",
|
||
timeZone: "Asia/Seoul",
|
||
}),
|
||
).toBe("7/27/26");
|
||
expect(formatNumber("en-US", 1234.5)).toBe("1,234.5");
|
||
expect(formatRelativeTime("en-US", -1, "day")).toBe("yesterday");
|
||
expect(formatList("en-US", ["one", "two", "three"])).toBe(
|
||
"one, two, and three",
|
||
);
|
||
expect(selectPlural("en-US", 1, { one: "item", other: "items" })).toBe(
|
||
"item",
|
||
);
|
||
expect(
|
||
selectPlural("en-US", 3, { one: "item", other: "items" }),
|
||
).toBe("items");
|
||
expect(
|
||
selectMessage("pending", {
|
||
ready: "Ready",
|
||
pending: "Pending",
|
||
other: "Unknown",
|
||
}),
|
||
).toBe("Pending");
|
||
});
|
||
|
||
it("does not throw or expose invalid formatter input", () => {
|
||
expect(formatDate("en-US", Number.NaN)).toBe(FORMAT_FALLBACK);
|
||
expect(formatNumber("en-US", Number.POSITIVE_INFINITY)).toBe(
|
||
FORMAT_FALLBACK,
|
||
);
|
||
expect(
|
||
formatDate("en-US", instant, { timeZone: "not/a-time-zone" }),
|
||
).toBe(FORMAT_FALLBACK);
|
||
});
|
||
});
|