Localization in React Native

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 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

    • 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  1. First, we need to create a new folder and an empty file for the card component:

     mkdir components
     touch components/Card.tsx
    
  2. 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;
    
  3. 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',
  },
});
  1. 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.

  1. Create a new file for the component by executing the following command:

     touch components/LangSwitcher.tsx
    
  2. 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;
    
  3. 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"
+ }
}
  1. 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).

  1. Update your app.json file to set CFBundleAllowMixedLocalizations to true like this:
{
  "expo": {
+   "ios": {
+     "infoPlist": {
+       "CFBundleAllowMixedLocalizations": true
+     }
+   },
  }
}
  1. You also need to provide a 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",
+   }
  }
}
  1. Create the new folder and required files with the following command

     mkdir metadata
     touch metadata/en.json
     touch metadata/es.json
    
  2. 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

  1. First, we need to create a new folder and an empty file for the new hook.

     mkdir hooks
     touch hooks/useLocalizedDate.tsx
    
  2. 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.

  3. 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.