Why Lingua?
Most i18n libraries either couple runtime and framework, or require a global plugin system. Lingua is a plain object with a subscription model that any framework can consume directly.
ts
// Before — manual key lookup with no type safety or fallback
const messages = { en: { greeting: 'Hello, {name}!' }, de: { greeting: 'Hallo, {name}!' } };
const locale = 'de';
const raw = (messages[locale] ?? messages['en'])['greeting'].replace('{name}', 'Alice');
// After — typed keys, fallback chain, plural resolution, reactive subscriptions
const i18n = createI18n({ locale: 'de', fallback: 'en', catalogs: messages });
const greeting = i18n.t('greeting', { name: 'Alice' });- Minimal API:
t,tp,extend,preload,setLocale,register,scope,fork,getSnapshot,subscribe,has,isLoaded,isRegistered,dispose,getSupportedLocales - Deterministic locale fallback chain resolution
- Typed leaf and plural branch keys with explicit APIs (
tandtp) - Explicit locale source model (static messages or async loaders)
- Typed error class
LinguaErrorwith stableE.*code constants - Framework-agnostic store primitives that compose with any UI framework
- Zero dependencies
| Feature | Lingua | i18next | FormatJS |
|---|---|---|---|
| Bundle size | 3.7 KB | ~24 kB | ~16 kB |
| Typed key ergonomics | Partial | Partial | |
| Deterministic fallback chain | |||
| Async locale preload | |||
| Namespace lazy loading | extend()) | Partial | |
| Runtime snapshots + subscriptions | |||
| External formatter bridge | @vielzeug/lingua/format) | Partial | |
| Framework agnostic | |||
| Zero dependencies |
Use Lingua when you want a compact, typed runtime with deterministic fallback behavior and framework-agnostic reactive state.
Consider i18next or FormatJS when you need larger ecosystem plugins, message extraction pipelines, or mature framework-specific integrations.
Installation
sh
pnpm add @vielzeug/linguash
npm install @vielzeug/linguash
yarn add @vielzeug/linguaQuick Start
ts
import { createI18n } from '@vielzeug/lingua';
import { createFormatter } from '@vielzeug/lingua/format';
const i18n = createI18n({
locale: 'en',
fallback: 'en',
catalogs: {
en: {
greeting: 'Hello, {name}!',
inbox: {
zero: 'No messages',
one: 'One message',
other: '{count} messages',
},
},
fr: () => import('./locales/fr.json').then((m) => m.default),
},
});
await i18n.preload('fr');
await i18n.setLocale('fr');
const greeting = i18n.t('greeting', { name: 'Alice' });
const messages = i18n.tp('inbox', 3);
// Scope reduces key repetition inside a namespace
const nav = i18n.scope('nav');
nav.t('home'); // resolves 'nav.home'
// Namespace-based lazy loading for route-specific keys
await i18n.extend('settings', (locale) => import(`./routes/${locale}/settings.i18n.json`).then((m) => m.default));
// Formatter bound to the current locale — follows locale changes automatically
const fmt = createFormatter(() => i18n.locale);
const price = fmt.currency(12.5, 'EUR');
const unsubscribe = i18n.subscribe(
(next) => {
console.log(next.locale);
},
{ immediate: true },
);
unsubscribe();
i18n.getSupportedLocales();Features
- One runtime primitive:
createI18n(options) - Explicit translation methods:
t(leafKey, vars?)andtp(branchKey, count, options?) - Explicit locale lifecycle:
register,preload,setLocale - Namespace lazy loading:
extend(ns, factory, locale?)registers and immediately loads a partial catalog — deduplicates perns + locale; use for per-route or per-feature keys - Scoped translation helpers:
scope(prefix)returns a{ fmt, t, tp, has }helper bound to a key prefix - Unified key existence check:
has(key)returnstruefor leaf keys, branch keys, and pipe-plural base keys in the active fallback chain - Loaded-locale predicate:
isLoaded(locale)returnstruewhen a catalog is fully resolved — safe forserializeI18n()guards - Registered-locale predicate:
isRegistered(locale)distinguishes "never configured" from "async loader not yet called" - Instance disposal:
dispose()clears all subscribers and catalog state — prevents memory leaks in route-scoped SPA instances - Typed error handling:
LinguaErrorclass withE.*code constants for safe error branching - Instance forking:
fork(overrides?)creates an isolated child for SSR or test isolation - Reactive model through snapshots:
getSnapshot,subscribe - Deterministic fallback chain using active locale plus configured fallback locales
- Separate missing handlers:
onMissingKey(key, locale)andonMissingVar(varName, key, locale) - Formatting kept separate via
createFormatter(source)from@vielzeug/lingua/format