All questions

i18n Engine

Premium

i18n Engine

An internationalization (i18n) engine turns a message key plus some parameters into a finished, human-readable string in the user's language — looking the key up in the active locale, filling in variables, and choosing the right plural form for a count. It is the core of libraries like i18next, FormatJS, and react-intl: the piece that decides cart.items with a count of 1 should read "1 item in your cart" while a count of 3 reads "3 items in your cart", and that a string missing in French quietly falls back to English instead of rendering blank.

You build i18nEngine(config), a factory that reads a nested table of messages and returns three methods: t to translate a key, setLocale to switch languages, and getLocale to read the current one.

Signature

type MessageValue =
  | string                                            // a finished message, maybe with {placeholders}
  | { one: string; other: string; zero?: string }     // a plural group
  | { [key: string]: MessageValue };                  // a nested table, addressed by dot-path

function i18nEngine(config: {
  locale: string;                          // the starting locale
  messages: Record<string, MessageValue>;  // locale -> nested message tree
  fallbackLocale?: string;                 // tried when a key is missing in `locale`
}): {
  t(key: string, params?: Record<string, unknown>): string; // resolve + interpolate
  setLocale(next: string): void;           // switch the active locale
  getLocale(): string;                     // read the active locale
};

Examples

const engine = i18nEngine({
  locale: 'en',
  fallbackLocale: 'en',
  messages: {
    en: { greeting: 'Hello, {name}!' },
    fr: { greeting: 'Bonjour, {name} !' },
  },
});

engine.t('greeting', { name: 'Sam' }); // 'Hello, Sam!'
engine.setLocale('fr');
engine.t('greeting', { name: 'Sam' }); // 'Bonjour, Sam !'
const engine = i18nEngine({
  locale: 'en',
  messages: {
    en: {
      cart: {
        items: {
          one: '{count} item in your cart',
          other: '{count} items in your cart',
        },
      },
    },
  },
});

engine.t('cart.items', { count: 1 }); // '1 item in your cart'
engine.t('cart.items', { count: 3 }); // '3 items in your cart'
engine.t('cart.missing');             // 'cart.missing' — no such key, so the key comes back

Notes

  • Keys are dot-pathst('cart.items') walks messages[locale].cart.items; a key with no matching path counts as not found.
  • Fallback, then the key — if a key is missing in the current locale, try fallbackLocale; if it is missing there too, return the key string itself so the interface is never blank.
  • Plurals pick a form by count — a resolved plural object like { one, other } chooses one when params.count is 1, zero when the count is 0 and a zero form exists, and other in every other case.
  • Interpolation fills placeholders — replace each {name} in the chosen string with params.name; {count} is always available from the plural count. A placeholder with no matching param stays literal — do not throw and do not insert undefined.
  • Do not worry aboutIntl.PluralRules locale-specific plural categories, date or number formatting, nested placeholders, or lazy message loading; strings, plurals, and single-level fallback are enough here.

FAQ

How does the engine decide which locale to use?
It resolves the dotted key in the current locale first; if the key is missing there it retries in the fallback locale, and if it is still missing it returns the key string itself so the UI never renders blank.
How is the plural form chosen?
From the count parameter: a count of one selects the one form, a count of zero selects the zero form when you provide one, and every other count selects the other form. The chosen string is then interpolated like any other message.
What happens when a placeholder has no matching parameter?
The placeholder is left in the output verbatim. The engine never throws and never inserts the word undefined, so a forgotten parameter degrades gracefully instead of breaking the render.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium