Localization in React Native
A Guide for Building Multi-Language Mobile Apps with Expo
Introduction
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.
Final code
If you want to jump directly into the code, here's the Snack
What is localization?
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.
Benefits of implementing localization
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
Creating a new React Native project
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.
Setting up localization
Creating the required files
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
Adding localization keys for each supported language
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"
}
Configuring the i18n instance
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 setupi18next
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
- If you want to learn more about namespaces in i18next, checkout this article
Finally, we export the
i18n
instance to use it later.
Wrapping our app with the I18nextProvider
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 thei18next
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 ofi18next
within components. It eliminates the need to manually import and initializei18next
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',
},
});
Using localization keys within a React component
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 theCard
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.
Switching between languages
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"
+ }
}
- Finally, we need to update the
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>
);
};
Translating app metadata (iOS - Optional)
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).
- Update your
app.json
file to setCFBundleAllowMixedLocalizations
totrue
like this:
{
"expo": {
+ "ios": {
+ "infoPlist": {
+ "CFBundleAllowMixedLocalizations": true
+ }
+ },
}
}
- You also need to provide a
locales
object, where you’ll define the path to ajson
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" }
Bonus: Adding a localized date
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> ); };
Final thoughts
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.