-
0.5.0
StableAll checks were successfulUnit Tests / Check Unit Tests (push) Successful in 28sreleased this
2026-07-07 01:54:31 +02:00 | 14 commits to main since this releasevode-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
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,_manyor 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 singleMap.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) => stringto produce a fallback value.
The returned context contains
localeand the six functions below.Translate to text
$T(key, pluralAndFirstArg?, ...restArgs): stringLooks up
keyand returns the translated string. Returnsundefinedif 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
keyis a plural form (an object with_otherkey) 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:
- an exact numeric key (
0,2, …) wins if present - otherwise the category chosen by
Intl.PluralRulesfor your locale (_zero,_one,_two,_few,_many) - 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
_manyfor 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 passingndirectly.)Translations as vodes
$V(key, pluralAndFirstArg?, ...restArgs): ChildVodeSame 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}insideclassorstylestays as-is. - The vode is deep-copied before replacement, so the catalog is never mutated.
- Plain strings work with
$Vtoo; it simply returns the translated string.
Raw catalog access
$R(key): anyReturns 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.$VPrefixand$RPrefixwork the same way for vodes and raw values.Fallback catalog
When a key is missing from the main catalog, the
fallbackCatalogis 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/$Rreturnundefined.Type safety
The key types are derived from your catalog with template literal types:
$Tonly accepts paths that lead to a string or plural form$Vonly accepts paths that lead to a vode, string, or vode plural form$Raccepts 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
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- strings: optionally containing