Localization in React Native
A Guide for Building Multi-Language Mobile Apps with Expo

Search for a command to run...
A Guide for Building Multi-Language Mobile Apps with Expo

No comments yet. Be the first to comment.
💡 This is the third part of the "Collecting payments" series. Before proceeding, make sure to read Part 1, Part 2 & Part 3 We've finally reached the last part of the series! With all the tedious aspects taken care of, we just need to connect everyt...

💡 This is the third part of the "Collecting payments" series. Before proceeding, make sure to read Part 1 and Part 2 Before diving into this third part, I highly recommend watching this video to become familiar with some essential RevenueCat conce...

💡 This is the third part of the "Collecting payments" series. Before proceeding, make sure to read Part 1 SpiroKit for SaaS In case you want to save weeks of painful hours of work & research on your next React Native app, checkout SpiroKit for Sa...

Introduction Creating a mobile app involves numerous challenges and time-consuming tasks that must be completed before we can release our app in the stores. In-app purchases and subscriptions are especially difficult examples of this. They require co...

Building apps take time Building a mobile app is a challenging task. From coming up with an idea, prototyping, and finally building the app, there are many things to consider and problems we'll have to figure out: How do we make sure the entire app ...

In today's connected world, it's crucial for mobile apps to provide proper support for users who speak different languages.
In this article, we are going to build a fully-functioning React Native app that supports multiple languages, allowing users to even switch between different languages based on their preferences.
As a bonus, we’ll also build a custom hook to deal with localized dates.
If you want to jump directly into the code, here's the Snack
Localization refers to the process of adapting an application to meet the specific language, cultural, and regional requirements of different target audiences. The process of implementing localization usually starts with translating the text of the UI, but it can go beyond and consider other preferences like date and time formats, currency symbols, measurement units, and more.
By implementing localization, mobile apps can reach a broader audience and enhance user engagement. It allows users to interact with the app in their native language, making it more accessible, intuitive, and enjoyable for them
Let’s start by creating an empty react native using Expo. I’ll be using the expo-template-blank-typescript to avoid dealing with TypeScript configuration.
npx create-expo-app -t expo-template-blank-typescript
Once the project is ready, cd into your project and run the following command in the root to add the required dependencies
npx expo install expo-localization i18next react-i18next
By using the npx expo install command, we make sure that we get the right version of each package for the Expo SDK version we are using. At the time I’m writing this article, the latest SDK version is 48.
We’ll use expo-localization to get the user’s preferred locale, i18next to simplify the process of translating and displaying text in different languages, and react-i18next as an extension of the i18next library that includes a list of convenient hooks and components we can use while translating a react app.
To setup localization in our Expo app, let’s start by creating a i18n.ts file at the root of the project.
# Create an empty file
touch i18n.ts
Feel free to choose whatever name you want for the file.
We also need to create a translate folder where we’ll add a separate json file for each supported language. In this article, I’ll add support for English and Spanish.
mkdir translate
touch translate/es.json
touch translate/en.json
In the new json files, make sure to initialize both json files to avoid annoying errors when setting up localizations later in the article.
translate/en.json
{
greetings: "Hello world"
}
translate/es.json
{
greetings: "Hola mundo"
}
Next, let’s add the following code in the new i18n.ts file to initialize i18next.
//i18n.ts
import i18n from "i18next"
import { initReactI18next } from "react-i18next"
import es from "./translate/es.json"
import en from "./translate/en.json"
import * as Localization from "expo-localization";
const getLangCode = () => {
const code = Localization.getLocales().shift();
if (!code) return "en";
return code.languageCode;
};
i18n.use(initReactI18next)
.init({
compatibilityJSON: "v3",
lng: getLangCode(),
defaultNS: "translation",
interpolation: {
// We disable this because it's not required, given that react already scapes the text
escapeValue: false
},
// Here you can add all your supported languages
resources: {
es: es,
en: en
}
})
export default i18n
Here are a few things to mention about this block of code:
We use initReactI18next to initialize and setup i18next on our expo app.
We retrieve the current device locales using expo-localization and extract the language code from the first locale.
We are using the compatibilityJSON setting to make sure the default behavior is to use a JSON parser that supports advanced features such as nested objects and pluralization.
We are disabling text escaping in the interpolation to rely on React’s built-in escaping.
We are defining the resource files for each supported language.
We set “translation” as the default namespace
Finally, we export the i18n instance to use it later.
Thanks to react-i18next, we can wrap our entire app using the I18nextProvider. Here are a few benefits to mention:
Centralized Configuration: The I18nextProvider allows you to centralize the configuration of i18next in a single place. By wrapping the app with the provider, you can set up and configure i18next once and have it accessible throughout your entire application.
Context Propagation: The I18nextProvider utilizes React's Context API to provide the i18next instance and translations to all nested components. This eliminates the need to pass down the i18next instance manually through props to every component that requires access to translations.
Efficient Updates: The I18nextProvider manages the i18next instance and ensures that updates to language and translations propagate efficiently to the components that depend on them. It optimizes the rendering process by selectively re-rendering only the affected components when language changes occur.
Simplified Usage: Wrapping the app with the I18nextProvider simplifies the usage of i18next within components. It eliminates the need to manually import and initialize i18next in every component that requires translations, as the provider takes care of this automatically.
Future-Proofing: Using the I18nextProvider ensures that your application is prepared for any future updates or changes to the i18next library. It provides a standardized and recommended way to integrate i18next with React, ensuring compatibility and ease of maintenance.
Let’s update the App.tsx file to include the provider:
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
+ import i18n from "./i18n";
+ import { I18nextProvider } from "react-i18next";
export default function App() {
return (
+ <I18nextProvider i18n={i18n}>
<View style={styles.container}>
<Text>Open up App.tsx to start working on your app!</Text>
<StatusBar style="auto" />
</View>
+ </I18nextProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
With everything in place, it’s time to use our localization keys inside a React component.
First, we need to create a new folder and an empty file for the card component:
mkdir components
touch components/Card.tsx
Add the following code to the Card component
// components/Card.tsx
import React from "react";
import { View, Text, StyleSheet } from "react-native";
type CardProps = {
title: string;
content: string;
};
const Card: React.FC<CardProps> = ({ title, content }) => {
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.content}>{content}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
borderRadius: 8,
padding: 16,
marginBottom: 16,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 2,
},
title: {
fontSize: 18,
fontWeight: "bold",
marginBottom: 8,
},
content: {
fontSize: 16,
lineHeight: 22,
},
});
export default Card;
Now, let’s update the App.tsx to add a few instances of the Card component:
import { StatusBar } from 'expo-status-bar';
- import { StyleSheet, Text, View } from 'react-native';
+ import { StyleSheet, SafeAreaView, View } from 'react-native';
import i18n from "./i18n";
- import { I18nextProvider } from "react-i18next";
+ import { I18nextProvider, useTranslation } from "react-i18next";
export default function App() {
return (
<I18nextProvider i18n={i18n}>
- <View style={styles.container}>
- <Text>Open up App.tsx to start working on your app!</Text>
- <StatusBar style="auto" />
- </View>
+ <Home />
+ <StatusBar style="auto" />
</I18nextProvider>
);
}
+ const Home = () => {
+ const { t } = useTranslation("cards");
+ return (
+ <SafeAreaView style={{ flex: 1 }}>
+ <View style={styles.container}>
+ <Card title={t("cardTitle")} content={t("cardDescription")} />
+ <Card
+ title={t("anotherCardTitle")}
+ content={t("anotherCardDescription")}
+ />
+ </View>
+ </SafeAreaView>
+ );
+ };
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
Finally, let’s update our resource files to include all the required localization keys:
translate/en.json
{
"cards": {
"cardTitle": "Card Title",
"cardDescription": "Card Description",
"anotherCardTitle": "Another Card Title",
"anotherCardDescription": "Another Card Description"
}
}
translate/es.json
{
"cards": {
"cardTitle": "Titulo",
"cardDescription": "Descripción",
"anotherCardTitle": "Otro Titulo",
"anotherCardDescription": "Otra Descripción"
}
}
Run npm start and you should see all the card content localized based on your preferences.
To wrap up this article, let’s add a new component that will list all the supported languages, allowing the user to switch between them.
Create a new file for the component by executing the following command:
touch components/LangSwitcher.tsx
Update the new empty file with the following code
// components/LangSwitcher.tsx
import { useTranslation } from "react-i18next";
import { TouchableOpacity, Text, View, StyleSheet } from "react-native";
const LangSwitcher = () => {
const { i18n, t } = useTranslation("supportedLanguages");
const supportedLanguages = ["en", "es"];
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<View style={styles.container}>
<Text style={styles.title}>Choose your language</Text>
{supportedLanguages.map((lng) => (
<TouchableOpacity
onPress={() => changeLanguage(lng)}
style={styles.item}
>
<Text style={styles.itemText}>{t(lng)}</Text>
</TouchableOpacity>
))}
</View>
);
};
// improve styles
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
padding: 16,
},
title: {
marginBottom: 16,
fontSize: 24,
fontWeight: "bold",
},
item: {
width: "100%",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#ccc",
},
itemText: {
fontSize: 16,
textAlign: "center",
},
});
export default LangSwitcher;
Update the resource files, so all the supported languages in the switcher are localized 😉
translate/en.json
{
"cards": {
"cardTitle": "Card Title",
"cardDescription": "Card Description",
"anotherCardTitle": "Another Card Title",
"anotherCardDescription": "Another Card Description"
},
+ "supportedLanguages": {
+ "en": "English",
+ "es": "Spanish"
+ }
}
translate/es.json
{
"cards": {
"cardTitle": "Titulo",
"cardDescription": "Descripción",
"anotherCardTitle": "Otro Titulo",
"anotherCardDescription": "Otra Descripción"
},
+ "supportedLanguages": {
+ "en": "Inglés",
+ "es": "Español"
+ }
}
App.tsx file to add the new component...
+ import LangSwitcher from "./components/LangSwitcher";
...
const Home = () => {
const { t } = useTranslation("cards");
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
...
+ <LangSwitcher></LangSwitcher>
</View>
</SafeAreaView>
);
};
To further improve your multi-language support, you can also provide localized string for your app metadata. This includes things like:
Your app display name
System dialogs (every time you request permissions like location, camera, etc).
app.json file to set CFBundleAllowMixedLocalizations to true like this:{
"expo": {
+ "ios": {
+ "infoPlist": {
+ "CFBundleAllowMixedLocalizations": true
+ }
+ },
}
}
locales object, where you’ll define the path to a json file for each supported language.{
"expo": {
"ios": {
"infoPlist": {
"CFBundleAllowMixedLocalizations": true
}
},
+ "locales": {
+ "en": "./metadata/en.json",
+ "es": "./metadata/es.json",
+ }
}
}
Create the new folder and required files with the following command
mkdir metadata
touch metadata/en.json
touch metadata/es.json
Update each json file to include your localized metadata. Here’s an example: metadata/en.json
{
"CFBundleDisplayName": "My cooking app",
"NSCameraUsageDescription": "The app need to use the camera so you can take photos of your dishes",
"NSLocationWhenInUseUsageDescription": "The app needs access to your location to share the address of your favorite restorants"
}
metadata/es.json
{
"CFBundleDisplayName": "Mi aplicación de cocina",
"NSCameraUsageDescription": "La aplicación necesita usar la cámara para que puedas tomar fotos de tus platos",
"NSLocationWhenInUseUsageDescription": "La aplicación necesita acceso a tu ubicación para compartir la dirección de tus restaurantes favoritos"
}
Just for fun, let’s combine expo-localization with the built-in method toLocaleString() to localize our dates.
Update the Card component to include an additional prop called date
import React from "react";
import { View, Text, StyleSheet } from "react-native";
+ import * as Localization from "expo-localization";
type CardProps = {
title: string;
content: string;
+ date: Date;
};
const Card: React.FC<CardProps> = ({ title, content, date }) => {
+ // Get the user's preferred locale
+ const locale = Localization.getLocales().shift();
+ const languageCode = locale?.languageCode || "en";
+ const localizedDate = date.toLocaleString(languageCode, {
+ dateStyle: "long",
+ timeStyle: "long",
+ });
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.content}>{content}</Text>
+ <Text style={styles.content}>{localizedDate}</Text>
</View>
);
};
Now, you should be able to see a localized date. You can even go a step further and add a convenient hook to encapsulate this logic, so you don’t need to add all this boilerplate every time you want to display a localized date.
Besides, the code provided above has a catch: If the user changes the language of the app in runtime, the localized string won’t update.
Let’s fix that problem with the useLocalizedDate hook
First, we need to create a new folder and an empty file for the new hook.
mkdir hooks
touch hooks/useLocalizedDate.tsx
Now, add the following code for the hook
// hooks/useLocalizedDate.tsx
import * as Localization from "expo-localization";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
const useLocalizedDate = (
date: Date,
options: Intl.DateTimeFormatOptions = {}
) => {
const { i18n } = useTranslation();
const defaultOptions: Intl.DateTimeFormatOptions = {
dateStyle: "long",
timeStyle: "long",
};
const [localizedDate, setLocalizedDate] = useState<string>("");
// When the component mounts, set the initial value of the localized date and add a listener for language changes
useEffect(() => {
setInitialValue();
i18n.on("languageChanged", (lng) => {
setLocalizedDate(date.toLocaleString(lng, defaultOptions));
});
return () => {
i18n.off("languageChanged");
};
}, []);
const setInitialValue = () => {
// Merge the user's options with the default options
const mergedOptions = { ...defaultOptions, ...options };
// Get the user's preferred locale
const locale = Localization.getLocales().shift();
const languageCode = locale?.languageCode || "en";
// Localize the date
setLocalizedDate(date.toLocaleString(languageCode, mergedOptions));
};
return localizedDate as string;
};
export default useLocalizedDate;
In this improved version, we are also listening to i18next events to update the localized dates when the language of the user changes.
Finally, we can update the card component to use the new hook like this:
// components/Card.tsx
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import useLocalizedDate from "../hooks/useLocalizedDate";
type CardProps = {
title: string;
content: string;
date: Date;
};
const Card: React.FC<CardProps> = ({ title, content, date }) => {
const localizedDate = useLocalizedDate(date);
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.content}>{content}</Text>
<Text style={styles.content}>{localizedDate}</Text>
</View>
);
};
Implementing localization in mobile apps is essential for reaching a broader audience and enhancing user engagement. Thanks to libraries like expo-localization, i18next and react-i18next, it’s easier than ever to implement localization.
By providing support for multiple languages, your app will be more accessible, intuitive, and enjoyable for your users.