Internationalization
The Lens frontend uses react-intl v8.1.3 (FormatJS / ICU MessageFormat) for internationalization. All user-visible strings must use translation keys.
Supported Locales
Section titled “Supported Locales”| Code | Language | Notes |
|---|---|---|
en | English | Source of truth — all keys must exist here first |
es | Spanish | |
fr | French | |
de | German | |
ja | Japanese | CJK — identical singular/plural forms |
pt-BR | Portuguese (Brazil) | |
zh-CN | Chinese (Simplified) | CJK — identical singular/plural forms |
ko | Korean | CJK — identical singular/plural forms |
it | Italian | |
ru | Russian | 3 plural forms (one/few/other) |
nl | Dutch |
Architecture
Section titled “Architecture”File Structure
Section titled “File Structure”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 # DutchIntlProvider
Section titled “IntlProvider”IntlProvider.tsx wraps the app in react-intl’s IntlProvider:
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).
Locale Detection
Section titled “Locale Detection”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.
Lazy Loading
Section titled “Lazy Loading”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.
Using Translations in Components
Section titled “Using Translations in Components”Basic Usage
Section titled “Basic Usage”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> );}With Variables
Section titled “With Variables”intl.formatMessage( { id: 'lensView.itemCount' }, { count: items.length })In en.ts:
'lensView.itemCount': '{count, plural, one {# item} other {# items}}'ICU Plural Syntax
Section titled “ICU Plural Syntax”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.
Adding New Strings (Checklist)
Section titled “Adding New Strings (Checklist)”- Add the key to
en.tswith a dot-delimited key organized by component:
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}}',};- Use
useIntl()in the component:
const intl = useIntl();const title = intl.formatMessage({ id: 'myFeature.title' });const count = intl.formatMessage({ id: 'myFeature.itemCount' }, { count: 5 });-
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
-
Run the i18n coverage test:
cd foundation/static/main && npm test -- --testPathPattern=i18n-coverageThis test verifies key parity across all locale files — every key in en.ts must exist in every other locale file.
Key Naming Convention
Section titled “Key Naming Convention”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 controlKeys 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 keysTesting
Section titled “Testing”Global Mock
Section titled “Global Mock”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 formTests assert on the English text that the mock returns:
expect(screen.getByText('Change Status')).toBeInTheDocument();// NOT: expect(screen.getByText('grid.changeStatus'))i18n Coverage Test
Section titled “i18n Coverage Test”The i18n-coverage test:
- Imports all locale files
- Extracts all keys from
en.ts - Verifies every key exists in every locale file
- Reports missing keys with the locale and key name
Exceptions
Section titled “Exceptions”Two places are exempt from i18n requirements:
-
ErrorBoundary (
App.tsx) — renders beforeIntlProvidermounts, souseIntl()is unavailable. Uses hardcoded English:"Something went wrong". -
Pre-IntlProvider loading states — the initial loading spinner before
IntlProviderresolves 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-coveragetest before committing new strings