If your React Native app only speaks English, you're leaving ~75% of smartphone users on the table. Here's the modern stack for shipping a multilingual Expo app without three days of glue code.
The Stack
- react-i18next: translation management (6M+ weekly downloads, TS-first)
-
expo-localization: device locale detection (or
react-native-localizefor bare RN) - Intl: dates, numbers, currencies (built into Hermes)
- I18nManager: RTL layout flipping
Setup in 4 Steps
1. Install
npx expo install expo-localization
npm i i18next react-i18next
2. Translation files
locales/
en.json
es.json
ar.json
{
"welcome": "Welcome",
"cart": {
"itemCount_one": "{{count}} item",
"itemCount_other": "{{count}} items"
}
}
Use the ICU plural suffixes (_one, _other, _few, _many, _zero). Arabic has 6 plural forms, so get this right on day one.
3. Bootstrap
// i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';
import en from './locales/en.json';
import es from './locales/es.json';
import ar from './locales/ar.json';
const deviceLocale = Localization.getLocales()[0]?.languageCode ?? 'en';
i18n.use(initReactI18next).init({
compatibilityJSON: 'v4',
resources: {
en: { translation: en },
es: { translation: es },
ar: { translation: ar },
},
lng: deviceLocale,
fallbackLng: 'en',
interpolation: { escapeValue: false },
});
export default i18n;
Two production-important bits: compatibilityJSON: 'v4' for modern plural rules, and escapeValue: false because RN already escapes JSX.
4. Wire into Expo Router
// app/_layout.tsx
import { Stack } from 'expo-router';
import './i18n';
export default function RootLayout() {
return <Stack />;
}
Then in any component:
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
<Text>{t('cart.itemCount', { count: 3 })}</Text>
// "3 items"
The Gotchas Nobody Warns You About
Never concatenate around t(). Word order changes across languages:
// ❌ Breaks in Japanese, German, Arabic
<Text>{t('hello')} {user.name}!</Text>
// ✅ Whole sentence is one key
<Text>{t('greeting', { name: user.name })}</Text>
RTL doesn't flip everything. Flexbox auto-flips, but you'll need to fix:
-
left/right→ usestart/end - Directional icons → wrap in
transform: [{ scaleX: -1 }]whenI18nManager.isRTL - Hardcoded
textAlign
Hardcoded strings in third-party UI kits. Buttons in libraries like RN Elements ship English defaults. Override or pick i18n-aware libs.
Push notifications aren't localized by the phone: your backend has to send the localized copy or use OS-level notification categories.
Formatting
Never string-concat dates or currencies. Use Intl:
new Intl.DateTimeFormat(i18n.language, { dateStyle: 'medium' }).format(new Date());
new Intl.NumberFormat(i18n.language, { style: 'currency', currency: 'USD' }).format(29.99);
Add @formatjs/intl-datetimeformat polyfills if you need older Android or less common locales.
Testing Discipline
- Turn on
saveMissingin dev. react-i18next logs every missing key. Fail CI on any. - Pseudo-localize with
en-XA. This surfaces hardcoded strings and 30–40% longer text for layout stress-tests. - Snapshot per locale. French/German/Arabic snapshots catch layout regressions.
Shortcut
If you're starting fresh, RapidNative generates the whole i18n scaffold (provider, layout wiring, translation files, RTL flags) from a natural-language prompt. You get the same setup you'd write by hand, in about 30 seconds.
Otherwise: ship it. It's 2–3 days of setup and it opens up 3 billion more users.
Top comments (0)