i18n
The i18n module provides reactive locale state, interpolation, pluralization, lazy locale loading, and Intl-based formatting. Locale changes propagate automatically through reactive translations.
import { createI18n, formatDate, formatNumber } from '@bquery/bquery/i18n';Creating an i18n Instance
createI18n()
Creates a reactive internationalization instance that manages translations, locale switching, lazy loading, and Intl-based formatting.
function createI18n(config: I18nConfig): I18nInstance;I18nConfig
type I18nConfig = {
/** The initial locale. */
locale: string;
/** Translation messages keyed by locale. */
messages: Messages;
/** Fallback locale when a key is missing in the active locale. */
fallbackLocale?: string;
};Messages
type Messages = {
[locale: string]: LocaleMessages;
};
type LocaleMessages = {
[key: string]: string | LocaleMessages;
};Messages support nested keys. Nested messages can be accessed with dot notation (e.g., 'nav.home').
Example
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: {
en: {
greeting: 'Hello, {name}!',
items: '{count} item | {count} items',
nav: {
home: 'Home',
about: 'About',
},
},
de: {
greeting: 'Hallo, {name}!',
items: '{count} Eintrag | {count} Einträge',
nav: {
home: 'Startseite',
about: 'Über uns',
},
},
},
});The I18nInstance API
I18nInstance
interface I18nInstance {
/** Reactive locale signal. Assign to switch locales. */
$locale: Signal<string>;
/** Translate a message key with optional parameters. */
t: (key: string, params?: TranslateParams) => string;
/** Reactive translation — returns a computed signal that updates on locale change. */
tc: (key: string, params?: TranslateParams) => ReadonlySignal<string>;
/** Register a lazy-loader for a locale. */
loadLocale: (locale: string, loader: LocaleLoader) => void;
/** Trigger the lazy-load for a locale and wait for it to complete. */
ensureLocale: (locale: string) => Promise<void>;
/** Locale-aware number formatting. */
n: (value: number, options?: NumberFormatOptions) => string;
/** Locale-aware date formatting. */
d: (value: Date | number, options?: DateFormatOptions) => string;
/** Get all messages for a specific locale. */
getMessages: (locale: string) => LocaleMessages | undefined;
/** Deep-merge additional messages into a locale. */
mergeMessages: (locale: string, messages: LocaleMessages) => void;
/** List all locales that have messages loaded. */
availableLocales: () => string[];
}Translation
t() — Static Translation
Translates a message key using the current locale. Supports parameter interpolation and pluralization.
type TranslateParams = Record<string, string | number>;// Simple translation
i18n.t('greeting', { name: 'Ada' }); // 'Hello, Ada!'
// Nested key access
i18n.t('nav.home'); // 'Home'
// Pluralization (pipe-separated)
i18n.t('items', { count: 1 }); // '1 item'
i18n.t('items', { count: 5 }); // '5 items'
i18n.t('items', { count: 0 }); // '0 items'Pluralization rules: Messages with | are split into forms. The count parameter determines which form is selected:
- 2 forms (
one | many):count === 1→ first form; otherwise → second form - 3 forms (
zero | one | many):count === 0→ first form;count === 1→ second form; otherwise → third form - More than 3 forms:
count === 0→ first form;count === 1→ second form; otherwise → last form
tc() — Reactive Translation
Returns a ReadonlySignal<string> that automatically updates when the locale changes. Use this in effects, computed values, or view bindings.
const greeting = i18n.tc('greeting', { name: 'Ada' });
console.log(greeting.value); // 'Hello, Ada!'
// Changing locale updates the translation reactively
i18n.$locale.value = 'de';
console.log(greeting.value); // 'Hallo, Ada!'Usage with effects:
import { effect } from '@bquery/bquery/reactive';
const title = i18n.tc('nav.home');
effect(() => {
document.title = title.value;
});
// When the locale changes, the document title updates automatically
i18n.$locale.value = 'de'; // document.title → 'Startseite'Locale Management
$locale — Reactive Locale Signal
The $locale property is a writable Signal<string>. Assigning a new value switches the active locale and triggers all reactive translations.
console.log(i18n.$locale.value); // 'en'
i18n.$locale.value = 'de';
// All tc() computed values now recomputeloadLocale() — Register a Lazy Loader
Registers a loader function for a locale that hasn't been loaded yet. The loader is only invoked when ensureLocale() is called.
type LocaleLoader = () => Promise<LocaleMessages | { default: LocaleMessages }>;i18n.loadLocale('fr', () => import('./locales/fr.json'));
i18n.loadLocale('ja', () => import('./locales/ja.json'));ensureLocale() — Trigger Lazy Loading
Triggers the lazy-load for a locale and returns a Promise that resolves when the messages are ready. Repeated calls for the same locale are cached — the loader runs only once.
await i18n.ensureLocale('fr');
i18n.$locale.value = 'fr'; // Now safe to useFull lazy-loading workflow:
// 1. Register loaders at startup
i18n.loadLocale('fr', () => import('./locales/fr.json'));
i18n.loadLocale('ja', () => import('./locales/ja.json'));
// 2. When user selects a new locale
async function switchLocale(locale: string) {
await i18n.ensureLocale(locale);
i18n.$locale.value = locale;
}
await switchLocale('fr');getMessages()
Returns all messages for a specific locale, or undefined if the locale hasn't been loaded.
const enMessages = i18n.getMessages('en');
// { greeting: 'Hello, {name}!', items: '...', nav: { home: 'Home', about: 'About' } }
const unknownMessages = i18n.getMessages('xx');
// undefinedmergeMessages()
Deep-merges additional messages into an existing locale. Useful for plugins or feature modules that add their own translation keys.
i18n.mergeMessages('en', {
settings: {
title: 'Settings',
theme: 'Theme',
},
});
i18n.t('settings.title'); // 'Settings'availableLocales()
Returns an array of all locales that currently have messages loaded.
console.log(i18n.availableLocales()); // ['en', 'de']Number and Date Formatting
n() — Locale-Aware Number Formatting
Formats a number using Intl.NumberFormat and the current locale.
i18n.n(1234.56);
// 'en' → '1,234.56'
// 'de' → '1.234,56'
i18n.n(0.756, { style: 'percent' });
// 'en' → '76%'
i18n.n(99.99, { style: 'currency', currency: 'EUR' });
// 'de' → '99,99 €'d() — Locale-Aware Date Formatting
Formats a date using Intl.DateTimeFormat and the current locale.
i18n.d(new Date('2026-03-26'));
// 'en' → '3/26/2026'
// 'de' → '26.3.2026'
i18n.d(new Date(), { dateStyle: 'long' });
// 'en' → 'March 26, 2026'
// 'de' → '26. März 2026'
i18n.d(new Date(), { dateStyle: 'full', timeStyle: 'short' });
// 'en' → 'Thursday, March 26, 2026, 2:30 PM'Standalone Formatting Helpers
These functions are available without creating an i18n instance. They accept an explicit locale parameter.
formatNumber()
function formatNumber(value: number, locale: string, options?: NumberFormatOptions): string;import { formatNumber } from '@bquery/bquery/i18n';
formatNumber(1234.56, 'en-US');
// '1,234.56'
formatNumber(1234.56, 'de-DE');
// '1.234,56'
formatNumber(0.85, 'en-US', { style: 'percent' });
// '85%'formatDate()
function formatDate(value: Date | number, locale: string, options?: DateFormatOptions): string;import { formatDate } from '@bquery/bquery/i18n';
formatDate(new Date('2026-03-26'), 'en-US');
// '3/26/2026'
formatDate(new Date('2026-03-26'), 'de-DE', { dateStyle: 'long' });
// '26. März 2026'Type Definitions
NumberFormatOptions
type NumberFormatOptions = Intl.NumberFormatOptions & {
/** Override the locale for this specific formatting call. */
locale?: string;
};DateFormatOptions
type DateFormatOptions = Intl.DateTimeFormatOptions & {
/** Override the locale for this specific formatting call. */
locale?: string;
};Full Example
import { createI18n } from '@bquery/bquery/i18n';
import { effect } from '@bquery/bquery/reactive';
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: {
en: {
welcome: 'Welcome, {name}!',
items: '{count} item | {count} items',
nav: { home: 'Home', settings: 'Settings' },
},
de: {
welcome: 'Willkommen, {name}!',
items: '{count} Eintrag | {count} Einträge',
nav: { home: 'Startseite', settings: 'Einstellungen' },
},
},
});
// Register lazy locale
i18n.loadLocale('fr', () => import('./locales/fr.json'));
// Static translations
console.log(i18n.t('welcome', { name: 'Ada' })); // 'Welcome, Ada!'
console.log(i18n.t('items', { count: 3 })); // '3 items'
console.log(i18n.t('nav.home')); // 'Home'
// Reactive translations
const title = i18n.tc('nav.home');
effect(() => {
document.title = title.value; // Updates when locale changes
});
// Number and date formatting
console.log(i18n.n(42000)); // '42,000'
console.log(i18n.d(new Date(), { dateStyle: 'long' })); // 'March 26, 2026'
// Switch locale
i18n.$locale.value = 'de';
console.log(i18n.t('welcome', { name: 'Ada' })); // 'Willkommen, Ada!'Notes
- Messages support nested keys accessed with dot notation.
- Repeated locale loads are cached — loaders only execute once.
- Deep message merges are hardened against prototype-pollution keys (
__proto__,constructor,prototype). - The fallback locale is used when a key is missing in the active locale.
tc()returns a computed signal, so it re-evaluates only when the locale actually changes.