← All @molecule/* packages · App templates

@molecule/api-i18n

Core interface · i18n · API (Node) · v1.0.1 · Apache-2.0

Multi-language support with translations

npm install @molecule/api-i18n

npm · Source on GitHub

How 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

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.ts JSDoc, not this file.

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.

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

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'
}

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
}

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
}

Translations

Translation key/value map.

interface Translations {
  [key: string]: string | Translations
}

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

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.

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.

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.

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.

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; unit forces the difference to be expressed in that unit.

Returns: The locale-formatted relative time string.

getLocale()

Returns the current locale code (e.g. 'en', 'fr', 'zh-TW').

function getLocale(): string

Returns: The active locale code.

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.

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.

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.

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.

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 zhTWzh-TW mapping.

function registerLocaleModule(moduleExports: Record<string, unknown>): void
  • moduleExports — The module's named exports (e.g. { en: {...}, fr: {...} }).

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.

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.

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.

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

ProviderPackage
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 like zhTW are normalized to zh-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 IGNORES count.

  • formatNumber/formatDate have 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-global setLocale() 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 showing user.error.notFound verbatim 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.