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

@3sln/bab

v0.2.0

Published

A small, pluggable localization library.

Readme

@3sln/bab

A small, pluggable localization library. No dependencies — plurals and number formatting go through Intl, which the runtime already has.

npm install @3sln/bab
import { createTranslator } from '@3sln/bab';

const tr = createTranslator({ locale: 'es', catalogue: messages });

const s = tr('My string');
const x = tr('{#} days', { count: n }).singular('{#} day');
const scoped = tr.scope('my-domain');

The source text is the key

There is no t('home.header.title') indirection to keep in sync with a catalogue. What you write is what an untranslated build renders, so a missing translation degrades to correct English rather than to a dotted path — and adding a language never means touching the call sites.

A translation is a String

tr(...) returns a String object. It interpolates, concatenates, compares with ==, has .length, and hands to a DOM node exactly like a primitive:

const s = tr('Sign out');
`${s}`            // 'Sign out'
s.toUpperCase()   // 'SIGN OUT'
button(s)         // works

It carries .singular() so a plural can be refined after the fact, which is what lets the plural case read as one expression rather than a three-argument ngettext. .singular() returns a new translation — a String's value is fixed at construction.

Plurals

The plural msgid is written first and is the catalogue key; .singular() supplies the source language's other half, used when nothing is translated yet:

tr('{#} days', { count: 1 }).singular('{#} day')  // '1 day'
tr('{#} days', { count: 3 }).singular('{#} day')  // '3 days'

Selection goes through Intl.PluralRules, so a locale with more than two categories is a catalogue concern rather than an API one. A Russian catalogue supplies one / few / many / other for the same call site; if it only has other, that's what renders rather than nothing.

{#} is the count, formatted for the locale (1.234 in de). {name} is params.name. An unknown placeholder is left as written, so a typo is visible instead of silently deleting text.

Lookup is pluggable

A catalogue is anything with lookup(scope, id, category) returning a string, or undefined to fall back to the source text — a bare function will do.

| Catalogue | Use | | --- | --- | | SourceCatalogue | The default: renders source text. An untranslated build. | | ObjectCatalogue | A plain messages object, which is what a compiled .json bundle is. | | ChainCatalogue | Try each in turn — per-page overrides over a base bundle, or machine translation behind human translations. |

{
  '': { 'Sign out': 'Cerrar sesión' },                        // unscoped
  player: { '{#} days': { one: '{#} día', other: '{#} días' } },
}

A scope falls back to the unscoped bucket, so a string shared across scopes is translated once.

Missing messages

onMissing(scope, id) fires once per message — a coverage hook for development. Production stays silent and renders the source text: a missing translation is never worth a blank page.

Extraction

Because the msgid is the source text, extraction is never a step you have to run before the app works — it is how you find out what there is to translate. @3sln/bab-extract reads the source with a parser and follows translators through imports and bindings, so a scope fixed in src/i18n.js is known at a call site three modules away:

npx @3sln/bab-extract 'src/**/*.{js,jsx}' -x '**/*.test.js' -o messages.json

It writes a bab catalogue, or a gettext POT for the platforms that speak it, and it is a separate package so that bab itself stays dependency-free.

Releasing

Bumping version in package.json on main opens a draft release. Publishing that draft creates the tag and publishes to npm, after re-running the tests and checking the tag matches package.json — a version bump alone tags nothing.