@molecule/api-i18n
Core interface · i18n · API (Node) · v1.0.3 · Apache-2.0
Multi-language support with translations
npm install @molecule/api-i18nHow it works
@molecule/api-i18n is the i18n core interface on the API (Node) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 1 provider: @molecule/api-i18n-simple.
import { registerLocaleModule, t } from '@molecule/api-i18n'
import * as locales from '@molecule/api-locales-user'
// Startup: register a companion locale bond (all 79 locales in one call)
registerLocaleModule(locales)
// Error responses — default locale
t('user.error.notFound', undefined, { defaultValue: 'User not found.' })
// Per-user content (emails, notifications) — per-request locale OPTION
t(
'user.email.resetSubject',
{ appName },
{ locale: user.locale, defaultValue: '{{appName}} password reset' },
)Providers (1): @molecule/api-i18n-simple
Works with: @molecule/api-bond
Reference
Internationalization (i18n) interface for molecule.dev API.
Mirrors @molecule/app-i18n for the server side: t() translations with
{{variable}} interpolation, locale bond registration, and number/date
formatting. Works with ZERO wiring — if no provider is bonded, a simple
in-memory provider ('en' default) is auto-created on first use, and t()
falls back to defaultValue (or the key itself), so untranslated strings
never crash a request.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { registerLocaleModule, t } from '@molecule/api-i18n'
import * as locales from '@molecule/api-locales-user'
// Startup: register a companion locale bond (all 79 locales in one call)
registerLocaleModule(locales)
// Error responses — default locale
t('user.error.notFound', undefined, { defaultValue: 'User not found.' })
// Per-user content (emails, notifications) — per-request locale OPTION
t(
'user.email.resetSubject',
{ appName },
{ locale: user.locale, defaultValue: '{{appName}} password reset' },
)
Type
core
Installation
npm install @molecule/api-i18n @molecule/api-bond
API
Interfaces
Translations
Translation key/value map.
interface Translations {
[key: string]: string | Translations
}
PluralRule
Plural form resolution rule mapping a count to a translation category (zero, one, two, few, many, other).
interface PluralRule {
/**
* Zero count text.
*/
zero?: string
/**
* One count text.
*/
one?: string
/**
* Two count text.
*/
two?: string
/**
* Few count text (for some languages).
*/
few?: string
/**
* Many count text.
*/
many?: string
/**
* Other count text (default).
*/
other: string
}
LocaleConfig
Server-side locale configuration (code, display name, text direction, translations or loader).
interface LocaleConfig {
/**
* Locale code (e.g., 'en', 'fr', 'zh-TW').
*/
code: string
/**
* Display name (e.g., 'English', 'Francais').
*/
name: string
/**
* Native display name.
*/
nativeName?: string
/**
* Text direction.
*/
direction?: 'ltr' | 'rtl'
/**
* Translations for this locale.
*/
translations?: Translations
}
NumberFormatOptions
Number format options.
interface NumberFormatOptions {
/**
* Number style.
*/
style?: 'decimal' | 'currency' | 'percent' | 'unit'
/**
* Currency code (for currency style).
*/
currency?: string
/**
* Minimum fraction digits.
*/
minimumFractionDigits?: number
/**
* Maximum fraction digits.
*/
maximumFractionDigits?: number
/**
* Use grouping separators.
*/
useGrouping?: boolean
}
DateFormatOptions
Date format options.
interface DateFormatOptions {
/**
* Date style.
*/
dateStyle?: 'full' | 'long' | 'medium' | 'short'
/**
* Time style.
*/
timeStyle?: 'full' | 'long' | 'medium' | 'short'
/**
* Custom format string (implementation-specific).
*/
format?: string
/**
* Relative time.
*/
relative?: boolean
}
I18nProvider
i18n provider interface.
All i18n providers must implement this interface.
interface I18nProvider {
/**
* Gets the current locale.
*/
getLocale(): string
/**
* Sets the current locale.
*/
setLocale(locale: string): void
/**
* Gets all available locales.
*/
getLocales(): LocaleConfig[]
/**
* Adds a locale.
*/
addLocale(config: LocaleConfig): void
/**
* Adds translations to a locale. Auto-creates the locale if it doesn't exist.
*/
addTranslations(locale: string, translations: Translations, namespace?: string): void
/**
* Translates a key.
*/
t(
key: string,
values?: InterpolationValues,
options?: { defaultValue?: string; count?: number; locale?: string },
): string
/**
* Checks if a translation exists.
*/
exists(key: string): boolean
/**
* Formats a number.
*/
formatNumber(value: number, options?: NumberFormatOptions): string
/**
* Formats a date.
*/
formatDate(value: Date | number | string, options?: DateFormatOptions): string
/**
* Formats a relative time (e.g., "2 hours ago").
*/
formatRelativeTime(value: Date | number, options?: { unit?: Intl.RelativeTimeFormatUnit }): string
/**
* Formats a list (e.g., "A, B, and C").
*/
formatList(values: string[], options?: { type?: 'conjunction' | 'disjunction' | 'unit' }): string
/**
* Gets the text direction for the current locale.
*/
getDirection(): 'ltr' | 'rtl'
}
Types
InterpolationValues
Key-value map of interpolation variables for server-side translation strings.
type InterpolationValues = Record<string, string | number | boolean | Date>
TranslateFunction
Translate Function type.
type TranslateFunction = (
key: string,
values?: InterpolationValues,
options?: { defaultValue?: string; count?: number; locale?: string },
) => string
TranslateOptions
Translate Options type.
type TranslateOptions = { defaultValue?: string; count?: number; locale?: string }
Functions
getPluralForm(count, locale)
Returns the CLDR plural category ('zero', 'one', 'two', 'few', 'many',
or 'other') for a given count and locale using Intl.PluralRules.
function getPluralForm(count: number, locale: string): string
count— The numeric count to determine the plural form for.locale— The locale code to use for plural rule selection (e.g.'en','ar').
Returns: The plural category string.
createSimpleI18nProvider(defaultLocale)
Creates a simple in-memory i18n provider with translation lookup, interpolation,
Intl-based number/date/relative-time formatting, and RTL detection.
Used as the default provider when no bond package is installed.
function createSimpleI18nProvider(defaultLocale?: string): I18nProvider
defaultLocale— The initial locale code (defaults to'en').
Returns: A fully functional I18nProvider backed by in-memory translation maps.
setProvider(provider)
Registers an i18n provider as the active singleton. Called by bond packages during application startup.
function setProvider(provider: I18nProvider): void
provider— The i18n provider implementation to bond.
getProvider()
Retrieves the bonded i18n provider. If none is bonded, auto-creates and
bonds a simple in-memory provider with 'en' as the default locale.
function getProvider(): I18nProvider
Returns: The bonded i18n provider.
hasProvider()
Checks whether an i18n provider is currently bonded.
function hasProvider(): boolean
Returns: true if an i18n provider is bonded.
getLocale()
Returns the current locale code (e.g. 'en', 'fr', 'zh-TW').
function getLocale(): string
Returns: The active locale code.
setLocale(locale)
Sets the active locale. Throws if the locale hasn't been registered.
function setLocale(locale: string): void
locale— The locale code to switch to (e.g.'fr','zh-TW').
Returns: Nothing.
addLocale(config)
Registers a locale configuration (code, display name, direction, translations).
function addLocale(config: LocaleConfig): void
config— The locale configuration to add.
Returns: Nothing.
addTranslations(locale, translations, namespace)
Adds translation key-value pairs for a locale. Auto-creates the locale if it doesn't already exist. Translations are deep-merged with existing ones.
function addTranslations(locale: string, translations: Translations, namespace?: string): void
locale— The locale code to add translations for (e.g.'en','fr').translations— The translation key-value map to merge.namespace— Optional namespace prefix to nest translations under.
Returns: Nothing.
t(key, values, options)
Translates a key using the bonded i18n provider. Falls back to defaultValue or
the key itself if no translation is found. Supports interpolation via {{variable}}
syntax and per-request locale override.
On the API side, use the locale option for per-request translation
(e.g., emails, notifications) to avoid global state race conditions:
// Error responses — use default locale (English)
t('user.error.usernameRequired')
// Emails — translate in user's locale
t('user.email.resetSubject', { appName }, { locale: userLocale })
function t(
key: string,
values?: InterpolationValues,
options?: { defaultValue?: string; count?: number; locale?: string },
): string
key— The translation key (dot-notation, e.g.'user.error.notFound').values— Optional interpolation values to substitute{{variable}}placeholders.options— Optional settings.options.defaultValue— Fallback string if no translation is found.options.count— Count for pluralization.options.locale— Override locale for this specific translation.
Returns: The translated string, or defaultValue, or the key if nothing matches.
formatNumber(value, options)
Formats a number according to the current locale using Intl.NumberFormat.
function formatNumber(value: number, options?: NumberFormatOptions): string
value— The number to format.options— Formatting options (style, currency, fraction digits, grouping).
Returns: The locale-formatted number string.
formatDate(value, options)
Formats a date according to the current locale using Intl.DateTimeFormat.
function formatDate(value: string | number | Date, options?: DateFormatOptions): string
value— The date to format (Date object, timestamp, or ISO string).options— Formatting options (dateStyle, timeStyle, relative).
Returns: The locale-formatted date string.
formatRelativeTime(value, options)
Formats a relative time string (e.g. "2 hours ago", "in 3 days") using
Intl.RelativeTimeFormat.
function formatRelativeTime(
value: number | Date,
options?: { unit?: Intl.RelativeTimeFormatUnit },
): string
value— The date or timestamp to express relative to now.options— Optional settings;unitforces the difference to be expressed in that unit.
Returns: The locale-formatted relative time string.
registerLocaleModule(moduleExports)
Registers all locale exports from a locale bond module. Iterates the module's
named exports, treating each object-valued export as a translation map keyed
by locale code (e.g. en, fr, zhTW). Handles zhTW → zh-TW mapping.
function registerLocaleModule(moduleExports: Record<string, unknown>): void
moduleExports— The module's named exports (e.g.{ en: {...}, fr: {...} }).
getNestedValue(obj, key)
Resolves a dot-notation key (e.g. 'auth.login.email') against a nested
translations object. Checks for a flat key match first, then traverses
the nested structure.
function getNestedValue(obj: Translations, key: string): string | undefined
obj— The translations object to search.key— The dot-notation key to resolve.
Returns: The translation string if found, or undefined.
interpolate(text, values)
Replaces {{variable}} placeholders in a translation string with their
corresponding values. Date values are formatted with toLocaleDateString().
function interpolate(text: string, values: InterpolationValues): string
text— The translation string containing{{variable}}placeholders.values— Key-value map of interpolation values.
Returns: The string with placeholders replaced by their values.
Constants
simpleProvider
Default simple in-memory i18n provider instance with 'en' locale.
Used as a fallback when no bond package provides a provider.
const simpleProvider: I18nProvider
Available Providers
| Provider | Package |
|---|---|
| Simple | @molecule/api-i18n-simple |
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1
Runtime Dependencies
-
@molecule/api-bond -
Never call
setLocale()per request on the server. The active locale is PROCESS-GLOBAL shared state — switching it for one user's email races every concurrent request. Pass the per-user locale in the third argument instead:t(key, values, { locale }).setLocale()is for single-locale deployments. -
Always pass
{ defaultValue }: it's the rendered English fallback (and what shows if a key is missing — otherwise users see the raw dot-notation key). Keys are namespaced dot-paths ('user.error.notFound'), never English text. -
Translations live in companion locale bond packages, not inline in features:
registerLocaleModule(moduleExports)registers every exported locale at once (export names likezhTWare normalized tozh-TW);addTranslations(locale, map, namespace?)deep-merges (later wins). -
Pluralization requires a real bonded provider.
{ count }resolves CLDR plural-suffixed keys (key_one/key_other/ …) in providers that implement it (e.g.@molecule/api-i18n-simple); the auto-created fallback provider IGNOREScount. -
formatNumber/formatDatehave NO per-call locale override — they format in the current global locale.
E2E Tests
Integration checklist — drive the real flow (no mocks), adapt each item to this app's actual localized responses/emails, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- A user-facing server string (an API error message, an email
subject/body, a notification) renders TRANSLATED for a non-default locale:
set the user's locale (or send Accept-Language) and confirm the
response/email comes back in that language, not English — passed per-request
as
t(key, values, { locale }), NEVER a process-globalsetLocale()per request (that races concurrent users and localizes the wrong one). - A missing/untranslated key falls back to its
defaultValue(rendered English), NOT the raw dot-notation key — a response or email showinguser.error.notFoundverbatim is exactly the bug this prevents. -
{{variable}}interpolation fills correctly — the appName/count/etc. appear in the message and no literal{{appName}}leaks through. - If the app pluralizes,
{ count }resolves the right CLDR form (one/other/…) — and ONLY with a real bonded provider (the auto fallback ignores count), so "1 item" vs "2 items" reads correctly. - The locale is derived per-request from the authenticated user's preference (or Accept-Language), so two concurrent users with different locales each get their own language — one user's locale never leaks into another's email/response.