Skip to content

Internationalization

The Lens frontend uses react-intl v8.1.3 (FormatJS / ICU MessageFormat) for internationalization. All user-visible strings must use translation keys.

CodeLanguageNotes
enEnglishSource of truth — all keys must exist here first
esSpanish
frFrench
deGerman
jaJapaneseCJK — identical singular/plural forms
pt-BRPortuguese (Brazil)
zh-CNChinese (Simplified)CJK — identical singular/plural forms
koKoreanCJK — identical singular/plural forms
itItalian
ruRussian3 plural forms (one/few/other)
nlDutch
src/i18n/
├── index.ts # detectLocale(), loadMessages(), re-exports en
├── IntlProvider.tsx # React wrapper with lazy loading
├── en.ts # English messages (source of truth)
├── es.ts # Spanish
├── fr.ts # French
├── de.ts # German
├── ja.ts # Japanese
├── pt-BR.ts # Portuguese (Brazil)
├── zh-CN.ts # Chinese (Simplified)
├── ko.ts # Korean
├── it.ts # Italian
├── ru.ts # Russian
└── nl.ts # Dutch

IntlProvider.tsx wraps the app in react-intl’s IntlProvider:

src/i18n/IntlProvider.tsx
export default function IntlProvider({ children }) {
const [locale, setLocale] = useState('en');
const [messages, setMessages] = useState(en);
useEffect(() => {
const detected = detectLocale();
setLocale(detected);
loadMessages(detected).then(setMessages);
}, []);
return (
<ReactIntlProvider locale={locale} messages={messages} defaultLocale="en">
{children}
</ReactIntlProvider>
);
}

The provider is mounted in App.tsx as the second layer (after ErrorBoundary, before FeatureFlagProvider).

src/i18n/index.ts
export function detectLocale(): SupportedLocale {
const nav = navigator.language || 'en';
// Exact match first (e.g., pt-BR, zh-CN)
if (localeModules[nav]) return nav;
// Language-only prefix (e.g., 'fr-CA' → 'fr')
const lang = nav.split('-')[0];
if (lang === 'en') return 'en';
if (localeModules[lang]) return lang;
return 'en';
}

Detection reads navigator.language and maps it to the closest supported locale. Falls back to English if no match.

Non-English locale files are dynamically imported to keep the initial bundle small:

const localeModules = {
es: () => import('./es'),
fr: () => import('./fr'),
de: () => import('./de'),
ja: () => import('./ja'),
'pt-BR': () => import('./pt-BR'),
'zh-CN': () => import('./zh-CN'),
ko: () => import('./ko'),
it: () => import('./it'),
ru: () => import('./ru'),
nl: () => import('./nl'),
};
export async function loadMessages(locale: string): Promise<Record<string, string>> {
if (locale === 'en' || !localeModules[locale]) return en;
const mod = await localeModules[locale]();
return { ...en, ...mod.default }; // Non-English merges OVER English
}

The merge ({ ...en, ...mod.default }) ensures that any missing keys in a non-English locale fall back to the English string rather than showing a raw key ID.

import { useIntl } from 'react-intl';
function MyComponent() {
const intl = useIntl();
return (
<div>
<h1>{intl.formatMessage({ id: 'myComponent.title' })}</h1>
<p>{intl.formatMessage({ id: 'myComponent.description' })}</p>
</div>
);
}
intl.formatMessage(
{ id: 'lensView.itemCount' },
{ count: items.length }
)

In en.ts:

'lensView.itemCount': '{count, plural, one {# item} other {# items}}'

Standard (English, most languages — 2 forms):

'{count, plural, one {# item} other {# items}}'

Russian (3 forms — one/few/other):

'{count, plural, one {# элемент} few {# элемента} other {# элементов}}'

CJK languages (Japanese, Chinese, Korean — identical forms):

'{count, plural, one {#個のアイテム} other {#個のアイテム}}'

CJK locales still require ICU plural syntax even though the forms are identical. This ensures the formatting engine processes them correctly.

  1. Add the key to en.ts with a dot-delimited key organized by component:
src/i18n/en.ts
const en: Record<string, string> = {
// ... existing keys ...
// MyFeature
'myFeature.title': 'My Feature',
'myFeature.description': 'This is a new feature',
'myFeature.itemCount': '{count, plural, one {# item} other {# items}}',
};
  1. Use useIntl() in the component:
const intl = useIntl();
const title = intl.formatMessage({ id: 'myFeature.title' });
const count = intl.formatMessage({ id: 'myFeature.itemCount' }, { count: 5 });
  1. Add translations to ALL 10 non-English locale files:

    • es.ts, fr.ts, de.ts, ja.ts, pt-BR.ts, zh-CN.ts, ko.ts, it.ts, ru.ts, nl.ts
  2. Run the i18n coverage test:

Terminal window
cd foundation/static/main && npm test -- --testPathPattern=i18n-coverage

This test verifies key parity across all locale files — every key in en.ts must exist in every other locale file.

Keys use dot-delimited component paths:

<component>.<element>

Examples:

'lensCard.renameLens' // LensList card rename action
'dialog.cancel' // Shared dialog cancel button
'toolbar.addIssue' // Toolbar add issue button
'inspector.columnsSection' // Inspector columns section title
'grid.noData' // Grid empty state
'gantt.zoomIn' // Gantt zoom control

Keys are organized in en.ts with comment headers by section:

// ---- Toolbar ----
'toolbar.addIssue': 'Add Issue',
'toolbar.addFlexItem': 'Add Flex Item',
// ... more toolbar keys
// ---- Inspector ----
'inspector.title': 'Inspector',
// ... more inspector keys

setupTests.ts provides a global mock for react-intl that returns English strings:

// The mock handles:
// 1. Simple keys: {id: 'key'} → returns en[key]
// 2. Variable interpolation: {key} → replaced with provided values
// 3. ICU plurals: {count, plural, ...} → resolved to correct plural form

Tests assert on the English text that the mock returns:

expect(screen.getByText('Change Status')).toBeInTheDocument();
// NOT: expect(screen.getByText('grid.changeStatus'))

The i18n-coverage test:

  1. Imports all locale files
  2. Extracts all keys from en.ts
  3. Verifies every key exists in every locale file
  4. Reports missing keys with the locale and key name

Two places are exempt from i18n requirements:

  1. ErrorBoundary (App.tsx) — renders before IntlProvider mounts, so useIntl() is unavailable. Uses hardcoded English: "Something went wrong".

  2. Pre-IntlProvider loading states — the initial loading spinner before IntlProvider resolves locale detection. Uses hardcoded "Loading...".

  • Never hardcode user-visible strings — use intl.formatMessage({ id: '...' })
  • English (en.ts) is the source of truth — add keys here first
  • Every new key must be added to all 11 locale files
  • Russian uses 3 plural forms — never use 2-form plurals for Russian
  • CJK locales need ICU plural syntax even with identical forms
  • Keep keys organized by component section with comment headers
  • Run i18n-coverage test before committing new strings