Lightweight internationalization (i18n) library with:
- Variable interpolation (
{{name}}) - Simple count-based pluralization (
zero | one | other) - Locale and missing key fallbacks
- Message function caching for performance
npm install @andersoncustodio/i18nRequires Node >= 14 (private class fields # support).
const { I18n } = require('@andersoncustodio/i18n');
const locales = {
en: {
greeting: 'Hello, {{name}}!',
apples: { zero: 'No apples', one: 'One apple', other: '{{count}} apples' }
},
pt: {
greeting: 'Olá, {{name}}!',
apples: { zero: 'Sem maçãs', one: 'Uma maçã', other: '{{count}} maçãs' }
}
};
const i18n = new I18n(locales, () => currentLocale);
let currentLocale = 'en';
console.log(i18n.t('greeting', { name: 'João' })); // Hello, João!
currentLocale = 'pt';
console.log(i18n.t('greeting', { name: 'João' })); // Olá, João!- Locale not found: uses the default locale (third constructor parameter, defaults to 'en').
- Message key not found: returns the key path itself (
"missing.key.path").
Strings with {{variable}} are transformed into templates. If a variable is not provided, the original placeholder remains: {{variable}}.
Define an object with zero, one, and other keys:
apples: { zero: 'No apples', one: 'One apple', other: '{{count}} apples' }Selection is based on options.count.
new I18n(locales: Record<string, any>, getLocale?: () => string | undefined, defaultLocale?: string)locales: Locales object. Each locale contains nested messages.getLocale: Function that returns the current locale (e.g., from AsyncLocalStorage or request context).defaultLocale: Default locale whengetLocalereturns no value or locale doesn't exist.
Methods:
t(keyPath: string, options?: Record<string, string | number>): string– resolves and interpolates a message.
Getters:
locale: string– returns the current active locale.locales: string[]– returns an array of available locale keys.
Each compiled key for a locale is stored internally to avoid recompiling the message function on subsequent calls.
The index.d.ts file exposes utility types for safe nested key resolution (NestedKeyOf). In TypeScript you get autocomplete for key paths.
const { AsyncLocalStorage } = require('node:async_hooks');
const storage = new AsyncLocalStorage();
const i18n = new I18n(locales, () => storage.getStore()?.locale);
function handleRequest(locale) {
storage.run({ locale }, () => {
console.log(i18n.t('greeting', { name: 'Maria' }));
});
}const fastify = require('fastify')();
const { AsyncLocalStorage } = require('node:async_hooks');
const { I18n } = require('@andersoncustodio/i18n');
const storage = new AsyncLocalStorage();
const i18n = new I18n(
{
en: { greeting: 'Hello, {{name}}!' },
pt: { greeting: 'Olá, {{name}}!' },
es: { greeting: '¡Hola, {{name}}!' },
},
() => storage.getStore()?.locale
);
const t = i18n.t.bind(i18n);
fastify.addHook('onRequest', (request, reply, done) => {
const store = {
locale: request.headers['content-language'] || 'en',
};
storage.run(store, done);
});
fastify.get('/greeting/:name', (request, reply) => {
const message = t('greeting', { name: request.params.name });
reply.send({ message, locale: i18n.locale, locales: i18n.locales });
});
fastify.listen({ port: 3000, host: '0.0.0.0' }, (err) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log('Server running on http://localhost:3000');
});# English (en)
curl -H "Content-Language: en" http://localhost:3000/greeting/Anderson
# Portuguese (pt)
curl -H "Content-Language: pt" http://localhost:3000/greeting/Anderson
# Spanish (es)
curl -H "Content-Language: es" http://localhost:3000/greeting/Anderson
# Without header (uses default: en)
curl http://localhost:3000/greeting/Anderson- Keep messages short and without extra logic (the library doesn't execute JS in strings, only replaces placeholders).
- Centralize pluralizations at a single level to avoid complexity.
- Avoid interpolating unsanitized user values if rendering directly to HTML.
MIT