npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ryupold/vode-i18n

v0.6.1

Published

A library for internationalization (i18n) specifically for usage in vode based apps

Readme

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

Binds the library to the global Vi18n object.

<script src="https://unpkg.com/@ryupold/vode-i18n/dist/vode-i18n.es5.min.js"></script>
<script>
    const { createI18nContext } = Vi18n;
</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}"]],
    },
    onMissingKey: (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?: DeepPartial<C | Record<string, unknown>> | undefined;
    onMissingKey?: (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.
  • onMissingKey (optional): consulted whenever a key misses the main catalog and the fallback catalog is not defined.

The returned context contains locale and the six functions below.

Translate to text

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

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

Placeholder arguments (strings or numbers) fill {1}, {2}, … in order:

const { $T } = createI18nContext({
    locale: "en",
    catalog: {
        summary: "Today {1} finished {2}",
        reversed: "{2} before {1}",
    },
});

$T("summary", "John", "his homework"); // "Today John finished his homework"
$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 be provided as fallback 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