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

@yaebal/i18n

v0.1.2

Published

yaebal i18n plugin — typed keys & params, nested dicts, Intl plurals, language_code detection, per-chat locale persistence, ctx.t / ctx.changeLanguage (powers useTranslation).

Readme

@yaebal/i18n

typed translations for yaebal bots: ctx.t with compile-time keys and params, nested dicts, real Intl.PluralRules plurals for any language, first-contact locale detection from telegram's language_code, and per-chat persistence through any StorageAdapter.

install

pnpm add @yaebal/i18n

usage

import { Bot } from "yaebal";
import { type Dict, i18n, type LocaleLike } from "@yaebal/i18n";

// `as const` keeps templates as literal types → typed keys and typed params
const en = {
  welcome: "hi {name}!",
  menu: { title: "menu" },                       // nested → t("menu.title")
  items: { one: "{n} item", other: "{n} items" }, // plural forms
} as const satisfies Dict;

// LocaleLike keeps ru structurally in sync with en; every key is optional —
// missing keys fall back to the default locale
const ru = {
  welcome: "привет {name}!",
  items: { one: "{n} товар", few: "{n} товара", many: "{n} товаров", other: "{n} товара" },
} as const satisfies LocaleLike<typeof en>;

const bot = new Bot(process.env.BOT_TOKEN!)
  .install(i18n({ defaultLocale: "en", locales: { en, ru } }))
  .command("start", (ctx) =>
    ctx.reply(ctx.t("welcome", { name: ctx.from?.first_name ?? "there" })),
  )
  .command("lang", async (ctx) => {
    await ctx.changeLanguage(ctx.locale === "ru" ? "en" : "ru"); // typed + validated
    return ctx.reply(ctx.t("welcome", { name: "again" }));
  });

ctx.t("welcom") is a compile error. ctx.t("welcome") without params is a compile error. ctx.changeLanguage("de") is a compile error (and a runtime TypeError from untyped code) — a bad locale can never be persisted, and stale storage values that no longer match a configured locale are ignored instead of poisoning the chat.

locale resolution

per update, in order: stored locale (set by changeLanguage, persisted per chat) → detected locale (ctx.from?.language_code, matched exactly and then by base language — "pt-BR""pt") → defaultLocale. so a russian user gets russian on the very first /start, before ever touching a language switcher.

detection and the storage key are overridable:

i18n({
  defaultLocale: "en",
  locales: { en, ru },
  storage: new RedisStorage(),                    // any StorageAdapter<string>
  getKey: (ctx) => ctx.from?.id.toString(),       // per-user instead of per-chat
  detectLocale: (ctx) => ctx.from?.language_code, // the default
});

plurals

a value whose keys are all Intl.PluralRules categories (zero/one/two/few/many/other, with other required) is a plural set. t selects the form with the rules of the locale that supplied the forms — a russian viewer falling back to an english string gets english plural rules — and requires a numeric n param, at compile time and at runtime.

ctx.t("items", { n: 5 }); // ru → "5 товаров", en → "5 items"

formatted translations

a dict value can be a function: it receives typed params and its return type flows through t. return an html/md/format result and the translation carries entities — no parse_mode, nothing to escape:

import { html } from "@yaebal/fmt";

const en = {
  hello: (p: { name: string }) => html`<b>hello</b>, ${p.name}!`,
} as const satisfies Dict;

ctx.reply(ctx.t("hello", { name: ctx.who })); // { text, entities } — reply takes it as-is

outside middleware

createI18n builds the translator without any bot wiring — for broadcasts, jobs, command menus — and the plugin can share the same instance:

import { createI18n, i18n } from "@yaebal/i18n";

const strings = createI18n({ defaultLocale: "en", locales: { en, ru } });

strings.t("ru", "welcome", { name: "Юра" }); // standalone
strings.resolveLocale("pt-BR");              // → configured locale or undefined
bot.install(i18n(strings, { storage }));     // same dicts, bot-wired

api

| export | what it is | | --- | --- | | i18n(options) / i18n(instance, options?) | the plugin — adds ctx.t, ctx.locale, ctx.locales, ctx.defaultLocale, ctx.changeLanguage | | createI18n(options) | standalone translator: t(locale, key, params?), locales, has, resolveLocale | | Dict / LocaleLike<Base> | dict shape + structural check for non-default locales | | PluralDict, TFn<D>, DictKeys<D>, I18nControls | the typed surface, exported for wrappers | | onMissingKey?: (key, locale) => string \| undefined | hook for keys missing everywhere (default: return the key itself) |

interpolation is a single pass — a param value can never be re-parsed as another placeholder, so user input like a first name containing {n} is inert. unknown placeholders stay verbatim.


part of yaebal — a type-safe, runtime-agnostic Telegram Bot API framework. MIT.