Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,14 @@ export function createI18n<
throw new Error("The hydrated i18n catalog must match its locale.");
}

const requestedDirection = hydrated
? hydrated.dir
: props.dir === undefined
? "ltr"
: props.dir;
const value = Object.freeze({
locale,
dir: assertDirection(hydrated?.dir ?? props.dir ?? "ltr"),
dir: assertDirection(requestedDirection),
catalog: locale,
});

Expand Down Expand Up @@ -243,8 +248,3 @@ export function createI18n<
},
});
}
/** Format a message in an explicitly selected locale. */
/** Read the active locale. */
/** Read the active locale's text direction. */
/** Read the active catalog identifier. */
/** Serialize the active locale state for hydration. */
79 changes: 79 additions & 0 deletions tests/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,85 @@ describe("createI18n", () => {
).toThrow("Invalid i18n text direction: sideways");
});

it.each(["", "auto", "RTL", "left-to-right", null, 0])(
"should reject additional initial and hydrated direction value %j",
(dir) => {
expect(() =>
renderToStringSync(() =>
messages.Scope({ locale: "en", dir, children: () => messages.direction() } as never),
),
).toThrow("Invalid i18n text direction");
expect(() =>
renderToStringSync(() =>
messages.Scope({
hydration: { version: 1, locale: "en", catalog: "en", dir },
children: () => messages.direction(),
} as never),
),
).toThrow("Invalid i18n text direction");
},
);

it("should switch locales after hydration without retaining direction state", () => {
const hydrated = renderToStringSync(() =>
messages.Scope({
hydration: { version: 1, locale: "ar", catalog: "ar", dir: "rtl" },
children: () => `${messages.locale()}:${messages.direction()}`,
}),
);
const switched = renderToStringSync(() =>
messages.Scope({
locale: "en",
children: () => `${messages.locale()}:${messages.direction()}`,
}),
);
expect(hydrated).toBe("ar:rtl");
expect(switched).toBe("en:ltr");
});

it("should support complex plural categories through application-owned message functions", () => {
const plurals = createI18n("ar", {
ar: {
items: (count: number) => new Intl.PluralRules("ar").select(count),
},
pl: {
items: (count: number) => new Intl.PluralRules("pl").select(count),
},
});
expect([0, 1, 2, 3, 11, 100].map((count) => plurals.format("ar", "items", count))).toEqual([
"zero",
"one",
"two",
"few",
"many",
"other",
]);
expect([1, 2, 5, 1.5].map((count) => plurals.format("pl", "items", count))).toEqual([
"one",
"few",
"many",
"other",
]);
});

it("should preserve placeholder-shaped interpolation values and mixed-direction text", () => {
const literal = "Ada {name} {{total}}";
expect(messages.format("en", "greeting", { name: literal })).toBe(`Hello, ${literal}`);
expect(messages.format("ar", "greeting", { name: "Ada 123" })).toBe("مرحبا، Ada 123");
});

it("should reject absent, partial, and missing-key catalog access explicitly", () => {
expect(() => createI18n("en", {} as never)).toThrow("at least one catalog");
expect(() =>
createI18n("en", {
en: { first: () => "first", second: () => "second" },
fr: { first: () => "premier" } as never,
}),
).toThrow("missing second");
const unsafeFormat = messages.format as unknown as (locale: string, key: string) => string;
expect(() => unsafeFormat("en", "missing")).toThrow("Missing i18n message: en.missing");
});

it("should reject invalid catalogs for untyped callers and own frozen copies", () => {
expect(() =>
createI18n("en", {
Expand Down
23 changes: 22 additions & 1 deletion tests/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { createI18n } from "../src/index";
import {
createI18n,
type Catalog,
type CatalogMessage,
type I18n,
type I18nHydration,
type I18nScopeProps,
type TextDirection,
} from "../src/index";

const i18n = createI18n("en", {
en: {
Expand All @@ -14,6 +22,19 @@ const i18n = createI18n("en", {
i18n.text("greeting", { name: "Ada" });
i18n.format("fr", "count", 2);

const direction: TextDirection = "rtl";
const message: CatalogMessage<[string]> = (value) => value;
const catalog: Catalog = { message };
const service: I18n<typeof i18n.catalogs> = i18n;
const hydration: I18nHydration<"en" | "fr"> = {
version: 1,
locale: "fr",
dir: direction,
catalog: "fr",
};
const scopeProps: I18nScopeProps<"en" | "fr"> = { hydration };
void [catalog, service, scopeProps];

createI18n("en", {
en: { greeting: (name: string) => name },
// @ts-expect-error every locale must contain every source key
Expand Down