Skip to main content

Internationalization

i18next + react-i18next

Out of the box. i18next@26.3.6, react-i18next@17.0.11.

i18next is the de-facto JS i18n core — resources, interpolation, plurals, namespaces, backends — and react-i18next binds it to React with useTranslation, <Trans> for translations containing markup, and provider-driven re-render on language change.

The core has no DOM in it and react-i18next renders whatever elements you hand it, so the whole stack runs unmodified: interpolation, count-based plurals, <Trans>, and i18n.changeLanguage('de') re-rendering the mounted tree in place. Node ships full ICU, so Intl.NumberFormat and Intl.DateTimeFormat formatting work too.

Two react-x11-specific rules:

  1. <Trans> component maps must use react-x11 elements. components={{ 1: <span/> }} throws react-x11: <span> is not allowed inside <text> — inside a <text>, only strings and nested <text> spans are legal. Use <text style={...}/> as the wrapper.
  2. Turn off HTML escaping. i18next escapes interpolated values for an HTML sink by default, and react-x11 draws raw glyphs, so O'Brien renders as O&#39;Brien. Set interpolation: { escapeValue: false }.
import React from 'react';
import i18next from 'i18next';
import {
I18nextProvider,
initReactI18next,
useTranslation,
Trans,
} from 'react-i18next';

const i18n = i18next.createInstance();
await i18n.use(initReactI18next).init({
lng: 'en',
resources: {
en: {
translation: {
greeting: 'Hello, {{name}}!',
items_one: '{{count}} item',
items_other: '{{count}} items',
rich: 'Press <1>Enter</1> to save',
},
},
},
interpolation: { escapeValue: false }, // no HTML sink to escape for
});

function Status() {
const { t } = useTranslation();
return (
<box style={{ flexDirection: 'column' }}>
<text>{t('greeting', { name: 'X11' })}</text>
<text>{t('items', { count: 2 })}</text>
<text>
<Trans
i18nKey="rich"
components={{ 1: <text style={{ color: '#e00' }} /> }}
/>
</text>
</box>
);
}

// mount under <I18nextProvider i18n={i18n}> inside your <window>;
// i18n.changeLanguage('de') re-renders live

For loading translations from disk, i18next-fs-backend is the natural desktop choice; i18next-http-backend also works, since Node has fetch.

What is missing below the string

These are react-x11 gaps rather than i18next ones, and they matter more than the library choice:

  • Layout is computed left-to-right unconditionally. react-x11 calls yoga's calculateLayout with DIRECTION_LTR and there is no direction style property, so an Arabic or Hebrew UI will mirror wrong even though every string translates correctly. Yoga supports DIRECTION_RTL; wiring it up is renderer work that has not been done.
  • There is no X input method. Input is limited to what the keyboard path produces, with no XIM integration, so languages that need composition — CJK, dead keys — can be displayed but not typed.
  • <Trans> with the default <strong>- and <em>-style numbered tags in translations is fine as long as every referenced component is a <text> span. An accidental DOM tag fails loudly at render, which is the good case.