@3sln/bab
v0.2.0
Published
A small, pluggable localization library.
Maintainers
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/babimport { 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) // worksIt 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.jsonIt 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.
