- 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.
Introduction
React Native internationalization (i18n) means writing your app in such a way that you can display, input, and understand text, dates, and numbers, as well as design anything that works for any language or region without changing any code. The actual translation of the locale then becomes the process of localization (l10n).
The time taken to retrofit an application when working with several markets through a React Native app is greatly reduced when i18n is implemented from the beginning. This guide will provide you with a full production-ready application that uses react-i18next – the most popular i18n library with over 2.1 million weekly npm downloads. It will also include device language detection, AsyncStorage storage, and RTL support.
What you will build: A React Native app that detects device language, allows language switching at runtime, stores the preference, and supports RTL layouts.
Why react-i18next, and not i18n-js?
The old i18n-js (which was integrated using react-native-i18n) is also officially deprecated. It is even advised to move away from it by its own developers. The 2026 standard for localization is i18next + react-i18next combo because of three reasons: support for pluralization rules for all 200+ languages, integration with useTranslation hook from React which re-renders components automatically when language changes, and plugin system for language detection, lazy-loading, and TypeScript safety checks.
| Library | Weekly downloads | Status | Hook support |
| react-i18next | 2.1M | ✅ Active | ✅ useTranslation |
| react-intl (FormatJS) | 1.2M | ✅ Active | ✅ useIntl |
| i18n-js | 180K | ❌ Deprecated | ❌ None |
Step 1: Install the packages
1 2 3 4 5 | # For Expo projects npx expo install expo-localization react-i18next i18next @react-native-async-storage/async-storage # For bare React Native projects npm install react-i18next i18next react-native-localize @react-native-async-storage/async-storage |
There are two separate packages doing separate things: i18next does the actual translation, and react-i18next handles the React side of things (with the useTranslation hook). That is by design; i18next can be used server-side and with regular JavaScript.
Step 2: Create your translation files
In order to have one file per language create src/i18n/locales/ folder and store all translations into JSON files for each language. Use namespaces in order to organize translations by features and not put everything into one big file, as it makes the app more manageable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // src/i18n/locales/en.json { "common": { "greeting": "Hello!", "welcome": "Welcome to the app." }, "profile": { "title": "Your Profile", "editButton": "Edit" } } // src/i18n/locales/es.json { "common": { "greeting": "¡Hola!", "welcome": "Bienvenido a la aplicación." }, "profile": { "title": "Tu Perfil", "editButton": "Editar" } } |
Key naming convention: use feature.action (e.g. profile.editButton), not generic names like button1. Semantic keys survive translator handoffs without confusion.
Step 3: Configure i18next with device detection and persistence
It is here that most tutorials fail. Three aspects are essential to a production setup that are not covered in the basics: the app must be able to detect the language of the device, persist the chosen language of the user, and gracefully handle missing translation keys.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // src/i18n/i18n.js import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import AsyncStorage from '@react-native-async-storage/async-storage'; import * as Localization from 'expo-localization'; // or react-native-localize for bare RN import en from './locales/en.json'; import es from './locales/es.json'; const LANGUAGE_KEY = '@app_language'; const initI18n = async () => { // 1. Check if user has previously chosen a language const savedLanguage = await AsyncStorage.getItem(LANGUAGE_KEY); // 2. Fall back to device language, then English const deviceLanguage = Localization.getLocales()[0]?.languageCode ?? 'en'; const initialLanguage = savedLanguage ?? deviceLanguage; await i18n .use(initReactI18next) .init({ compatibilityJSON: 'v3', // Required for Android resources: { en: { translation: en }, es: { translation: es }, }, lng: initialLanguage, fallbackLng: 'en', interpolation: { escapeValue: false }, }); }; export const changeLanguage = async (lang) => { await i18n.changeLanguage(lang); await AsyncStorage.setItem(LANGUAGE_KEY, lang); // Persist the choice }; initI18n(); export default i18n; |
Without the compatibilityJSON: ‘v3’ parameter, pluralization will fail in Android when using old versions of JavaScript engine.
Step 4: Wrap your app with the provider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // App.js import './src/i18n/i18n'; // Initialise before anything renders import React from 'react'; import { I18nextProvider } from 'react-i18next'; import i18n from './src/i18n/i18n'; import HomeScreen from './src/screens/HomeScreen'; export default function App() { return ( <I18nextProvider i18n={i18n}> <HomeScreen /> </I18nextProvider> ); } |
Step 5: Use the useTranslation hook in components
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // src/screens/HomeScreen.js import React from 'react'; import { View, Text } from 'react-native'; import { useTranslation } from 'react-i18next'; const HomeScreen = () => { const { t } = useTranslation(); return ( <View> <Text>{t('common.greeting')}</Text> <Text>{t('common.welcome')}</Text> </View> ); }; export default HomeScreen; |
The useTranslation hook will automatically re-render the component in case of changing language – no additional state required.
Step 6: Build a language switcher with persistence
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // src/components/LanguageSwitcher.js import React from 'react'; import { View, TouchableOpacity, Text, StyleSheet } from 'react-native'; import { useTranslation } from 'react-i18next'; import { changeLanguage } from '../i18n/i18n'; const LANGUAGES = [ { code: 'en', label: 'English' }, { code: 'es', label: 'Español' }, ]; const LanguageSwitcher = () => { const { i18n } = useTranslation(); return ( <View style={styles.row}> {LANGUAGES.map((lang) => ( <TouchableOpacity key={lang.code} onPress={() => changeLanguage(lang.code)} style={[ styles.btn, i18n.language === lang.code && styles.active, ]} > <Text>{lang.label}</Text> </TouchableOpacity> ))} </View> ); }; const styles = StyleSheet.create({ row: { flexDirection: 'row', gap: 8 }, btn: { padding: 10, borderRadius: 6, borderWidth: 1, borderColor: '#ccc' }, active: { borderColor: '#0066ff', backgroundColor: '#e8f0ff' }, }); export default LanguageSwitcher; |
Handling RTL languages (Arabic, Hebrew, Urdu)
Right-to-left support is the most commonly skipped step and the one most likely to break your layout. React Native’s I18nManager handles this, but it requires an app reload to take effect.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import { I18nManager } from 'react-native'; import * as Updates from 'expo-updates'; // Expo only export const changeLanguage = async (lang) => { const isRTL = lang === 'ar' || lang === 'he'; if (I18nManager.isRTL !== isRTL) { I18nManager.forceRTL(isRTL); await Updates.reloadAsync(); // Reload required for RTL to apply } await i18n.changeLanguage(lang); await AsyncStorage.setItem(LANGUAGE_KEY, lang); }; |
This is a mandatory step if you target Arabic or Hebrew audience – React Native won’t reverse the layout direction otherwise.
Limitations to know before you ship
The process of implementing internationalization using react-i18next is relatively easy for most scenarios. However, there are three main limitations that production applications might run into:
Bundle size: The larger the translation files imported at the start, the larger the bundle size becomes. In applications supporting multiple languages, it is necessary to configure i18next-http-backend with lazy loading of translation files rather than importing all translation files at once.
TypeScript key safety: With no additional configuration, t(‘misspelled.key’) will return the key without an error. To prevent such situations in large development teams, i18next-typescript should be used.
Plural forms: English uses two plural forms (one/other). Arabic uses six. Polish has four. It is important to check pluralization with i18n.t(‘itemCount’, { count: 0 }), { count: 1 }, { count: 2 }, and { count: 21 } in all supported languages.
