React Native Internationalization: react-i18next Guide

Unlock global reach with our React Native guide. Learn internationalization using the i18n package to build multilingual, culturally adapted mobile apps.

react_localization
Key takeaways
  • 1 Use React-i18next Instead of Outdated Options: i18n-js has been deprecated and thus the currently recommended stack in 2026 is i18next + react-i18next combination. It allows for pluralisation of 200+ locales and gives access to useTranslation hook for language change re-renders.
  • 2 Create Translations File Per Feature and Namespace: Create one JSON translation file per language and structure it semantically according to feature.actions (for example, profile.editButton), allowing large projects to maintain order and avoid misunderstandings between translators.
  • 3 Configure i18next for Device Language Detection and Persistence: In order to create a professional setup, you will need i18next to identify the language of the device, store user selection manually through AsyncStorage and fallback on key if there's no translation. Never forget to configure 'compatibilityJSON': 'v3', otherwise plurals won't work on Androids.
  • 4 Create a Persistent Language Switcher: A language switcher needs to call changeLanguage() during the execution and then save its result to the AsyncStorage. Otherwise your application will always use language set on the device, reverting user choice.
  • 5 Use I18nManager.forceRTL() for Arabic/Hebrew: Call I18nManager.forceRTL() during language change for right-to-left languages and reload the whole app immediately afterwards as React-Native does layout direction detection only once on initial load.

Your app might work perfectly in English. However, users in Spain, Saudi Arabia, or Japan expect it to speak their language, too. React Native internationalization lets one codebase serve all of them. It’s much easier to plan for it early than to retrofit it later.

This guide walks through a complete setup with i18next and react-i18next. You’ll learn how to organize translations, detect the device language, remember the user’s choice, and support right-to-left languages like Arabic and Hebrew.

What Is React Native Internationalization?

Internationalization (i18n) means preparing your app to support multiple languages and regions. In practice, you move every user-facing string out of your components and into translation files. You also format dates, numbers, and currencies based on the user’s locale.

Localization (l10n) comes after that. It means adding the actual translated content for each language. You build the i18n foundation once, and then each new language only needs a new translation file.

Why Internationalization Matters for Mobile Apps

App stores make your app available in dozens of countries from day one. Still, many users skip apps that don’t support their language. Translated apps also tend to earn better reviews, because users understand every screen and message.

Internationalization also saves engineering time. Hardcoded strings spread across hundreds of components are painful to extract later. When you set up i18n at the start, adding a language becomes a content task instead of a code change.

How to Set Up React Native Internationalization with react-i18next

The i18next ecosystem is the most popular choice for React Native today. It handles translations, plural rules, and interpolation. Meanwhile, react-i18next adds React hooks that re-render your components when the language changes.

Step 1: Install the Required Packages

First, install i18next and react-i18next:

npm install i18next react-i18next

Next, add packages for device language detection and storage. In an Expo project, run:

npx expo install expo-localization @react-native-async-storage/async-storage

In a bare React Native project, use react-native-localize instead:

npm install react-native-localize @react-native-async-storage/async-storage
cd ios && pod install

This guide uses expo-localization in its examples. With react-native-localize, the setup stays the same, and only the detection call changes.

Step 2: Create Translation Files

Create one JSON file per language inside a locales folder:

src/
  i18n/
    index.js
    locales/
      en.json
      es.json
      ar.json

Inside each file, group keys by feature. This structure keeps large projects organized, and it tells translators exactly where each string appears.

{
  "home": {
    "title": "Welcome",
    "greeting": "Hello, {{name}}!"
  },
  "profile": {
    "editButton": "Edit profile"
  },
  "cart": {
    "items_one": "{{count}} item",
    "items_other": "{{count}} items"
  }
}

The {{name}} placeholder lets you insert values at runtime. Similarly, the _one and _other suffixes define plural forms. i18next picks the right form based on the count and the language’s plural rules. Some languages, such as Arabic, use up to six forms, and i18next handles those too.

For very large apps, you can split each language into several files called namespaces, such as common.json and profile.json. For most projects, however, one file per language works well.

Step 3: Configure i18next with Language Detection and Persistence

Now create src/i18n/index.js. This file decides which language to load. It checks for a saved user choice first, then falls back to the device language, and finally to English.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';
import AsyncStorage from '@react-native-async-storage/async-storage';

import en from './locales/en.json';
import es from './locales/es.json';
import ar from './locales/ar.json';

export const LANGUAGE_KEY = 'app-language';

const resources = {
  en: { translation: en },
  es: { translation: es },
  ar: { translation: ar },
};

export async function initI18n() {
  const savedLanguage = await AsyncStorage.getItem(LANGUAGE_KEY);
  const deviceLanguage = Localization.getLocales()[0]?.languageCode;
  const supported = Object.keys(resources);

  const lng =
    savedLanguage ??
    (supported.includes(deviceLanguage) ? deviceLanguage : 'en');

  await i18n.use(initReactI18next).init({
    resources,
    lng,
    fallbackLng: 'en',
    interpolation: { escapeValue: false },
  });
}

export default i18n;

A few settings deserve a closer look. The fallbackLng option shows English whenever a translation is missing, so users never see raw keys. Also, escapeValue: false is safe here, because React already escapes rendered text.

Plural rules rely on the Intl.PluralRules API. If your JavaScript engine doesn’t support it, i18next logs a warning, and plurals won’t work correctly. In that case, install the polyfill with npm install intl-pluralrules. Then add import 'intl-pluralrules'; at the top of this file.

Step 4: Load i18n Before Rendering the App

Since reading from AsyncStorage is asynchronous, your app should wait for i18n to finish loading. Otherwise, users might see English for a split second before their saved language appears.

import { useEffect, useState } from 'react';
import { initI18n } from './src/i18n';
import RootNavigator from './src/navigation/RootNavigator';

export default function App() {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    initI18n().then(() => setReady(true));
  }, []);

  if (!ready) return null; // or your splash screen

  return <RootNavigator />;
}

Step 5: Use Translations in Components

The useTranslation hook gives you a t function. You pass it a key, plus any values the string needs.

import { View, Text } from 'react-native';
import { useTranslation } from 'react-i18next';

export function HomeScreen({ name, cartCount }) {
  const { t } = useTranslation();

  return (
    <View>
      <Text>{t('home.title')}</Text>
      <Text>{t('home.greeting', { name })}</Text>
      <Text>{t('cart.items', { count: cartCount })}</Text>
    </View>
  );
}

Notice that the plural key is just cart.items. i18next adds the _one or _other suffix for you, based on the count value. Also, any component that uses this hook re-renders automatically when the language changes.

Step 6: Build a Persistent Language Switcher

A language switcher needs to do two things. It must change the active language, and it must save the choice. Otherwise, the app reverts to the device language on the next launch.

import { View, Text, Pressable } from 'react-native';
import { useTranslation } from 'react-i18next';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { LANGUAGE_KEY } from '../i18n';
import { applyLayoutDirection } from '../i18n/rtl';

const LANGUAGES = [
  { code: 'en', label: 'English' },
  { code: 'es', label: 'Español' },
  { code: 'ar', label: 'العربية' },
];

export function LanguageSwitcher() {
  const { i18n } = useTranslation();

  const selectLanguage = async (code) => {
    await i18n.changeLanguage(code);
    await AsyncStorage.setItem(LANGUAGE_KEY, code);
    await applyLayoutDirection(code);
  };

  return (
    <View>
      {LANGUAGES.map(({ code, label }) => (
        <Pressable key={code} onPress={() => selectLanguage(code)}>
          <Text>{i18n.language === code ? `✓ ${label}` : label}</Text>
        </Pressable>
      ))}
    </View>
  );
}

Show each language name in its own language, such as “Español” rather than “Spanish.” That way, users can find their language even if the app currently displays one they can’t read.

Step 7: Handle Right-to-Left Languages

Arabic, Hebrew, Persian, and Urdu read from right to left. For these languages, the entire layout should flip, including navigation, icons, and text alignment.

React Native handles this through I18nManager. However, it only applies layout direction when the app starts. So after you change direction, you need to reload the app.

import { I18nManager } from 'react-native';
import * as Updates from 'expo-updates';

const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];

export async function applyLayoutDirection(language) {
  const shouldBeRTL = RTL_LANGUAGES.includes(language);

  if (I18nManager.isRTL !== shouldBeRTL) {
    I18nManager.allowRTL(shouldBeRTL);
    I18nManager.forceRTL(shouldBeRTL);
    await Updates.reloadAsync();
  }
}

In bare React Native apps, replace Updates.reloadAsync() with RNRestart.restart() from the react-native-restart package.

To make your layouts flip correctly, use direction-aware style properties. For example, write marginStart and paddingEnd instead of marginLeft and paddingRight. React Native then mirrors them automatically in RTL mode.

Formatting Dates, Numbers, and Currency

Translation covers only part of React Native internationalization. Formats matter too. For example, the US writes dates as 12/31/2026, while most of Europe writes 31/12/2026. Number separators and currency symbols also change by region.

The built-in Intl APIs handle this for you. Simply pass the current language from i18next:

const { i18n } = useTranslation();

const price = new Intl.NumberFormat(i18n.language, {
  style: 'currency',
  currency: 'EUR',
}).format(1299.5);

const date = new Intl.DateTimeFormat(i18n.language, {
  dateStyle: 'long',
}).format(new Date());

As a result, German users see “1.299,50 €,” while English users see “€1,299.50.”

Common React Native Internationalization Mistakes

A few mistakes appear in almost every first i18n project, so it helps to watch for them early.

First, developers leave hardcoded strings in the code. These strings often hide in alerts, placeholders, and accessibility labels. Search your codebase for plain text inside Text components and Alert.alert() calls before each release.

Second, teams build sentences by joining translated fragments, such as t('you_have') + count + t('messages'). This breaks in languages with different word order. Instead, keep the full sentence in one key and use placeholders.

Third, many designs assume English text length. Translations in languages like German or Finnish often run much longer. Therefore, avoid fixed widths on buttons and labels, and test every screen in your longest language.

Finally, teams often skip RTL testing until the end. Flipped layouts can break custom animations, icons, and absolute positioning. Test Arabic or Hebrew as soon as you add support, not a week before launch.

Conclusion

React Native internationalization comes down to a few core pieces. Store translations in JSON files, load the right language at startup, and save the user’s choice. Then handle plurals, formats, and RTL layouts properly.

With i18next and react-i18next, this setup takes an afternoon. After that, adding a new language only means adding a new translation file. Your app can then reach users around the world without a separate codebase for each market.

Need help taking your mobile app global? You can hire experienced React Native developers through Innostax, or reach out to us to discuss your project.

Frequently Asked Questions

Internationalization (i18n) is Engineering the app to support multiple languages - that is, abstracting all the strings, dates, and numbers. l10n Localization (l10n) is populating the specific locale with the translated content. i18n is done once, l10n is done each time you add a language.

Most React Native projects should use react-i18next since it has better React Native support and a smaller bundle size (about 35KB vs about 60KB) as well as easier useTranslation hooks. react-intl (FormatJS) should be used where more advanced ICU message formatting is needed, or if you're already using it in a shared web/native codebase.

In case of expo projects, use expo-localization. For RN projects, use react-native-localize. You can get the current device language by calling Localization.getLocales()[0].languageCode and pass it as an initial lng in i18next configuration.

The layout direction in React Native is set at the beginning of the app start-up process, not at the run-time. When calling I18nManager.forceRTL(true), the changes will be applied only on the next app start-up. Use expo-updates reloadAsync() in case of expo, or RNRestart in case of bare RN.

Not really. You will have to persist the user-selection yourself into AsyncStorage, or into MMKV, which performs faster than AsyncStorage. In order to achieve that, you need to read the persisted data and feed it into i18next init() function, just like shown in step 3.