React Native i18n: A Practical Guide to Multi-Language Mobile Apps
Most i18n bugs in React Native apps trace back to two decisions made early: hardcoding strings, and using string concatenation to build sentences. Both feel fine when you're shipping in English. Both are catastrophic the day you add a second locale.
This is the practical playbook I wish I had when I first added Spanish, Japanese, and Arabic to a React Native app: which library to pick, the patterns that scale, and the traps that only show up in production.
TL;DR
- Use
i18next+react-i18next+expo-localization(orreact-native-localizeon bare RN). - Never hardcode strings, never concatenate to build sentences.
- Use ICU pluralization —
count === 1 ? 'a' : 'b'is wrong in most languages. - Plan for RTL from day one with
marginStart/marginEndinstead of left/right. - Use namespaces (
auth.json,home.json) once you cross ~500 keys. - Format dates/numbers/currencies with the
IntlAPI. - Hook up a translation management service (Crowdin, Lokalise, Locize) before you ship to a third locale.
Why i18n Is an Architectural Decision
Retrofitting translations into a codebase that hardcodes strings everywhere is one of the most painful refactors in mobile development. For a 50-screen app, expect 1-3 weeks of dedicated work to extract strings, set up a translation library, and validate every screen.
The discipline of always using t('home.welcome') instead of "Welcome" from day one means you never have a "what strings did we miss?" problem when you add a second language. It also doubles as a code review smell test.
Picking a Library
| Library | Best For | Bundle Size |
|---|---|---|
i18next + react-i18next
|
Most apps | ~50KB |
react-intl (FormatJS) |
ICU MessageFormat purists | ~80KB |
| LinguiJS | Type-safe, compile-time extraction | ~20KB |
For 95% of React Native projects, default to i18next unless you have a specific reason not to.
Minimal Setup (Expo)
// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';
import en from './locales/en/common.json';
import es from './locales/es/common.json';
i18n
.use(initReactI18next)
.init({
resources: {
en: { common: en },
es: { common: es },
},
lng: Localization.locale.split('-')[0],
fallbackLng: 'en',
defaultNS: 'common',
interpolation: { escapeValue: false },
compatibilityJSON: 'v3',
});
export default i18n;
// App.js
import './i18n';
import { useTranslation } from 'react-i18next';
export default function App() {
const { t } = useTranslation();
return <Text>{t('greeting', { name: 'Maria' })}</Text>;
}
7 Patterns That Scale
1. Use interpolation, not concatenation
// Bad
t('hello') + ' ' + user.name + '!'
// Good
t('greeting', { name: user.name })
// en.json: { "greeting": "Hello, {{name}}!" }
// ja.json: { "greeting": "{{name}}さん、こんにちは!" }
2. Use ICU pluralization
English has 2 plural forms, Russian has 3, Arabic has 6. Don't roll your own.
{
"items_one": "{{count}} item",
"items_other": "{{count}} items"
}
3. Plan for RTL from day one
- Use
marginStart/marginEndinstead of left/right. - Mirror directional icons:
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }]. - Switching between LTR/RTL requires an app restart — design your language switcher UX accordingly.
4. Namespace your translations
locales/en/
├── common.json
├── auth.json
├── home.json
└── errors.json
Lazy-load namespaces per screen to reduce startup load on lower-end Android.
5. Format with the Intl API
new Intl.DateTimeFormat(i18n.language, { dateStyle: 'long' }).format(date);
new Intl.NumberFormat(i18n.language, { style: 'currency', currency: 'USD' }).format(amount);
Hermes supports Intl natively as of RN 0.71+. If you're older, use the formatjs polyfill — but upgrade Hermes if you can.
6. Set up a translation pipeline
Hook your locales/ folder to Crowdin, Lokalise, or Locize before you ship to a third language. CI should fail the build on missing keys in production locales.
7. Test with pseudo-localization
Use a "fake locale" that wraps every string in brackets and adds 30% length. Catches truncation, hardcoded strings, and layout breakage before real translators see anything.
How RapidNative Handles This
RapidNative is an AI app builder that generates React Native + Expo code from natural-language prompts. The generated components use translation keys (t('auth.sign_in_button')) instead of hardcoded strings, and the translation JSON is scaffolded alongside the code — so adding a new locale is dropping in a translated JSON file, not refactoring imports.
If you're starting a new project and don't want to build the i18n scaffolding by hand, that's a faster path to "i18n-ready by default."
Wrapping Up
Multi-language support gets dramatically more expensive over time. Build it in from day one — translation keys, ICU plurals, RTL-aware layouts, real translation pipeline — and your app can ship to any market without a future refactor.
What pattern do you wish you'd known earlier? Drop it in the comments — always curious how other teams handle the RTL transition specifically.
Top comments (0)