IntlPull
Tutorial
13 min read

React Native i18n: Complete Localization Guide for 2026

Production-ready React Native internationalization with i18next and react-native-localize. Learn RTL support, OTA updates, Expo integration, platform-specific strings, and automated translation workflows.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Production-ready React Native internationalization with i18next and react-native-localize. Learn RTL support, OTA updates, Expo integration, platform-specific strings, and automated translation workflows.

React Native's "learn once, write anywhere" philosophy extends to internationalization (i18n), enabling developers to build multilingual mobile apps that feel native on both iOS and Android. With the powerful combination of react-native-localize for device locale detection and i18next for translation management, React Native provides a flexible and robust i18n foundation. This comprehensive guide covers everything from basic setup to advanced patterns including RTL support, over-the-air (OTA) translation updates, platform-specific strings, Expo considerations, and integration with modern translation management systems like IntlPull for automated workflows and instant updates without app store submissions.

Understanding React Native i18n Architecture

React Native i18n relies on three core libraries:

  1. react-native-localize: Detects device locale, timezone, and language preferences
  2. i18next: Translation framework with powerful features (plurals, interpolation, namespaces)
  3. react-i18next: React bindings for i18next with hooks and HOCs

This stack provides runtime translation loading, dynamic locale switching, and seamless integration with React's component lifecycle. Unlike compile-time solutions, React Native's approach enables over-the-air updates and A/B testing without app resubmission.

Basic Setup with i18next

Step 1: Install Dependencies

Terminal
1# For standard React Native
2npm install i18next react-i18next react-native-localize
3npm install --save-dev @types/i18next
4
5# Install native dependencies (iOS)
6cd ios && pod install && cd ..
7
8# For Expo (no pod install needed)
9npx expo install react-native-localize

Step 2: Create Translation Files

Create src/i18n/locales/en.json:

JSON
1{
2  "common": {
3    "welcome": "Welcome to {{appName}}",
4    "loading": "Loading...",
5    "error": "Something went wrong",
6    "retry": "Retry"
7  },
8  "home": {
9    "title": "Home",
10    "greeting": "Hello, {{name}}!",
11    "itemCount": "{{count}} item",
12    "itemCount_other": "{{count}} items"
13  },
14  "settings": {
15    "title": "Settings",
16    "language": "Language",
17    "theme": "Theme",
18    "logout": "Log Out"
19  }
20}

Create src/i18n/locales/es.json:

JSON
1{
2  "common": {
3    "welcome": "Bienvenido a {{appName}}",
4    "loading": "Cargando...",
5    "error": "Algo salió mal",
6    "retry": "Reintentar"
7  },
8  "home": {
9    "title": "Inicio",
10    "greeting": "¡Hola, {{name}}!",
11    "itemCount": "{{count}} artículo",
12    "itemCount_other": "{{count}} artículos"
13  },
14  "settings": {
15    "title": "Configuración",
16    "language": "Idioma",
17    "theme": "Tema",
18    "logout": "Cerrar Sesión"
19  }
20}

Step 3: Configure i18next

Create src/i18n/index.ts:

TypeScript
1import i18n from 'i18next';
2import { initReactI18next } from 'react-i18next';
3import * as RNLocalize from 'react-native-localize';
4
5import en from './locales/en.json';
6import es from './locales/es.json';
7import fr from './locales/fr.json';
8
9const resources = {
10  en: { translation: en },
11  es: { translation: es },
12  fr: { translation: fr },
13};
14
15// Get device locale
16const deviceLocale = RNLocalize.getLocales()[0];
17const languageCode = deviceLocale?.languageCode || 'en';
18
19i18n
20  .use(initReactI18next)
21  .init({
22    resources,
23    lng: languageCode,
24    fallbackLng: 'en',
25    interpolation: {
26      escapeValue: false, // React already escapes
27    },
28    react: {
29      useSuspense: false, // Disable suspense for React Native
30    },
31    compatibilityJSON: 'v3', // For i18next v21+
32  });
33
34// Listen for locale changes
35RNLocalize.addEventListener('change', () => {
36  const newLocale = RNLocalize.getLocales()[0];
37  if (newLocale?.languageCode) {
38    i18n.changeLanguage(newLocale.languageCode);
39  }
40});
41
42export default i18n;

Step 4: Initialize in App Entry

Update App.tsx:

TypeScript
1import React from 'react';
2import { SafeAreaView, Text, View } from 'react-native';
3import './src/i18n'; // Import i18n configuration
4import { useTranslation } from 'react-i18next';
5
6function HomeScreen() {
7  const { t } = useTranslation();
8
9  return (
10    <SafeAreaView>
11      <View>
12        <Text>{t('common.welcome', { appName: 'MyApp' })}</Text>
13        <Text>{t('home.greeting', { name: 'Alice' })}</Text>
14        <Text>{t('home.itemCount', { count: 0 })}</Text>
15        <Text>{t('home.itemCount', { count: 1 })}</Text>
16        <Text>{t('home.itemCount', { count: 5 })}</Text>
17      </View>
18    </SafeAreaView>
19  );
20}
21
22export default function App() {
23  return <HomeScreen />;
24}

Advanced i18next Features

Namespaces for Code Splitting

Split translations by feature for better performance:

TypeScript
1// src/i18n/index.ts
2const resources = {
3  en: {
4    common: require('./locales/en/common.json'),
5    home: require('./locales/en/home.json'),
6    checkout: require('./locales/en/checkout.json'),
7  },
8  es: {
9    common: require('./locales/es/common.json'),
10    home: require('./locales/es/home.json'),
11    checkout: require('./locales/es/checkout.json'),
12  },
13};
14
15i18n.init({
16  resources,
17  ns: ['common', 'home', 'checkout'],
18  defaultNS: 'common',
19  // ...
20});

Usage:

TypeScript
1const { t } = useTranslation(['home', 'common']);
2
3// Explicit namespace
4<Text>{t('home:greeting', { name: 'Bob' })}</Text>
5
6// Default namespace
7<Text>{t('welcome')}</Text>

Pluralization

i18next supports complex plural rules:

JSON
1{
2  "key": "item",
3  "key_other": "items",
4  "keyWithCount": "{{count}} item",
5  "keyWithCount_other": "{{count}} items",
6  "keyWithCount_zero": "No items"
7}

Usage:

TypeScript
t('keyWithCount', { count: 0 }); // "No items"
t('keyWithCount', { count: 1 }); // "1 item"
t('keyWithCount', { count: 5 }); // "5 items"

Context for Variations

JSON
1{
2  "friend": "A friend",
3  "friend_male": "A boyfriend",
4  "friend_female": "A girlfriend"
5}
TypeScript
t('friend', { context: 'male' }); // "A boyfriend"

Date and Number Formatting

TypeScript
1import i18n from 'i18next';
2
3i18n.init({
4  // ...
5  interpolation: {
6    format: (value, format, lng) => {
7      if (format === 'uppercase') return value.toUpperCase();
8      if (format === 'currency') {
9        return new Intl.NumberFormat(lng, {
10          style: 'currency',
11          currency: 'USD',
12        }).format(value);
13      }
14      if (value instanceof Date) {
15        return new Intl.DateTimeFormat(lng).format(value);
16      }
17      return value;
18    },
19  },
20});

Usage:

TypeScript
t('price', { value: 19.99, formatParams: { value: { format: 'currency' } } });

RTL (Right-to-Left) Support

Detect RTL Languages

TypeScript
1import { I18nManager } from 'react-native';
2import * as RNLocalize from 'react-native-localize';
3
4const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];
5
6export function setupRTL() {
7  const deviceLocale = RNLocalize.getLocales()[0];
8  const isRTL = RTL_LANGUAGES.includes(deviceLocale?.languageCode || '');
9
10  if (isRTL !== I18nManager.isRTL) {
11    I18nManager.forceRTL(isRTL);
12    // Requires app restart on native
13    if (Platform.OS !== 'web') {
14      RNRestart.Restart(); // npm install react-native-restart
15    }
16  }
17}

RTL-Aware Styling

TypeScript
1import { I18nManager, StyleSheet } from 'react-native';
2
3const styles = StyleSheet.create({
4  container: {
5    flexDirection: 'row',
6    paddingStart: 16, // Auto-flips for RTL
7    paddingEnd: 8,
8  },
9  text: {
10    textAlign: I18nManager.isRTL ? 'right' : 'left',
11  },
12});

Use start/end instead of left/right for automatic RTL flipping.

Platform-Specific Strings

iOS vs Android Differences

JSON
1{
2  "action": {
3    "ios": "Tap to continue",
4    "android": "Tap to continue",
5    "default": "Click to continue"
6  }
7}
TypeScript
1import { Platform } from 'react-native';
2
3const platform = Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'default';
4<Text>{t(`action.${platform}`)}</Text>

Platform-Specific JSON Files

src/i18n/locales/
├── en.json (shared)
├── en.ios.json (iOS overrides)
└── en.android.json (Android overrides)
TypeScript
1import { Platform } from 'react-native';
2
3const en = require('./locales/en.json');
4const enPlatform = Platform.select({
5  ios: require('./locales/en.ios.json'),
6  android: require('./locales/en.android.json'),
7  default: {},
8});
9
10const resources = {
11  en: {
12    translation: { ...en, ...enPlatform },
13  },
14};

Expo Considerations

react-native-localize and Expo Go Compatibility

Many developers search for react-native-localize Expo Go compatibility because the native module story can be confusing. In a standard Expo project, prefer expo-localization while developing inside Expo Go. Use react-native-localize when you are building a custom dev client, an EAS build, or a bare React Native app where native modules are compiled into the binary.

EnvironmentRecommended Locale LibraryNotes
Expo Goexpo-localizationWorks without a custom native build
Expo custom dev clientreact-native-localize or expo-localizationRebuild the client after adding native modules
EAS production buildEither libraryTest locale changes on real devices
Bare React Nativereact-native-localizeRun pods for iOS after install

If react-native-localize works in your EAS build but not Expo Go, that is usually expected. Expo Go only includes the native modules bundled by Expo.

Expo Localization API

Terminal
npx expo install expo-localization
TypeScript
1import * as Localization from 'expo-localization';
2
3const deviceLocale = Localization.locale; // "en-US"
4const languageCode = deviceLocale.split('-')[0]; // "en"
5
6i18n.init({
7  lng: languageCode,
8  // ...
9});

EAS Build Configuration

For OTA updates with Expo:

JSON
1// eas.json
2{
3  "build": {
4    "production": {
5      "env": {
6        "INTLPULL_PROJECT_ID": "your-project-id",
7        "INTLPULL_API_KEY": "ip_live_..."
8      }
9    }
10  }
11}

IntlPull CLI Integration

Installation and Setup

Terminal
1# Install CLI
2npm install -g @intlpullhq/cli
3
4# Initialize in React Native project
5cd MyReactNativeApp
6intlpull init --framework react-native

Configuration (intlpull.config.json):

JSON
1{
2  "projectId": "your-project-id",
3  "apiKey": "ip_live_...",
4  "framework": "react-native",
5  "sourcePath": "src/i18n/locales",
6  "format": "json",
7  "languages": ["en", "es", "fr", "ar"],
8  "defaultLanguage": "en",
9  "namespaces": ["common", "home", "settings"]
10}

Automated Translation Extraction

Terminal
1# Scan code for hardcoded strings
2intlpull scan --auto-wrap
3
4# Before:
5<Text>Welcome to our app</Text>
6
7# After:
8<Text>{t('common.welcome')}</Text>

Push and Pull Translations

Terminal
1# Upload local JSON to IntlPull
2intlpull push
3
4# Download latest translations
5intlpull pull
6
7# Real-time sync during development
8intlpull watch

OTA Translation Updates

IntlPull's React Native OTA SDK enables instant updates:

Terminal
npm install @intlpullhq/react-native

Integration:

TypeScript
1// src/i18n/index.ts
2import { IntlPullOTA } from '@intlpullhq/react-native';
3import i18n from 'i18next';
4import { initReactI18next } from 'react-i18next';
5
6const ota = new IntlPullOTA({
7  projectId: 'your-project-id',
8  apiKey: process.env.INTLPULL_API_KEY,
9  environment: __DEV__ ? 'development' : 'production',
10  cacheEnabled: true,
11  updateInterval: 3600000, // Check every hour
12});
13
14// Initialize with OTA resources
15ota.initialize().then((resources) => {
16  i18n
17    .use(initReactI18next)
18    .init({
19      resources,
20      // ...
21    });
22});
23
24// Listen for updates
25ota.on('update', (newResources) => {
26  Object.keys(newResources).forEach((lng) => {
27    i18n.addResourceBundle(lng, 'translation', newResources[lng], true, true);
28  });
29});
30
31export default i18n;

Publish Updates:

Terminal
# From CLI
intlpull publish --version 1.2.0 --message "Fixed checkout typos"

App fetches updates on launch (cached for 1 hour). Critical translation fixes deploy in minutes.

Testing Localization

Unit Tests with jest

TypeScript
1import i18n from '../src/i18n';
2
3describe('i18n', () => {
4  beforeAll(async () => {
5    await i18n.changeLanguage('en');
6  });
7
8  it('translates basic strings', () => {
9    expect(i18n.t('common.welcome', { appName: 'TestApp' })).toBe('Welcome to TestApp');
10  });
11
12  it('handles pluralization', () => {
13    expect(i18n.t('home.itemCount', { count: 0 })).toBe('0 items');
14    expect(i18n.t('home.itemCount', { count: 1 })).toBe('1 item');
15    expect(i18n.t('home.itemCount', { count: 5 })).toBe('5 items');
16  });
17
18  it('switches languages', async () => {
19    await i18n.changeLanguage('es');
20    expect(i18n.t('common.loading')).toBe('Cargando...');
21  });
22});

Component Tests

TypeScript
1import { render } from '@testing-library/react-native';
2import { I18nextProvider } from 'react-i18next';
3import i18n from '../src/i18n';
4import HomeScreen from '../src/screens/HomeScreen';
5
6describe('HomeScreen', () => {
7  it('displays translated greeting', () => {
8    const { getByText } = render(
9      <I18nextProvider i18n={i18n}>
10        <HomeScreen />
11      </I18nextProvider>
12    );
13
14    expect(getByText(/Welcome to/i)).toBeTruthy();
15  });
16});

Manual Testing on Simulators

iOS Simulator:

  1. Settings → General → Language & Region → iPhone Language
  2. Select target language
  3. Tap "Change to [Language]"
  4. Relaunch app

Android Emulator:

  1. Settings → System → Languages & input → Languages
  2. Add a language
  3. Drag to top of list
  4. Relaunch app

Test Checklist:

  • Translations load correctly
  • RTL layout works (Arabic, Hebrew)
  • Plurals display properly
  • Date/number formats are locale-appropriate
  • Text doesn't overflow containers
  • Platform-specific strings show correctly

Best Practices and Performance

1. Lazy Load Translations

For large apps, load translations on demand:

TypeScript
1import i18n from 'i18next';
2import Backend from 'i18next-http-backend';
3
4i18n
5  .use(Backend)
6  .init({
7    backend: {
8      loadPath: '/locales/{{lng}}/{{ns}}.json',
9    },
10    // ...
11  });

2. Memoize Translation Hooks

TypeScript
1import { useMemo } from 'react';
2import { useTranslation } from 'react-i18next';
3
4function MyComponent() {
5  const { t } = useTranslation();
6
7  const title = useMemo(() => t('home.title'), [t]);
8
9  return <Text>{title}</Text>;
10}

3. Avoid Inline Functions in t()

Bad:

TypeScript
<Text>{t('welcome', { appName: getAppName() })}</Text> // Re-runs on every render

Good:

TypeScript
const appName = useMemo(() => getAppName(), []);
<Text>{t('welcome', { appName })}</Text>

4. Use Translation Keys as Fallback

TypeScript
1i18n.init({
2  fallbackLng: 'en',
3  returnEmptyString: false,
4  saveMissing: true, // Log missing keys in dev
5  missingKeyHandler: (lngs, ns, key) => {
6    if (__DEV__) {
7      console.warn(`Missing translation: ${key}`);
8    }
9  },
10});

5. Validate Translations in CI/CD

Terminal
1# Install CLI in CI
2npm install -g @intlpullhq/cli
3
4# Validate
5intlpull validate --missing --unused
6
7# CI script
8- name: Check Translations
9  run: |
10    intlpull pull
11    intlpull validate --fail-on-error

Common Pitfalls and Solutions

Issue 1: "Translation key not found"

Cause: Missing key in translation file or wrong namespace.

Solution:

TypeScript
1// Enable debugging
2i18n.init({ debug: true });
3
4// Check loaded namespaces
5console.log(i18n.options.ns);

Issue 2: RTL not flipping layout

Cause: I18nManager.forceRTL() requires app restart on native.

Solution:

TypeScript
1import RNRestart from 'react-native-restart';
2
3I18nManager.forceRTL(true);
4RNRestart.Restart();

Issue 3: Translations not updating on OTA

Cause: i18n cache not cleared.

Solution:

TypeScript
1ota.on('update', (newResources) => {
2  // Force reload
3  i18n.reloadResources().then(() => {
4    console.log('Translations updated');
5  });
6});

Issue 4: Expo Go doesn't support RTL

Cause: I18nManager.forceRTL() unavailable in Expo Go.

Solution: Use development builds:

Terminal
npx expo install expo-dev-client
npx expo run:ios

Production Deployment Checklist

  • All languages have complete translations
  • Plurals tested for all supported languages
  • RTL layouts verified (if supporting Arabic/Hebrew)
  • Platform-specific strings validated on iOS and Android
  • OTA SDK configured for production environment
  • Fallback language set to en
  • Translation memory populated
  • CI/CD validates translations before merge
  • Device locale detection tested on physical devices
  • App Store/Play Store metadata translated

Frequently Asked Questions

How do I handle region-specific locales (e.g., en-US vs en-GB)?

Use full locale codes:

TypeScript
1const resources = {
2  'en-US': { translation: require('./locales/en-US.json') },
3  'en-GB': { translation: require('./locales/en-GB.json') },
4};

Can I use i18next with Redux?

Yes, combine with i18next-react-native-language-detector:

TypeScript
1import { useSelector, useDispatch } from 'react-redux';
2
3const language = useSelector((state) => state.settings.language);
4i18n.changeLanguage(language);

How do I translate push notifications?

Send locale-specific payloads from backend:

JavaScript
1const message = {
2  notification: {
3    title: translations[userLocale].title,
4    body: translations[userLocale].body,
5  },
6};

What's the bundle size impact?

~50KB for i18next + react-i18next. Optimize with lazy loading for 100+ KB translation files.

How do I test OTA updates locally?

Use IntlPull's staging environment:

TypeScript
1const ota = new IntlPullOTA({
2  environment: 'staging',
3  // ...
4});

Can I use IntlPull with Expo?

Yes, IntlPull CLI and OTA SDK both support Expo managed workflows.

How often should I check for OTA updates?

Recommended: On app launch + every 1 hour in background. Avoid excessive polling (battery drain).

Conclusion

React Native's i18n ecosystem provides unparalleled flexibility for building multilingual mobile apps that feel native on every platform. By combining react-native-localize for device detection, i18next for powerful translation features, and IntlPull for automated workflows and OTA updates, you can reduce localization overhead by 80% while delivering instant fixes to users worldwide.

Start with the basics (i18next + JSON files), gradually adopt namespaces and lazy loading for performance, add RTL support for global reach, and integrate IntlPull CLI for team collaboration. Your international users will appreciate the linguistic attention to detail, and your team will appreciate the time saved on tedious translation management.

Ready to ship your React Native app globally? Try IntlPull free with 500 keys and 3 languages, or explore our React Native documentation for advanced patterns and best practices.

Tags
react-native
i18n
localization
mobile
expo
i18next
javascript
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.