Files
clean-architecture-frontend…/tests/unit/i18n-contract.test.ts
T

116 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});