UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

562 lines (454 loc) 15.4 kB
import { Alepha } from "alepha"; import { describe, test } from "vitest"; import { AlephaReactI18n } from "../index.ts"; import { $dictionary } from "../primitives/$dictionary.ts"; import { I18nProvider } from "../providers/I18nProvider.ts"; describe("I18nProvider", () => { test("should register dictionaries on initialization", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { hello: "Hello" } }), }); fr = $dictionary({ lazy: async () => ({ default: { hello: "Bonjour" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); expect(i18n.registry).toHaveLength(2); expect(i18n.registry[0].lang).toBe("en"); expect(i18n.registry[1].lang).toBe("fr"); }); test("should load all translations on server start", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { hello: "Hello", world: "World" } }), }); fr = $dictionary({ lazy: async () => ({ default: { hello: "Bonjour", world: "Monde" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.registry[0].translations).toEqual({ hello: "Hello", world: "World", }); expect(i18n.registry[1].translations).toEqual({ hello: "Bonjour", world: "Monde", }); }); test("should default to fallback language", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { hello: "Hello" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); expect(i18n.lang).toBe("en"); }); test("should translate keys in current language", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { greeting: "Hello, World!" } }), }); fr = $dictionary({ lazy: async () => ({ default: { greeting: "Bonjour, Monde!" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.tr("greeting")).toBe("Hello, World!"); await i18n.setLang("fr"); expect(i18n.tr("greeting")).toBe("Bonjour, Monde!"); }); test("should fallback to fallback language when key missing", async ({ expect, }) => { class App { en = $dictionary({ lazy: async () => ({ default: { hello: "Hello", world: "World" }, }), }); fr = $dictionary({ lazy: async () => ({ default: { hello: "Bonjour" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); await i18n.setLang("fr"); expect(i18n.tr("hello")).toBe("Bonjour"); expect(i18n.tr("world")).toBe("World"); // Falls back to English }); test("should return key when translation not found", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { hello: "Hello" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.tr("missing.key")).toBe("missing.key"); }); test("should support default option when key missing", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { hello: "Hello" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.tr("missing", { default: "Default Text" })).toBe( "Default Text", ); }); test("should support variable interpolation", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { greeting: "Hello, $1! You have $2 messages." }, }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.tr("greeting", { args: ["John", "5"] })).toBe( "Hello, John! You have 5 messages.", ); }); test("should list all available languages", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { hello: "Hello" } }), }); fr = $dictionary({ lazy: async () => ({ default: { hello: "Bonjour" } }), }); de = $dictionary({ lazy: async () => ({ default: { hello: "Hallo" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); const languages = i18n.languages; expect(languages).toContain("en"); expect(languages).toContain("fr"); expect(languages).toContain("de"); expect(languages).toHaveLength(3); }); test("should support custom language codes", async ({ expect }) => { class App { enUS = $dictionary({ lang: "en-US", lazy: async () => ({ default: { color: "color" } }), }); enGB = $dictionary({ lang: "en-GB", lazy: async () => ({ default: { color: "colour" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.registry[0].lang).toBe("en-US"); expect(i18n.registry[1].lang).toBe("en-GB"); }); test("should support custom dictionary names", async ({ expect }) => { class App { english = $dictionary({ name: "custom-en", lang: "en", lazy: async () => ({ default: { hello: "Hello" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); expect(i18n.registry[0].name).toBe("custom-en"); }); test("should update state when language changes", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { hello: "Hello" } }), }); fr = $dictionary({ lazy: async () => ({ default: { hello: "Bonjour" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); // State is set lazily, so we should set a language first await i18n.setLang("en"); expect(alepha.store.get("alepha.react.i18n.lang")).toBe("en"); await i18n.setLang("fr"); expect(alepha.store.get("alepha.react.i18n.lang")).toBe("fr"); }); test("should have number formatter for current language", async ({ expect, }) => { class App { en = $dictionary({ lazy: async () => ({ default: {} }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.numberFormat).toBeDefined(); expect(i18n.numberFormat.format).toBeTypeOf("function"); }); test("should handle multiple translations with same key across dictionaries", async ({ expect, }) => { class App { en = $dictionary({ lazy: async () => ({ default: { app: "Application", settings: "Settings", }, }), }); fr = $dictionary({ lazy: async () => ({ default: { app: "Programme", settings: "Paramètres", }, }), }); es = $dictionary({ lazy: async () => ({ default: { app: "Aplicación", settings: "Configuración", }, }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.tr("app")).toBe("Application"); expect(i18n.tr("settings")).toBe("Settings"); await i18n.setLang("fr"); expect(i18n.tr("app")).toBe("Programme"); expect(i18n.tr("settings")).toBe("Paramètres"); await i18n.setLang("es"); expect(i18n.tr("app")).toBe("Aplicación"); expect(i18n.tr("settings")).toBe("Configuración"); }); test("should handle empty dictionaries", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: {} }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.tr("anything")).toBe("anything"); }); test("should handle complex interpolation patterns", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { complex: "User $1 has $2 followers and follows $3 people", }, }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.tr("complex", { args: ["Alice", "1000", "500"] })).toBe( "User Alice has 1000 followers and follows 500 people", ); }); describe("when only non-English dictionaries are registered", () => { test("should default to the first registered language", async ({ expect, }) => { class App { fr = $dictionary({ lazy: async () => ({ default: { hello: "Bonjour" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); expect(i18n.lang).toBe("fr"); }); test("should translate using the first registered language", async ({ expect, }) => { class App { fr = $dictionary({ lazy: async () => ({ default: { greeting: "Bonjour!", settings: "Paramètres" }, }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); expect(i18n.tr("greeting")).toBe("Bonjour!"); expect(i18n.tr("settings")).toBe("Paramètres"); }); test("should not include 'en' in languages list", async ({ expect }) => { class App { fr = $dictionary({ lazy: async () => ({ default: { hello: "Bonjour" } }), }); de = $dictionary({ lazy: async () => ({ default: { hello: "Hallo" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); expect(i18n.languages).toEqual(["fr", "de"]); }); test("should fallback to the first registered language for missing keys", async ({ expect, }) => { class App { fr = $dictionary({ lazy: async () => ({ default: { hello: "Bonjour", world: "Monde" }, }), }); de = $dictionary({ lazy: async () => ({ default: { hello: "Hallo" } }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); await i18n.setLang("de"); expect(i18n.tr("hello")).toBe("Hallo"); expect(i18n.tr("world")).toBe("Monde"); // Falls back to FR }); }); describe("server-side language autodetect (Accept-Language)", () => { /** * Injects a real, dictionary-populated `I18nProvider` and exposes the * protected resolution used by the `server:onRequest` hook, so the priority * (cookie → header → fallback) can be unit-tested against a true registry. */ const setup = (AppClass: new () => unknown) => { const alepha = Alepha.create().with(AlephaReactI18n); alepha.inject(AppClass as any); const i18n = alepha.inject(I18nProvider); return { i18n, resolve: (cookieLang?: string, headerLang?: string): string => (i18n as any).resolveRequestLang(cookieLang, headerLang), }; }; class App { en = $dictionary({ lazy: async () => ({ default: { hello: "Hello" } }), }); fr = $dictionary({ lazy: async () => ({ default: { hello: "Bonjour" } }), }); } test("uses the Accept-Language header when no cookie is set", ({ expect, }) => { expect(setup(App).resolve(undefined, "fr")).toBe("fr"); }); test("lets a manually-selected cookie win over the header", ({ expect, }) => { expect(setup(App).resolve("en", "fr")).toBe("en"); }); test("falls back when the header language is not registered", ({ expect, }) => { expect(setup(App).resolve(undefined, "de")).toBe("en"); }); test("falls back when neither cookie nor header is present", ({ expect, }) => { expect(setup(App).resolve(undefined, undefined)).toBe("en"); }); test("normalizes a region-qualified header to a registered base language", ({ expect, }) => { expect(setup(App).resolve(undefined, "fr-FR")).toBe("fr"); }); test("prefers an exact region match when one is registered", ({ expect, }) => { class RegionApp { enUS = $dictionary({ lang: "en-US", lazy: async () => ({ default: { color: "color" } }), }); fr = $dictionary({ lazy: async () => ({ default: { color: "couleur" } }), }); } expect(setup(RegionApp).resolve(undefined, "en-US")).toBe("en-US"); }); test("ignores the header when autoDetect is disabled", ({ expect }) => { const { i18n, resolve } = setup(App); i18n.options.autoDetect = false; expect(resolve(undefined, "fr")).toBe("en"); }); test("still honours a manual cookie when autoDetect is disabled", ({ expect, }) => { const { i18n, resolve } = setup(App); i18n.options.autoDetect = false; expect(resolve("fr", "en")).toBe("fr"); }); }); test("should handle partial interpolation args", async ({ expect }) => { class App { en = $dictionary({ lazy: async () => ({ default: { message: "Hello $1, you have $2 messages and $3 alerts" }, }), }); } const alepha = Alepha.create().with(AlephaReactI18n); const app = alepha.inject(App); const i18n = alepha.inject(I18nProvider); await alepha.start(); // Provide fewer args than placeholders expect(i18n.tr("message", { args: ["Bob"] })).toBe( "Hello Bob, you have $2 messages and $3 alerts", ); }); });