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

@zoijs/i18n

v0.1.0

Published

Tiny, reactive internationalization for Zoijs: a reactive locale, message lookup with plurals, and the platform's Intl formatters. No JSX, no build step.

Readme

@zoijs/i18n

Tiny, reactive internationalization for Zoijs. A reactive locale, a message lookup with interpolation and plurals, and the platform's own Intl formatters for numbers, dates, and lists. Switch the locale and every binding that read a translation updates in place — no provider, no context, no re-render.

npm i @zoijs/i18n   # peer: @zoijs/core
import { createI18n } from "@zoijs/i18n";

export const i18n = createI18n({
  locale: "en",
  fallback: "en",
  messages: {
    en: {
      hello: "Hello, {name}!",
      items: { one: "{count} item", other: "{count} items" },
      nav: { home: "Home" },
    },
    fr: {
      hello: "Bonjour, {name} !",
      items: { one: "{count} article", other: "{count} articles" },
      nav: { home: "Accueil" },
    },
  },
});
import { html, mount } from "@zoijs/core";
import { i18n } from "./i18n.js";

function Greeting() {
  return html`
    <p>${() => i18n.t("hello", { name: "Ada" })}</p>
    <p>${() => i18n.t("items", { count: 3 })}</p>
    <button onclick=${() => i18n.setLocale(i18n.locale() === "en" ? "fr" : "en")}>
      ${() => i18n.t("nav.home")}
    </button>
  `;
}
mount(Greeting, "#app");

Wrap each translation in ${() => …} so it's a live binding — then setLocale() updates exactly those nodes.

API

createI18n({ locale, fallback?, messages? }) returns an instance with reader methods (reactive — read them inside ${() => …}) and two writers:

| Method | Kind | Description | |---|---|---| | t(key, vars?) | reader | Translate key. Fills {placeholders} from vars; selects a plural by vars.count. Dotted keys ("nav.home") walk nested tables. A missing key returns the key itself, so gaps are obvious. | | has(key) | reader | Whether key resolves in the current or fallback locale. | | locale() | reader | The current locale tag. | | n(value, options?) | reader | A number via Intl.NumberFormat in the current locale. | | d(value, options?) | reader | A date via Intl.DateTimeFormat. | | list(values, options?) | reader | A list ("a, b, and c") via Intl.ListFormat. | | setLocale(locale) | writer | Switch locale; every reader binding updates. | | add(locale, messages) | writer | Merge more messages into a locale — for lazily-loaded bundles. |

Plurals

A message can be an object keyed by CLDR plural category (one / other, plus zero / two / few / many where a language uses them). The right entry is chosen by Intl.PluralRules for the active locale — so Polish, Arabic, or Russian plural rules just work, with no rules of your own:

messages: {
  pl: { files: { one: "{count} plik", few: "{count} pliki", many: "{count} plików", other: "{count} pliku" } },
}
i18n.t("files", { count: 5 }); // → "5 plików"

Formatting

n, d, and list are thin, memoized wrappers over Intl — they take the same options and follow the current locale:

i18n.n(1234.5, { style: "currency", currency: "EUR" }); // "€1,234.50"
i18n.d(new Date(), { dateStyle: "long" });               // "June 26, 2026"
i18n.list(["a", "b", "c"], { type: "conjunction" });     // "a, b, and c"

Lazy-loading locales

Ship one locale and fetch others on demand — add() is reactive, so bindings update when the bundle arrives:

async function load(locale) {
  i18n.add(locale, await fetch(`/i18n/${locale}.json`).then((r) => r.json()));
  i18n.setLocale(locale);
}

What it is not

No global singleton, no provider component, no context, no AST/ICU message compiler, no build step, and no runtime dependencies. Translations come back as plain strings — Zoijs renders them as inert text, so an interpolated value like <img onerror=…> is shown literally, never executed.

License

MIT © Zoijs contributors