I've been working with React Native and Expo for a little over a year now and I love it. I started my cross-platform mobile development journey with Flutter back when it was just a baby in 2017. But, being a primarily React developer, it just made sense for me to make the switch to React Native.
Up until a few weeks before the writing of this article, I had only ever worked in code bases where the internationalization work had already been completed. I had never actually implemented i18n in an application. Be it web or mobile.
So, let's take a look at how I internationalized Hundred Habits, my habit tracker app based on the Law of 100 principle.
Install the packages
First, you'll want to install the necessary i18n packages we'll work with. I use Expo for my React Native projects, so we'll use the npx expo install command to install what we need.
i18next and react-i18next are internationalization frameworks. We will use methods and hooks from these frameworks to interpolate translation "keys" into localized / translated text.
expo-localization is provided by Expo and will give us access to our device's system locale settings.
npx expo install i18next react-i18next expo-localization
Set up the necessary files
I typically have a services folder inside of my project for "services". I consider i18n a service. Create a folder called i18n. This will hold our translation JSON files.

We'll talk about what these files look like a little later.
Initialize your i18n instance
I use expo-router so I have a root _layout file in the app folder of my project. Within the root _layout file, you'll want to set up a useEffect to initiate your i18n instance. I also chose to create a state variable to track what we have initialized so that we don't unnecessarily fire the initialization multiple times.
import { useState } from 'react'
import i18n from 'i18next'
import * as Localization from 'expo-localization'
import en from '@/services/i18n/en-US.json';
import pt from '@/services/i18n/pt-PT.json';
import ru from '@/services/i18n/ru-RU.json';
import zh from '@/services/i18n/zh-CN.json';
import es from '@/services/i18n/es-ES.json';
import fr from '@/services/i18n/fr-FR.json';
import de from '@/services/i18n/de-DE.json';
import hi from '@/services/i18n/hi-IN.json';
export default function RootLayout() {
const resources = {
en,
pt,
ru,
zh,
de,
es,
fr,
hi,
};
const [ languageLoaded, setLanguageLoaded ] = useState(false)
const [ language, setLanguage ] = useState<string | null>()
useEffect(() => {
if (!language) return;
i18n.use(initReactI18next).init({
compatibilityJSON: 'v3',
resources,
lng: language,
fallbackLng: 'en',
});
setLanguageLoaded(true);
}, [language, languageLoaded]);
}
Set up your language JSON files
Each of the language JSON files will contain a JSON object of a unique translation key, and the value will be the translated text that the key represents.
US English file: en-US.json
{
"translation": {
"yes": "Yes",
"no": "No",
"addAGoal": "Add a goal",
"createGoal": "Create goal",
"editGoal": "Edit goal",
"deleteGoal": "Delete goal",
"goal_one": "Goal",
"goal_other": "Goals",
"title": "Title",
"back": "Back"
}
Portuguese file: pt-PT.json
{
"translation": {
"yes": "Sim",
"no": "Não",
"addAGoal": "Adicionar um objetivo",
"createGoal": "Criar objetivo",
"editGoal": "Editar objetivo",
"deleteGoal": "Excluir objetivo",
"goal_one": "Objetivo",
"goal_other": "Objetivos",
"title": "Título",
"back": "Voltar"
}
Get your device's system locale on app load
You may have noticed we had language and setLanguage variables in our _layout. Let's create another useEffect and set our language that needs to be passed into our i18next initialization.
import { useState } from 'react'
import i18n from 'i18next'
import * as Localization from 'expo-localization'
import en from '@/services/i18n/en-US.json';
import pt from '@/services/i18n/pt-PT.json';
import ru from '@/services/i18n/ru-RU.json';
import zh from '@/services/i18n/zh-CN.json';
import es from '@/services/i18n/es-ES.json';
import fr from '@/services/i18n/fr-FR.json';
import de from '@/services/i18n/de-DE.json';
import hi from '@/services/i18n/hi-IN.json';
export default function RootLayout() {
const resources = {
en,
pt,
ru,
zh,
de,
es,
fr,
hi,
};
const [ languageLoaded, setLanguageLoaded ] = useState(false)
const [ language, setLanguage ] = useState<string | null>()
useEffect(() => {
if (!language || languageLoaded) return;
i18n.use(initReactI18next).init({
compatibilityJSON: 'v3',
resources,
lng: language,
fallbackLng: 'en',
});
setLanguageLoaded(true);
}, [language, languageLoaded]);
useEffect(() => {
const getSystemLanguageAndSet = async () => {
const phoneLocale = Localization.getLocales()?.[0]?.languageTag ?? 'en-US';
setLanguage(phoneLocale);
};
getSystemLanguageAndSet();
}, [])
if (!languageLoaded) {
return null
}
return <App />
}
Translate the content
To translate our content, we'll use the useTranslation hook provided by react-i18next.
Here's a simple component that uses the hook and the t method to translate a key.
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useTranslation } from 'react-i18next';
export default function SimpleExample = () => {
const { t } = useTranslation();
return (
<View style={styles.main}>
<Text>{t('hello')}</Text>
</View>
);
};
const styles = StyleSheet.create({
main: {
width: 356,
top: 123,
bottom: 342,
backgroundColor: 'red',
},
});
Here, you can see the key 'hello' being used as the argument in the t method. Based on what locale your device is set to, i18next will look in the appropriate JSON file, find the key 'hello', and display the appropriate localized / translated value for that key.
Android caveat
For iOS, the app reloads when the system locale is changed. This isn't true for Android. So, for Android, you'll need to listen for changes to the AppState and manually change the language with i18next.
Add the following useEffect to you root _layout . Don't forget to import AppState and Platform from 'react-native.
useEffect(() => {
if (Platform.OS !== 'android') return;
const _handleAppStateChange = async () => {
i18n.changeLanguage(
Localization.getLocales()?.[0]?.languageTag) ?? 'en-US'
);
};
const subscription = AppState.addEventListener(
'change',
_handleAppStateChange
);
return () => {
subscription.remove();
};
}, []);
Notice we use the i18n.changeLanguage method to change the language that our i18n instance has stored. We'll leverage this method in a future article where we change the language within the app instead of only relying on the system locale settings.
That's all for now!
Join me in a part 2 where we use AsyncStorage to set our language outside of our system locale settings.
Thanks for reading! Cheers.