@bemedev/i18n
v0.5.2
Published
Internationalization library for Bemedev projects, providing utilities for managing translations and locale-specific content.
Maintainers
Readme
@bemedev/i18n
A modern, fully-typed internationalization (i18n) library for TypeScript and JavaScript with a fluent, composable API. Define once, compose per-lo// Locales are narrowed from machine.keys const call = machine.translate('greetings', { name: 'A' }).to; // call: (locale?: 'en' | 'es-ES' | 'en-US') => string
### Utility types
If you need to extract types from your i18n machine, several utility types
are provided:
```ts
import type {
ConfigFrom, // Extract the configuration type
KeysFrom, // Extract available locale keys
TranslationsFrom, // Extract all translations
TranslationFrom, // Extract a single locale translation
KeyFrom, // Extract the current locale key
} from '@bemedev/i18n/class';
import type {
RequiredTranslations, // Make all translations required
_Translations, // Make all translations optional
_Params, // Extract parameters for a translation key
CheckParams, // Validate parameter requirements
} from '@bemedev/i18n/types';
// Example usage
type Config = ConfigFrom<typeof machine>;
type Locales = KeysFrom<typeof machine>;
type AllTranslations = TranslationsFrom<typeof machine>;
type EnglishTranslation = TranslationFrom<typeof machine>;
// Extract parameters for a specific key
type GreetingParams = _Params<Config, 'greetings'>;
// => { name: string; lastLoginDate: Date }Note: Properties prefixed with __ (like __key, __translation) are
internal and marked as deprecated. They exist only for type inference and
should not be used at runtime.te safely with strong types.
Features
- ✅ Strong TypeScript typing for keys and params
- ✅ Simple, fluent API with a class-like builder
- ✅ Formatting helpers: date, number, plural, list, enum
- ✅ Nested structures with dot-path access
- ✅ Locale fallback (en, en-US ➜ en)
- ✅ Zero-deps runtime, tiny footprint
Installation
# npm
npm install @bemedev/i18n
# yarn
yarn add @bemedev/i18n
# pnpm
pnpm add @bemedev/i18nQuick start
import { create } from '@bemedev/i18n';
// Create a machine with a base locale and a default fallback
const machine = create(
dt => ({
localee: 'en',
greetings: 'Hello {name}! Your last login was {lastLoginDate:date}.',
inboxMessages: dt('Hello {name}, you have {messages:plural}.', {
plural: { messages: { one: '1 message', other: '{?} messages' } },
}),
nested: {
greetings: dt('Hello {names:list}!', {
list: { names: { style: 'short' } },
}),
},
}),
'en',
)
.provideTranslation('es-ES', dt => ({
localee: 'es-ES',
greetings: dt(
'¡Hola {name}! Tu última conexión fue el {lastLoginDate:date}.',
{
date: {
lastLoginDate: {
year: 'numeric',
month: '2-digit',
day: '2-digit',
},
},
},
),
}))
.provideTranslation('en-US', { localee: 'en-US' });
// Translate
const msg = machine
.translate('greetings', {
name: 'John',
lastLoginDate: new Date('2023-10-01T12:00:00Z'),
})
.to('es-ES');
// => "¡Hola John! Tu última conexión fue el 01/10/2023."API overview
- create(config | define => config, ...fallbacks)
- Returns an I18n machine with methods:
- provideTranslation(locale, value | define => value): chain new locale entries
- translate(key, args?).to(locale?): lazy translator returning a string
- translateWithLocale(locale, { key, args? }): direct translation function
- keys: string[] of known locales
- translations: resolved map of locale ➜ messages
- Returns an I18n machine with methods:
Defining translations
You can write plain messages or use dt to attach formatting options.
const machine = create(
dt => ({
greetings: 'Hello {name}!',
status: dt('Order is {status:enum}', {
enum: { status: { pending: 'pending', shipped: 'shipped' } },
}),
lastSeen: dt('Last seen: {date:date}', {
date: { date: { dateStyle: 'long' } },
}),
items: dt('You have {count:plural}', {
plural: { count: { one: '1 item', other: '{?} items' } },
}),
friends: dt('Online: {users:list}', {
list: { users: { style: 'long', type: 'conjunction' } },
}),
}),
'en',
);Using translate vs translateWithLocale
// Lazy: bind key/args, choose locale later
const invite = machine.translate('greetings', { name: 'Ada' });
invite.to('en');
invite.to('es-ES');
// Direct: provide locale immediately
machine.translateWithLocale('en', {
key: 'greetings',
args: { name: 'Ada' },
});Nested keys and arrays
Dot-paths are supported for deep access; non-string values (objects/arrays) are returned as-is when defined without dt.
const m = create(
{
nested: {
data: { lang: 'en', langs: ['fr', 'gb', 'es'] },
someArray: ['string1', 'string2'],
},
},
'en',
);
m.translate('nested.data').to('en'); // => { lang: 'en', langs: ['fr','gb','es'] }
m.translate('nested.someArray').to('en'); // => ['string1', 'string2']Type safety
Types are inferred from your config and preserved per-locale.
// Keys and params are type-checked
machine.translate('greetings', {
name: 'John',
lastLoginDate: new Date(),
}); // ✅
machine.translate('greetings'); // ❌ missing 'name'
machine.translate('unknown.key'); // ❌ unknown key
// Locales are narrowed from machine.keys
const call = machine.translate('greetings', { name: 'A' }).to;
// // call: (locale?: 'en' | 'es-ES' | 'en-US') => stringUtility types
If you need to extract types from your i18n machine, several utility types are provided:
import type {
ConfigFrom, // Extract the configuration type
KeysFrom, // Extract available locale keys
TranslationsFrom, // Extract all translations
TranslationFrom, // Extract a single locale translation
KeyFrom, // Extract the current locale key
} from '@bemedev/i18n/class';
import type {
RequiredTranslations, // Make all translations required
_Translations, // Make all translations optional
_Params, // Extract parameters for a translation key
CheckParams, // Validate parameter requirements
} from '@bemedev/i18n/types';
// Example usage
type Config = ConfigFrom<typeof machine>;
type Locales = KeysFrom<typeof machine>;
type AllTranslations = TranslationsFrom<typeof machine>;
type EnglishTranslation = TranslationFrom<typeof machine>;
// Extract parameters for a specific key
type GreetingParams = _Params<Config, 'greetings'>;
// => { name: string; lastLoginDate: Date }Note: Properties prefixed with __ (like __key, __translation) are
internal and marked as deprecated. They exist only for type inference and
should not be used at runtime.
Advanced Usage
If you want to re-use the machine’s internal types, utility types are
provided:
```ts
import type {
ConfigFrom,
KeysFrom,
KeyFrom, // @deprecated – internal typing only
TranslationsFrom,
TranslationFrom, // @deprecated – internal typing only
} from '@bemedev/i18n/class';Note: properties prefixed by ** (like **key, \_\_translation) are
marked as deprecated and exist only to carry types. Don’t use them at
runtime.
Advanced Usage
Creating translations from existing configurations
If you want to create a new translation based on an existing configuration
without copying the entire object, use the translation helper:
Basic usage with translation
import { translation } from '@bemedev/i18n';
const translate = translation(
{
greeting: 'Hello',
farewell: 'Goodbye',
withParam: 'Hello {name}!',
nested: {
welcome: 'Welcome back',
deep: {
message: 'Deep message',
},
},
},
'en',
);
// Simple translations
translate('greeting'); // => 'Hello'
translate('farewell'); // => 'Goodbye'
// With parameters
translate('withParam', { name: 'John' }); // => 'Hello John!'
// Nested keys
translate('nested.welcome'); // => 'Welcome back'
translate('nested.deep.message'); // => 'Deep message'Using translation.derived for type-safe derived translations
Create translations that derive from an existing configuration while maintaining full type safety:
import { translation } from '@bemedev/i18n';
import { machine } from './your-base-config';
const spanishTranslation = translation.derived<typeof machine.config>(
dt => ({
localee: 'es-ES',
greetings: dt(
'¡Hola {name}! Tu última conexión fue el {lastLoginDate:date}.',
{
date: {
lastLoginDate: {
month: '2-digit',
year: 'numeric',
day: '2-digit',
},
},
},
),
nested: {
greetings: dt('¡Hola {names:list}!'),
},
}),
'es-ES',
);
// Fully typed translations
spanishTranslation('greetings', {
name: 'Juan',
lastLoginDate: new Date('2024-06-15T12:00:00Z'),
});
// => '¡Hola Juan! Tu última conexión fue el 15/06/2024.'Using translation.fromMachine for machine-based translations
Create translations directly from an existing i18n machine:
import { translation } from '@bemedev/i18n';
import { machine } from './your-machine';
const germanTranslation = translation.fromMachine<typeof machine>(
dt => ({
localee: 'de-DE',
greetings: dt(
'Hallo {name}! Deine letzte Anmeldung war {lastLoginDate:date}.',
),
// ... other translations
}),
'de-DE',
);Accessing the configuration
The translation function returns an object with a config property that
exposes the underlying configuration:
const translate = translation({ greeting: 'Hello' }, 'en');
console.log(translate.config); // => { greeting: 'Hello' }Licence
MIT
CHANGE_LOG
Acknowledgements
Special thanks to all contributors, testers, and users who provided feedback and helped improve this library. Your support and suggestions are greatly appreciated!
This was inspired by Web Dev Simplified
The Youtube video that inspired this library can be found here.
Author
chlbri ([email protected])
