• 0.6.1 b7d0ce8926

    0.6.1
    All checks were successful
    Unit Tests / Check Unit Tests (push) Successful in 26s
    Stable

    michi released this 2026-07-07 12:33:30 +02:00 | 6 commits to main since this release

    • fix: enhance robustness of object property access
    Downloads
  • 0.6.0 bc709a6020

    0.6.0
    All checks were successful
    Unit Tests / Check Unit Tests (push) Successful in 20s
    Stable

    michi released this 2026-07-07 11:37:32 +02:00 | 8 commits to main since this release

    • feat: introduce onMissingKey and refine missing key resolution
    • fix: key type inconsistency
    • fix: recursive multiple arg replacements
    • build: fix missing pieces in package.json
    Downloads
  • 0.5.0 971a9c4510

    0.5.0
    All checks were successful
    Unit Tests / Check Unit Tests (push) Successful in 28s
    Stable

    michi released this 2026-07-07 01:54:31 +02:00 | 14 commits to main since this release

    vode-i18n

    Internationalization (i18n) library for vode based apps.

    Support for plurals (utilizing Intl.PluralRules).
    Translations can even be vodes.

    Usage

    ESM

    import { createI18nContext } from 'https://unpkg.com/@ryupold/vode-i18n/dist/vode-i18n.min.mjs';
    

    IIFE

    <script src="https://unpkg.com/@ryupold/vode-i18n/dist/vode-i18n.es5.min.js"></script>
    <script>
        const { createI18nContext } = V;
    </script>
    

    NPM

    NPM

    import { createI18nContext } from '@ryupold/vode-i18n';
    

    Quick start

    import { createI18nContext } from '@ryupold/vode-i18n';
    
    const { $T, $V } = createI18nContext({
        locale: "en",
        catalog: {
            greeting: "Hello, {1}!",
            menu: {
                file: {
                    open: "Open…",
                    save: "Save {1}",
                },
            },
            items: {
                0: "no items at all",
                _one: "{1} item",
                _other: "{1} items",
            },
            banner: ["div", { class: "banner" }, "Welcome ", ["b", "{1}"]],
        },
        fallbackCatalog: (key) => key, // return the key as-is if no translation is found
    });
    
    $T("greeting", "John");     // "Hello, John!"
    $T("menu.file.open");       // "Open…"
    $T("items", 1);             // "1 item"
    $T("items", 0);             // "no items at all"
    $V("banner", "John");       // ["div", { class: "banner" }, "Welcome ", ["b", "John"]]
    

    When using TypeScript, all keys are typed. So for example, $T("menu.file.foobar") is a compile error because "foobar" is not a valid key for "menu.file".

    The catalog

    A catalog is a plain nested object. Leaves can be:

    • strings: optionally containing {1}, {2}, … placeholders
    • plural forms: objects with _other (required) and optional _zero, _one, _two, _few, _many or exact numeric keys. See plural forms.
    • vodes: [TAG, PROPS?, ...CHILDREN] tuples, for translations with markup
    • anything else: numbers, dates, functions… accessible via $R

    Nested objects become dot-separated key paths: { menu: { file: { open: "Open…" } } } is addressed as "menu.file.open".

    For multiple languages, create one context per locale. Type the other languages against your primary one so the compiler tells you when a translation is missing:

    const en = { greeting: "Hello, {1}!" };
    const de: typeof en = { greeting: "Hallo, {1}!" };
    
    const i18n = createI18nContext({
        locale: userLocale,
        catalog: userLocale === "de" ? de : en,
        fallbackCatalog: en,
    });
    

    createI18nContext

    function createI18nContext<C>(options: {
        locale: Intl.UnicodeBCP47LocaleIdentifier;
        catalog: C;
        fallbackCatalog?: Partial<C> | object | ((key: string) => unknown);
    }): I18nContext<C>
    
    • locale: a BCP 47 locale identifier like "en", "es-MX", "zh-Hant-TW". Used to pick the right plural rules.
    • catalog: your translation object. It is flattened once at creation, so lookups afterwards are a single Map.get.
    • fallbackCatalog (optional): consulted whenever a key misses the main catalog. Either another catalog object (e.g. your primary language) or a function (key: string) => string to produce a fallback value.

    The returned context contains locale and the six functions below.

    Translate to text

    $T(key, pluralAndFirstArg?, ...restArgs): string
    

    Looks up key and returns the translated string. Returns undefined if the key exists in neither catalog.

    Placeholder arguments fill {1}, {2}, … in order:

    const { $T } = createI18nContext({
        locale: "en",
        catalog: {
            summary: "Today {1} has accomplished {2} tasks",
            reversed: "{2} before {1}",
        },
    });
    
    $T("summary", "John", "2");          // "Today John has accomplished 2 tasks"
    $T("reversed", "world", "hello");    // "hello before world"
    

    Placeholders without a matching argument stay in the text ($T("summary", "John")"Today John has accomplished {2} tasks"), and extra arguments are silently ignored.

    Plurals

    If the value at key is a plural form (an object with _other key) and the first argument is a number, it selects the plural category (cardinal) and also fills {1}:

    const { $T } = createI18nContext({
        locale: "en",
        catalog: {
            files: {
                _one: "one file in {2}",
                _other: "{1} files in {2}",
            },
        },
    });
    
    $T("files", 1, "folder");   // "one file in folder"
    $T("files", 3, "trash");    // "3 files in trash"
    

    Selection order:

    1. an exact numeric key (0, 2, …) wins if present
    2. otherwise the category chosen by Intl.PluralRules for your locale (_zero, _one, _two, _few, _many)
    3. otherwise _other

    Which of the six CLDR categories apply is decided by the locale, not by hardcoded English logic. French, for example, treats 0 as singular and uses _many for millions:

    const { $T } = createI18nContext({
        locale: "fr",
        catalog: {
            jours: {
                _one: "{1} jour restant",
                _many: "{1} de jours restants",
                _other: "{1} jours restants",
            },
        },
    });
    
    $T("jours", 0);        // "0 jour restant" (singular!)
    $T("jours", 1);        // "1 jour restant"
    $T("jours", 5);        // "5 jours restants"
    $T("jours", 1000000);  // "1000000 de jours restants"
    

    For ordinal plurals (1st, 2nd, 3rd…), pass { type: "ordinal", value: n } as first argument after the key instead of a plain number:

    const { $T } = createI18nContext({
        locale: "en",
        catalog: {
            place: {
                _one: "{1}st place",
                _two: "{1}nd place",
                _few: "{1}rd place",
                _other: "{1}th place",
            },
        },
    });
    
    $T("place", { type: "ordinal", value: 1 });     // "1st place"
    $T("place", { type: "ordinal", value: 22 });    // "22nd place"
    $T("place", { type: "ordinal", value: 103 });   // "103rd place"
    

    ({ type: "cardinal", value: n } is equivalent to passing n directly.)

    Translations as vodes

    $V(key, pluralAndFirstArg?, ...restArgs): ChildVode
    

    Same lookup, plural, and placeholder rules as $T, but the catalog value may be a vode:

    const { $V } = createI18nContext({
        locale: "en",
        catalog: {
            banner: ["div", { class: "banner" }, "Welcome {1}", ["b", "to {2}"]],
            items: {
                _one: ["span", { class: "single" }, "one item"],
                _other: ["span", { class: "multi" }, "{1} items"],
            },
        },
    });
    
    $V("banner", "John", "Wonderland");
    // ["div", { class: "banner" }, "Welcome John", ["b", "to Wonderland"]]
    
    $V("items", 4);
    // ["span", { class: "multi" }, "4 items"]
    

    Drop the result straight into your view:

    app(document.body, state, (s) => [DIV,
        $V("banner", s.userName, s.appName),
    ]);
    

    Notes:

    • Placeholders are replaced in children (recursively), never in props: a {1} inside class or style stays as-is.
    • The vode is deep-copied before replacement, so the catalog is never mutated.
    • Plain strings work with $V too; it simply returns the translated string.

    Raw catalog access

    $R(key): any
    

    Returns whatever sits at key. No placeholder replacement, no plural selection. Use it for non-string values or to grab whole subtrees:

    const { $R } = createI18nContext({
        locale: "en",
        catalog: {
            meta: { version: 3, released: new Date("2026-07-06") },
            a: { b: { c: "leaf" } },
        },
    });
    
    $R("meta.version");    // 3
    $R("a.b");             // { c: "leaf" }
    $R("a.b.c");           // "leaf"
    

    $TPrefix / $VPrefix / $RPrefix

    Each function has a prefix companion that binds it to a subtree of the catalog. Great for components that only care about one corner of your translations:

    const { $TPrefix } = createI18nContext({
        locale: "en",
        catalog: {
            menu: {
                file: {
                    open: "Open…",
                    save: "Save {1}",
                    recent: { _one: "{1} recent file", _other: "{1} recent files" },
                },
            },
        },
    });
    
    const $file = $TPrefix("menu.file");
    
    $file("open");           // "Open…"
    $file("save", "a.txt");  // "Save a.txt"
    $file("recent", 12);     // "12 recent files"
    

    The returned function is fully typed against the subtree. $file("nope") won't compile in TS. $VPrefix and $RPrefix work the same way for vodes and raw values.

    Fallback catalog

    When a key is missing from the main catalog, the fallbackCatalog is consulted before giving up:

    const { $T } = createI18nContext({
        locale: "de",
        catalog: de,        // partial German translation
        fallbackCatalog: en, // complete English catalog
    });
    
    • The main catalog always wins when both have the key.
    • Fallback values support everything the main catalog does: placeholders, plural forms, vodes.
    • A function can also fallback be provided to handle missing keys. It is good practice to return a default value indicating the key was not found. I usually return the key itself.
    createI18nContext({
        locale: "en",
        catalog: en,
        fallbackCatalog: (key) => key,
    });
    

    If the key is found nowhere, $T/$V/$R return undefined.

    Type safety

    The key types are derived from your catalog with template literal types:

    • $T only accepts paths that lead to a string or plural form
    • $V only accepts paths that lead to a vode, string, or vode plural form
    • $R accepts any path in the catalog

    Development

    npm run test      # compile & run the test suite
    npm run release   # build all dist variants (ESM, IIFE, minified, ES5) + type declarations
    

    TypeScript

    License: MIT

    Downloads