How to Offer Multi-language Support in a React Native App

As a business grows, it's only natural to consider expansion into markets in other countries and regions as a next step. If you have an app, it's important to consider catering to the language needs of the communities you are expanding into as well—in app development, this is called Internationalization!

In this tutorial, we're going to go through the process of adding multi-language support to a React Native app using i18next. If you're unfamiliar with it, I18next is an internationalization framework written in and for JavaScript that provides a complete solution for localizing your product from web to mobile and desktop.

Prerequisites

To follow along with this tutorial, please make sure that you are 1) familiarized with JavaScript/ES6, 2) understand the basics of React Native, and 3) able to meet the following requirements in your local dev environment:

  • You have Node.js version 12.x.x or above installed
  • You have access to a package manager like npm, yarn, or npx
  • You have react-native-cli installed, or you are using npx

Depending on your skill level and experience, it may also be beneficial to brush up on how to scaffold a new custom mobile app with Crowdbotics prior to jumping into this tutorial.

Now that we've gotten all of that out of the way, let's get started!

Setting Up a React Native App

After initializing a React Native project, it's important to install the external libraries needed to properly follow along in this tutorial. To do this, navigate inside the project directory and run the following install command for the following libraries:

yarn add react-i18next i18next @react-navigation/native @react-navigation/bottom-tabs @react-native-async-storage/async-storage react-native-vector-icons react-native-screens react-native-safe-area-context react-native-reanimated react-native-localize react-native-gesture-handler

# after this step, for iOS, install pods
npx pod-install ios

We'll be using React Native Vector Icons for adding icons in our app and React Navigation to add and enable navigation between screens in the app. For React Navigation, make sure to initialize and configure navigation as described in the Getting Started documentation.

We will use the following libraries to add multi-language support to our app:

  • i18next: As previously mentioned, this is our internationalization library.
  • react-i18next: This library provides binding for React and React Native projects using Hooks, High Order Components (HOCs), etc. We will use the useTranslation hook to translate the text within our React Native function components.
  • react-native-localize: This library provides helper functions based on the device's localized language preferences.
  • @react-native-async-storage/async-storage: This is an unencrypted, asynchronous, persistent, key-value storage system that is global to the app. It is used to store the user's language preference so that it remains constant even when the app restarts.

🔥 Tip: It's always a good rule of thumb to check out the installation steps laid out in the documentation for the libraries you're installing in your React Native app. They are prone to changing over time, and it can be challenging to keep blog posts up-to-date with all of these changes.

Building a React Native app

Now that we've installed our libraries, let's set up our React Native app with mock screens and navigation.

Create an src/ folder in the project root directory and inside of that, create the following files and folders:

  • /constants
  • /translations
  • IMLocalize.js
  • /navigation
  • RootNavigator.js
  • /screens
  • /HomeScreen.js
  • SettingsScreen.js
  • /components
  • LanguageSelector.js

Next, add the RootNavigator.js file to the /navigation folder. The snipped below is used to create the "Home" and "Settings" tabs located in the navigation at the bottom of the screen, and it includes some configuration to display an icon and a label for each.

import * as React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Ionicons from 'react-native-vector-icons/dist/Ionicons';

import HomeScreen from '../screens/HomeScreen';
import SettingsScreen from '../screens/SettingsScreen';

const Tab = createBottomTabNavigator();

export default function RootNavigator() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;

            if (route.name === 'Home') {
              iconName = focused ? 'ios-home' : 'ios-home-outline';
            } else if (route.name === 'Settings') {
              iconName = focused ? 'ios-settings' : 'ios-settings-outline';
            }

            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: 'tomato',
          tabBarInactiveTintColor: 'gray',
          headerShown: false
        })}
      >
        <Tab.Screen name='Home' component={HomeScreen} />
        <Tab.Screen name='Settings' component={SettingsScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

Now that we have our nav bar, let's add some code snippets for the screens they correspond to. In HomeScreen.js, add the following code. For now, it will only display a Text component, but we'll be adding to this later in the tutorial:

import React from 'react';
import { Text, View } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home</Text>
    </View>
  );
}

Similarly, add the following snippet to the SettingsScreen.js—this file will also display a Text component for the time being:

import React from 'react';
import { Text, View } from 'react-native';

export default function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

Next, modify the App.js file to add the following code snippet:

import React from 'react';

import RootNavigator from './src/navigation/RootNavigator';

export default function App() {
  return <RootNavigator />;
}

At this point, if you run the npx react-native run-ios or npx react-native run-android command, you should see the following screen on a simulator/emulator or on a device:

Create Translation Files

Now, we need to translate our tab names based on the preferred language selected within the app. To do this, we'll need to create translation config files.

You can organize these translation files in whatever style you'd like, but it's important to follow some kind of pattern. Inside the constants/translations/ directory, create subdirectories for each language you'd like to support in this demo app. For our purposes, we're going to be using en for English and fr for French.

Inside each language directory, we'll need to create separate files that we can use to split the translations for the text that we'll use in our app, like the navigation tab labels we just created. Under i18n, this separation leads to creating namespaces for each language. Later in the tutorial, we'll discuss how to access the value of a key—for example, home from the namespace navigation to translate the tab bar label for the Home screen.

Here is how the directory structure will look under translations/:

Inside en/common.js file, add the following snippet:

export default {
  hello: 'Hello',
  languageSelector: 'Select Your Language'
};

Similarly, add the following code snippet to the fr/common.js file:

export default {
  hello: 'Bonjour',
  languageSelector: 'Sélecteur de langue'
};

Next, we'll want to add translated tab labels for each language in their corresponding navigate.js files:

// en/navigate.js
export default {
  home: 'Home!',
  settings: 'Settings'
};


// fr/navigate.js
export default {
  home: 'Écran principal',
  settings: 'Le réglage'
};

Finally, export these translated texts:

// en/index.js
import common from './common';
import navigate from './navigate';

export default {
  common,
  navigate
};

// fr/index.js
import common from './common';
import navigate from './navigate';

export default {
  common,
  navigate
};

try our app estimate calculator CTA image

Adding Multi-language Support Configuration

Now that we have translation files ready and dependencies installed, let's create a configuration using those libraries we installed previously.

This configuration will live inside the IMLocalize.js file, and we'll want to start off by importing the following dependencies. Also, we'll want to define our LANGUAGES constants—it saves each language file as an object and, using the JavaScript syntax Object.keys, will convert the LANGUAGES objects to an array.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as RNLocalize from 'react-native-localize';

import en from './translations/en';
import fr from './translations/fr';

const LANGUAGES = {
  en,
  fr
};

const LANG_CODES = Object.keys(LANGUAGES);

i18n is configured in a very specific way. It will check the user's stored language preference when the app starts, and it will default to the next available language that you suggest if the user's language preference is not available through your app. As a result, you'll need to define a fallback language.

To accomplish this, let's create a LANGUAGE_DETECTOR configuration object:

const LANGUAGE_DETECTOR = {
  type: 'languageDetector',
  async: true,
  detect: callback => {
    AsyncStorage.getItem('user-language', (err, language) => {
      // if error fetching stored data or no language was stored
      // display errors when in DEV mode as console statements
      if (err || !language) {
        if (err) {
          console.log('Error fetching Languages from asyncstorage ', err);
        } else {
          console.log('No language is set, choosing English as fallback');
        }
        const findBestAvailableLanguage =
          RNLocalize.findBestAvailableLanguage(LANG_CODES);

        callback(findBestAvailableLanguage.languageTag || 'en');
        return;
      }
      callback(language);
    });
  },
  init: () => {},
  cacheUserLanguage: language => {
    AsyncStorage.setItem('user-language', language);
  }
};

Once this is in place, add the configuration for i18n below. It will start by detecting the language, passing the i18n instance over to react-i18next, and then it will offer some other language recommends. This option makes i18n available for all React Native components.

i18n
  // detect language
  .use(LANGUAGE_DETECTOR)
  // pass the i18n instance to react-i18next.
  .use(initReactI18next)
  // set options
  .init({
    resources: LANGUAGES,
    react: {
      useSuspense: false
    },
    interpolation: {
      escapeValue: false
    }
  });

These options may vary depending on your React Native project, so I recommend going through all of the available configuration options for i18n.

Next, let's import the IMLocalize file to the App.js file:

// after other import statements
import './src/constants/IMLocalize';

Creating a Language Selector Component

Now that you have initialized the languages for your React Native app, the next step is to allow the user to select between different languages available within the app.

Inside the LanguageSelector.js file, start by importing the following libraries:

import React from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';
import Ionicons from 'react-native-vector-icons/dist/Ionicons';
import { useTranslation } from 'react-i18next';

The useTranslation hook will allow you to access the i18n instance inside the custom component used to change the language.

Next, define the array of LANGUAGES you plan on supporting:

const LANGUAGES = [
  { code: 'en', label: 'English' },
  { code: 'fr', label: 'Français' }
];

Once we've defined our language array, we'll want to define the function component Selector. This is what will allow the user to switch between different languages inside the app.

It will get the currently selected language from the i18n instance. Using a handler method called setLanguage, you can allow the functionality to switch between different languages from the LANGUAGES array defined above this function component.

This function component uses Pressable from React Native to change the language.

const LANGUAGES = [
  { code: 'en', label: 'English' },
  { code: 'fr', label: 'Français' }
];

const Selector = () => {
  const { i18n } = useTranslation();
  const selectedLanguageCode = i18n.language;

  const setLanguage = code => {
    return i18n.changeLanguage(code);
  };

  return (
    <View style={styles.container}>
      <View style={styles.row}>
        <Text style={styles.title}>Select a Language</Text>
        <Ionicons color='#444' size={28} name='ios-language-outline' />
      </View>
      {LANGUAGES.map(language => {
        const selectedLanguage = language.code === selectedLanguageCode;

        return (
          <Pressable
            key={language.code}
            style={styles.buttonContainer}
            disabled={selectedLanguage}
            onPress={() => setLanguage(language.code)}
          >
            <Text
              style={[selectedLanguage ? styles.selectedText : styles.text]}
            >
              {language.label}
            </Text>
          </Pressable>
        );
      })}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    paddingTop: 60,
    paddingHorizontal: 16
  },
  row: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between'
  },
  title: {
    color: '#444',
    fontSize: 28,
    fontWeight: '600'
  },
  buttonContainer: {
    marginTop: 10
  },
  text: {
    fontSize: 18,
    color: '#000',
    paddingVertical: 4
  },
  selectedText: {
    fontSize: 18,
    fontWeight: '600',
    color: 'tomato',
    paddingVertical: 4
  }
});

export default Selector;

Import the Selector component inside the SettingsScreen.js file:

import React from 'react';
import { View } from 'react-native';

import Selector from '../components/LanguageSelector';

export default function SettingsScreen() {
  return (
    <View style={{ flex: 1, backgroundColor: '#fff' }}>
      <Selector />
    </View>
  );
}

Here is the output in the simulator following this step:

Using the useTranslation Hook


The useTranslation hook has two important functions that you can utilize inside your React Native app. You have already seen the first one (i18n instance) in the previous step. The next is called t (my personal guess is that it is short for translation) function. Using this function, you can refer to the namespaces defined in the translation files and pass them as arguments to this function.

Let's see it in action! Starting with the LanguageSelector component itself, we can see that it has a title of Select a Language. When defining the translation files, we have already defined its translation in both English and French in their corresponding common.js files.

The first step to implementing the t function is to import the useTranslation hook, however, the LanguageSelector.js file already has it from the previous section.

We'll need to modify the following line to get the t function from the hook inside the Selector component:

const { t, i18n } = useTranslation();

Next, let's modify the Text component contents used to define the title:

<Text style={styles.title}>{t('common:languageSelector')}</Text>

In the emulator below, you'll see the result of these updates. The default, or initial language, in our case is English, but when you select "Français", it translates the title on the Settings screen to French.

You can also modify the text strings according to the previously defined namespaces in the translation files.

As an example, the RootNavigator can be modified as followed:

import * as React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Ionicons from 'react-native-vector-icons/dist/Ionicons';
import { useTranslation } from 'react-i18next';

import HomeScreen from '../screens/HomeScreen';
import SettingsScreen from '../screens/SettingsScreen';

const Tab = createBottomTabNavigator();

export default function RootNavigator() {
  const { t } = useTranslation();
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;

            if (route.name === 'Home') {
              iconName = focused ? 'ios-home' : 'ios-home-outline';
            } else if (route.name === 'Settings') {
              iconName = focused ? 'ios-settings' : 'ios-settings-outline';
            }

            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: 'tomato',
          tabBarInactiveTintColor: 'gray',
          headerShown: false
        })}
      >
        <Tab.Screen
          name='Home'
          component={HomeScreen}
          options={{ tabBarLabel: t('navigate:home') }}
        />
        <Tab.Screen
          name='Settings'
          component={SettingsScreen}
          options={{ tabBarLabel: t('navigate:settings') }}
        />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

Here is the final output:

Conclusion

This completes our tutorial on how to add multi-language support in a React Native app! If you'd like to add multi-language support for an app that you're building, Crowdbotics can help. Our expert team of professional developers and engineers are here to ensure that you are able to offer the language options that you and your users need. Get in touch with us today for a detailed quote and timeline!

Useful Links:

Originally published:

August 12, 2021