i18n Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Externalize all user-visible copy: Literals in JSX block translators, break plural rules, and prevent RTL truncation QA - use t() from day one.
One key per complete phrase: Never concatenate t("welcome") + name + t("exclaim") - word order differs in Arabic, Japanese, and German.
Context-aware keys: Duplicate English homographs (close dialog vs close account) need distinct keys - translators cannot guess from "close".
English JSON is the source locale: Ship en changes in the same PR as new t() calls - TMS uploads and verify scripts treat English as authoritative.
Resolve locale at boot before navigation: Gate root layout until device default and AsyncStorage override resolve - avoid English flash on Arabic devices.
Device default from expo-localization: Use getLocales()[0].languageTag and walk the preference list - not Intl guesses or hard-coded en-US.
User override beats device locale: Settings persistence in AsyncStorage (or server profile) wins over OS language on next launch.
Map BCP 47 tags to supported app locales: es-419 → es, pt-PT → pt - explicit tables beat bare language-code truncation.
Fallback chains to English: Configure fallbackLng (fr-CA → fr → en) - missing keys must never render blank in production.
Format with Intl at the display edge: Store ISO dates and raw numbers in state/API; format with languageTag and currencyCode from expo-localization.
Never parse locale-formatted input: Users type decimals with , or . - parse with known separators or structured pickers, not reversed display strings.
Adopt i18next past ~100 keys: Namespaces, plurals, lazy language packs, and TMS workflows outgrow hand-rolled t() helpers.
Split namespaces by feature: common, auth, billing - not one monolithic translation.json that conflicts on every merge.
Lazy-load non-default languages: Dynamic import() per locale keeps startup small - preload on Wi‑Fi after user selects a language.
escapeValue: false in react-i18next: React Native Text already escapes - escapeValue: true mangles apostrophes and ICU markup.
Plural keys in JSON, not JSX branches: Use count with _one / _other suffixes - Arabic and Polish need more than two forms.
Error mappers return message keys: Not literal English strings - resolve with t() in components so offline and auth copy translates.
RTL is layout, not just translation: Enable I18nManager, use marginStart/paddingEnd, mirror directional icons - reload when crossing LTR ↔ RTL.
Do not mirror logos, media, or maps: Chevrons and back arrows flip; brand marks and play buttons do not.
Refresh locale on Android foreground: AppState + getLocales() when returning active - users change language in Settings without restart.
Pseudolocale before long locales ship: en-XA or padded strings surface truncation in buttons and tabs before German copy arrives.
Translation CI on every PR: i18next-parser extract + verify-locales parity - block merge when en drifts or required locales miss keys.
TMS for non-English writes: Crowdin/Lokalise download into git - translators do not edit es.json in pull requests manually.
testID for E2E, not copy assertions: Detox and Maestro survive locale changes when selectors decouple from translated labels.
Brand flavors may scope locales and RTL: White-label tenants ship different language lists and direction rules without forking code - coordinate with design-system theming.
expo-localization + i18next / react-i18next + AsyncStorage override + I18nManager for RTL + i18next-parser and verify script in CI. Start with i18n Basics, graduate to i18next / react-i18next.
Two locales, fewer than ~100 keys, no plurals, and no TMS. Replace before plurals, lazy packs, or translator workflows - see i18n Basics.
Locale choice is independent of light/dark theme, but brand flavors may fix supported locales, default language, and RTL policy per tenant - Theming & Brand Flavors.
Store metadata is separate from in-app i18n. In-app locales come from JSON bundles or OTA updates - Translation CI gates production catalogs.
I18nManager and mirrored iconsStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026