React Native localization and internationalization

React Native app localization tutorial with react-native-localize

In this tutorial, we will learn how to implement React Native localization and internationalization.

Internationalization (I18n) makes it simpler to translate our React Native app into multiple languages. Once internationalization is performed, we will see how to perform React Native localization by supporting English and Russian languages. Finally, we will understand that localization is not just about translating to another language, but keeping it’s semantics intact by properly performing interpolation and pluralization.

The source code for the sample app we are going to build can be found on GitHub.

This post was initially penned by Akshay Kadam.

    One World, Dozens of Languages

    When you create an app, the primary language you choose is usually English. But here’s the thing:

    Out of the world’s approximately 7.5 billion inhabitants, 1.5 billion speak English — that’s 20% of the Earth’s population. However, most of those people aren’t native English speakers. About 360 million people speak English as their first language.

    So, only 360 million people out of 7.5 billion people are fluent in English. The rest of them use English as their second or third language. The market for other languages combined is much larger than that of English alone. Yet we only support English.

    It’s fine to test the market with English when the majority of your target audience is English-speaking. However, when your target audience consists of other languages then you have to translate your app. As your app grows, translating becomes more and more important to support the local audience.

    It is vital for people to use applications of all kinds without having to learn yet another language. We have to give up the idea of making people learn English in advance just to use a computer and have access to all of its programs. As the barrier to entry to make an app or starting a business is getting increasingly low, the fastest way to diversify yourself is by providing translation in the language of your target audience.

    Why Should You Translate Your App?

    According to CSA Research,

    • 50% of all queries on Google are in languages other than English.
    • 7 of the 10 top markets by iOS downloads and 9 of the 10 top markets by Google Play downloads are non-native English markets.
    • 78% of online shoppers are more likely to make a purchase on online stores that are localized.
    • About 72.1% of internet users prefer to dwell on websites translated into their native language.
    • In Sweden – which has one of the world’s best non-native English speakers – over 80% of online shoppers prefer to make a purchase in their own language.
    • Even among people with high proficiency in English, 60.6% prefer to browse the World Wide Web in their native language.
    • Around 90% of online shoppers choose their native language when it’s available.
    • Nearly 75% of Internet users prefer to read product information in their native language.

    Basically, you can’t sell in English to your non-English speakers. If they can’t read, they won’t buy. You need to translate your app into their local language in order for them to understand and then buy your product.

    Distomo found that localized apps generated up to 128% more ROI than non-localized apps.

    Differences Between Internationalization and Localization

    Definition of Internationalization (I18n)

    I believe W3C said it best,

    Internationalization is the design and development of a product, application or document content that enables easy localization for target audiences that vary in culture, region, or language.

    In simpler words, internationalization is the process of designing a software application to adapt it to various languages and regions without engineering changes, thus enabling localization. It makes the process of implementing localization much simpler.

    Fun fact, i18n is a numeronym for “internationalization” as there are exactly 18 characters between i and n.

    What Tasks Does I18n Involve?

    • Elimination of hard-coded text (strings) in the code.
    • Colocating hard-coded text in a single place for easier editing.
    • Support for right-to-left (RTL) languages, such as Urdu, Arabic, Hebrew, and Persian.
    • Having enough breathing space as phrases in different languages have varying lengths.
    • Unicode compliance.

    Benefits of I18n

    • Single source of truth.
    • Reduced time to localize software in other languages.
    • Maintenance becomes easy for future iterations of the software.
    • Customer satisfaction.

    Definition of Localization (L10n)

    W3C provides a great definition for localization:

    Localization refers to the adaptation of a product, application or document content to meet the language, cultural and other requirements of a specific target market (a locale).”

    In simpler words, localization is the process of adapting internationalized software for a specific region or language by translating text and adding locale-specific components. Therefore, in order to implement localization, we must first implement internationalization.

    L10n is also a numeronym for “localization” as there are exactly 10 characters between l and n.

    What Tasks Does L10n Involve?

    • Support for appropriate date format, e.g. French locale uses dd/mm/yyyy convention whereas Canadian locale uses yyyy-mm-dd convention.
    • Support for appropriate time format, e.g. German, French, Romanian use 24-hour clock format even while speaking whereas people from some other countries use 12-hour clock format.
    • Support for appropriate calendar format, e.g, Wednesday, 20 November 2019 or 20/11/2019.
    • Optimizing graphics and messaging to meet the tastes and habits of the market.

    If L10n is not done right, it can be an epic failure and sometimes hilarious as well.

    Benefits of l10n

    • Increase market share.
    • Increase revenue.
    • Gain a competitive advantage.
    • Build customer rapport.
    • Strengthen global presence.
    • More return on investment (ROI).

    Prerequisites

    Expected Knowledge

    For this tutorial, you need a basic knowledge of React Native and its concepts like state, props, hooks.

    Additionally, you’ll require a basic knowledge of how to setup a simple bottom tab navigation using React Navigation.

    This tutorial also assumes that you know how to use UI libraries like React Native Elements for faster prototyping.

    Installation

    Throughout the course of this tutorial, we’ll be using Yarn. If you don’t have yarn on your PC, install it from the official website.

    Also, make sure you’ve already installed react-native-cli globally on your computer:

    $ yarn global add react-native-cli

    To make sure we’re on the same page, these are the versions used in this tutorial:

    • Node 12.12.0
    • NPM 6.11.3
    • Yarn 1.19.1
    • react-native-cli 2.0.1
    • react-native 0.61.4

    Creating Application

    We are going to build a simple Shopping Cart app in order to demonstrate React Native localization and internationalization.

    Go ahead and clone the starter kit from GitHub. Then switch to the starter branch:

    $ git clone git@github.com:deadcoder0904/TranslateRNApp.git
    $ cd TranslateRNApp
    $ git checkout starter

    To run the project, type:

    $ yarn start

    In another terminal, while keeping the one above running, type the following:

    $ yarn ios (for iOS)

    $ yarn android (for Android)

    This will automatically run the iOS Simulator even if it’s not yet opened.

    Note that the emulator must be already started before running the above command. Otherwise it will throw an error in the terminal.

    Entry Point

    Let’s take a look at the initial code. Our main entry point is index.js:

    import {AppRegistry} from 'react-native';
    import App from './App';
    import {name as appName} from './app.json';
    
    AppRegistry.registerComponent(appName, () => App);

    index.js just includes App component from App.js.

    Implementing Routes

    Our App.js looks like:

    import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
    import {NavigationNativeContainer} from '@react-navigation/native';
    import React from 'react';
    import {Platform, StatusBar} from 'react-native';
    import {ThemeProvider} from 'react-native-elements';
    import {SafeAreaProvider} from 'react-native-safe-area-context';
    import Ionicons from 'react-native-vector-icons/Ionicons';
    import {Home} from './screens/Home';
    import {Settings} from './screens/Settings';
    
    const Tab = createBottomTabNavigator();
    
    const isIOS = Platform.OS === 'ios';
    
    const App = () => (
      <>
        <NavigationNativeContainer>
          <StatusBar barStyle="dark-content" />
          <SafeAreaProvider>
            <ThemeProvider>
              <Tab.Navigator
                screenOptions={({route}) => ({
                  tabBarIcon: ({focused, color, size}) => {
                    let iconName;
                    if (route.name === 'Home') {
                      iconName = `${isIOS ? 'ios' : 'md'}-information-circle${
                        focused ? '' : '-outline'
                      }`;
                    } else if (route.name === 'Settings') {
                      iconName = `${isIOS ? 'ios' : 'md'}-options`;
                    }
                    // You can return any component that you like here!
                    return <Ionicons name={iconName} size={size} color={color} />;
                  },
                })}
                tabBarOptions={{
                  activeTintColor: 'tomato',
                  inactiveTintColor: 'gray',
                }}>
                <Tab.Screen name="Home" component={Home} />
                <Tab.Screen name="Settings" component={Settings} />
              </Tab.Navigator>
            </ThemeProvider>
          </SafeAreaProvider>
        </NavigationNativeContainer>
      </>
    );
    
    export default App;

    We use @react-navigation/bottom-tabs to add bottom tabs navigation in react-navigation to switch between screens.

    We use react-native-safe-area-context to ensure our app remains confined between device screens on both Android and iOS.

    The difference between SafeAreaProvider and react-native‘s SafeAreaView is that SafeAreaView only works for iOS while SafeAreaProvider works for both Android and iOS.

    We then use ThemeProvider from react-native-elements to make sure default theme is applied from react-native-elements. react-native-elements allows us to quickly create beautiful components with an intuitive API without having to write too many styles.

    Later, we use Tab.Navigator with the Tab.Screen inside. The createBottomTabNavigator() function from @react-navigation/bottom-tabs returns Tab.Navigator and Tab.Screen.

    Tab.Navigator keeps track of which route we are currently at. Tab.Screen focuses on which component to display when a specific route is navigated. For example, when a route with the name Home is selected, it shows the Home component. When a route with the name Settings is selected, it shows the Settings component.

    In Tab.Navigator, we have two props, namely, screenOptions and tabBarOptions:

    • screenOptions displays an appropriate icon depending on the platform. If the platform is iOS then it adds the prefix ios to it. When the platform is Android then the prefix is md. The icons are used from the Ionicons component which is imported from react-native-vector-icons.
    • tabBarOptions displays gray color when a particular bottom tab is inactive and it displays a tomato color when a particular bottom tab is active.

    So, to summarize, we have two bottom tabs named Home and Settings which switch between Home component and Settings component respectively.

    Implementation of the Home Screen

    Our Home component looks like:

    import React, {useState} from 'react';
    import {ScrollView, StyleSheet, View} from 'react-native';
    import {Text} from 'react-native-elements';
    import {useSafeArea} from 'react-native-safe-area-context';
    import {Tile} from '../components/Tile';
    
    const fruits = [
      {
        name: 'Apple',
        price: '$3',
        pic: require('../assets/apple.png'),
      },
      {
        name: 'Banana',
        price: '$2',
        pic: require('../assets/banana.png'),
      },
      {
        name: 'Watermelon',
        price: '$5',
        pic: require('../assets/watermelon.png'),
      },
    ];
    
    export const Home = () => {
      const [total, changeTotal] = useState(0);
      const insets = useSafeArea();
    
      const addToTotal = price => {
        changeTotal(total + price);
      };
    
      const removeFromTotal = price => {
        changeTotal(total - price);
      };
    
      return (
        <ScrollView>
          <View
            style={[
              styles.container,
              {paddingTop: insets.top, paddingBottom: insets.bottom},
            ]}>
            <Text h1 h1Style={styles.grocery}>
              Grocery Shop
            </Text>
            <Text style={styles.date}>Today's date: 21/11/2019</Text>
            {fruits.map(fruit => {
              return (
                <React.Fragment key={fruit.name}>
                  <Tile
                    fruit={fruit}
                    addToTotal={addToTotal}
                    removeFromTotal={removeFromTotal}
                  />
                </React.Fragment>
              );
            })}
            <Text h3 h3Style={styles.total}>
              Total Sum: ${total}
            </Text>
          </View>
        </ScrollView>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        marginTop: 10,
      },
      grocery: {
        textAlign: 'center',
        marginBottom: 10,
      },
      date: {
        textAlign: 'center',
        marginBottom: 20,
        fontSize: 20,
      },
      total: {
        marginTop: 30,
        textAlign: 'center',
        color: 'red',
      },
    });

    And here’s our Tile component:

    import React, {useState} from 'react';
    import {Dimensions, StyleSheet, View} from 'react-native';
    import {Icon, Image, Text} from 'react-native-elements';
    
    const {width} = Dimensions.get('window');
    
    export const Tile = ({fruit, addToTotal, removeFromTotal}) => {
      const [cart, changeCart] = useState(0);
      const {name, pic, price} = fruit;
      const fruitPrice = +fruit.price.substring(1);
    
      const addToCart = () => {
        const noOfItems = cart + 1;
        changeCart(noOfItems);
        addToTotal(fruitPrice);
      };
    
      const removeFromCart = () => {
        if (cart > 0) {
          const noOfItems = cart - 1;
          changeCart(noOfItems);
          removeFromTotal(fruitPrice);
        }
      };
    
      return (
        <>
          <Image
            resizeMode="contain"
            source={pic}
            style={{width, height: width * 0.8}}
          />
          <View style={styles.flex}>
            <Icon name="pluscircleo" type="antdesign" onPress={addToCart} />
            <Text h4 h4Style={styles.name}>
              {name} ({price})
            </Text>
            <Icon name="minuscircleo" type="antdesign" onPress={removeFromCart} />
          </View>
          <Text style={styles.cart}>Added {cart} items</Text>
        </>
      );
    };
    
    const styles = StyleSheet.create({
      flex: {
        display: 'flex',
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
      },
      name: {
        margin: 20,
      },
      cart: {
        textAlign: 'center',
      },
    });

    The Home component should now look like this:

    Preparing the Application - Home Screen Part 1
    Preparing the Application - Home Screen Part 2

    Implementation of the Settings Screen

    Now lets see how our Settings component looks like:

    import React, {useState} from 'react';
    import {StyleSheet, View} from 'react-native';
    import {ListItem, Text} from 'react-native-elements';
    import {useSafeArea} from 'react-native-safe-area-context';
    
    const langs = ['en', 'ru'];
    
    export const Settings = () => {
      const [lang, changeLang] = useState('en');
      const insets = useSafeArea();
      return (
        <View style={[styles.container, {paddingTop: insets.top}]}>
          <Text h4 h4Style={styles.language}>
            Change Language
          </Text>
          {langs.map((currentLang, i) => (
            <ListItem
              key={i}
              title={currentLang}
              bottomDivider
              checkmark={currentLang === lang}
              onPress={() => changeLang(currentLang)}
            />
          ))}
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      language: {
        paddingTop: 10,
        textAlign: 'center',
      },
    });

    This screen contains only a language switcher allowing us to choose the desired language. The selected language has a checkmark to the right. Notice, this doesn’t have any effect as we are yet to implement localization.

    It currently displays:

    React Native Localization - Settings Screen

    Implementing React Native Internationalization

    Now we are going to tweak our Translation app to add support for I18n.

    Go ahead and create a localization/ folder. Inside it, create en.json and ru.json files.

    Creating Translation Files

    en.json will contain our English translations. Go ahead and paste the following:

    {
      "shop.title": "Grocery Shop",
      "date.title": "Today's Date",
      "date.format": "21/11/2019",
      "app.currency": "$",
      "fruit.apple": "Apple",
      "fruit.apple.price.value": 3,
      "fruit.banana": "Banana",
      "fruit.banana.price.value": 2,
      "fruit.watermelon": "Watermelon",
      "fruit.watermelon.price.value": 5,
      "added.items.one": "Added {no} item",
      "added.items.endingWithZero": "Added {no} items",
      "added.items.endingWithOne": "Added {no} item",
      "added.items.endingWithTwoToFour": "Added {no} items",
      "added.items.endingWithOther": "Added {no} items",
      "cart.total.title": "Total Sum",
      "cart.total.value.currencyStart": "{currencyStart}{value}",
      "cart.total.value.currencyEnd": "{value}{currencyEnd}",
      "settings.change_language": "Change Language"
    }
    

    ru.json is going to store Russian translations. Its contents should be:

    {
      "shop.title": "Продуктовый магазин",
      "date.title": "Сегодняшняя дата",
      "date.format": "21.11.19",
      "app.currency": "₽",
      "fruit.apple": "яблоко",
      "fruit.apple.price.value": 10,
      "fruit.banana": "Банан",
      "fruit.banana.price.value": 7,
      "fruit.watermelon": "Арбуз",
      "fruit.watermelon.price.value": 25,
      "cart.total": "Итого",
      "added.items.one": "добавлено {no} товар",
      "added.items.endingWithZero": "добавлено {no} товаров",
      "added.items.endingWithOne": "добавлено {no} товар",
      "added.items.endingWithTwoToFour": "добавлено {no} товара",
      "added.items.endingWithOther": "добавлено {no} товара",
      "cart.total.title": "Итого",
      "cart.total.value.currencyStart": "{currencyStart}{value}",
      "cart.total.value.currencyEnd": "{value}{currencyEnd}",
      "settings.change_language": "Изменить язык"
    }
    

    We have created translation files in JSON format containing keys with the corresponding values for each language our app is going to support. The {} syntax in the values denote interpolation. Variables with that name will be interpolated with their value.

    Before proceeding, make sure to install @react-native-community/async-storage, react-native-localization and react-native-localize by using the following command:

    $ yarn add @react-native-community/async-storage react-native-localization react-native-localize

    React Native Localization

    Next let’s see how to introduce React Native localization. Create Translations.js file in the components/ folder and paste the following code inside:

    import AsyncStorage from '@react-native-community/async-storage'; // 1
    import React, {createContext, useState} from 'react';
    import LocalizedStrings from 'react-native-localization'; // 2
    import * as RNLocalize from 'react-native-localize'; // 3
    import en from '../localization/en.json';
    import ru from '../localization/ru.json';
    
    const DEFAULT_LANGUAGE = 'en';
    const APP_LANGUAGE = 'appLanguage';
    
    const languages = {en, ru};
    
    const translations = new LocalizedStrings(languages); // 4
    
    export const LocalizationContext = createContext({ // 5
      translations,
      setAppLanguage: () => {}, // 6
      appLanguage: DEFAULT_LANGUAGE, // 7
      initializeAppLanguage: () => {}, // 8
    });
    
    export const LocalizationProvider = ({children}) => { // 9
      const [appLanguage, setAppLanguage] = useState(DEFAULT_LANGUAGE);
    
      // 11
      const setLanguage = language => {
        translations.setLanguage(language);
        setAppLanguage(language);
        AsyncStorage.setItem(APP_LANGUAGE, language);
      };
    
      // 12
      const initializeAppLanguage = async () => {
        const currentLanguage = await AsyncStorage.getItem(APP_LANGUAGE);
    
        if (currentLanguage) {
          setLanguage(currentLanguage);
        } else {
          let localeCode = DEFAULT_LANGUAGE;
          const supportedLocaleCodes = translations.getAvailableLanguages();
          const phoneLocaleCodes = RNLocalize.getLocales().map(
            locale => locale.languageCode,
          );
          phoneLocaleCodes.some(code => {
            if (supportedLocaleCodes.includes(code)) {
              localeCode = code;
              return true;
            }
          });
          setLanguage(localeCode);
        }
      };
    
      return (
        <LocalizationContext.Provider
          value={{
            translations,
            setAppLanguage: setLanguage, // 10
            appLanguage,
            initializeAppLanguage,
          }}>
          {children}
        </LocalizationContext.Provider>
      );
    };

    What This Code Means

    1. We use @react-native-community/async-storage to store our selected language in the local database. This way, the next time we open the app, we use that language automatically without having to select it again.
    2. We use react-native-localization to internationalize React Native application. This library uses a native library to get the current interface language. Then it loads and displays the strings matching the current interface locale or the default language if a specific localization can’t be found.
    3. react-native-localize provides a toolbox for React Native app localization. It lets us find available locales, time, country, and calendar. It even searches for the best available languages from our device so that we can use it to apply localization.
    4. translations contain all our localized translations depending on the language specified. We pass it through LocalizedStrings method from react-native-localization.
    5. We create a constant named LocalizationContext using React Native’s createContext API. Here we pass in translations, setAppLanguage, appLanguage, and initializeAppLanguage.
    6. setAppLanguage is an empty function at the moment.
    7. appLanguage has a default value of en (English locale).
    8. initializeAppLanguage is also an empty function at the moment.
    9. In our LocalizationProvider method, we take children as a prop and return them while wrapping with LocalizationContext.Provider. Inside this function, we fill in the empty functions, i.e, setAppLanguage and initializeAppLanguage.
    10. setAppLanguage maps to the setLanguage in the value prop of LocalizationContext.Provider.
    11. setLanguage takes in a language to be set as the primary language. It then calls the setLanguage API on translations to get the translations of the specified language. It then sets the local state appLanguage using React‘s useState hook to pass it along with LocalizationContext.Provider‘s value. Finally, it saves the primary language to the database using AsyncStorage.setItem(). This way, the next time our users open the app, the previously selected language is chosen automatically.
    12. initializeAppLanguage firstly gets the currentLanguage using AsyncStorage.getItem(). It then checks if currentLanguage is set. If yes — it calls setLanguage. Otherwise, it matches the language that has available translations with the nearest language that is set on our device and then it calls setLanguage.

    Finally, we export both LocalizationContext and LocalizationProvider so we can import them throughout our app.

    Importing LocalizationProvider

    Open up App.js and import LocalizationProvider from ./components/Translations and wrap ThemeProvider with LocalizationProvider as follows:

    import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
    import {NavigationNativeContainer} from '@react-navigation/native';
    import React from 'react';
    import {Platform, StatusBar} from 'react-native';
    import {ThemeProvider} from 'react-native-elements';
    import {SafeAreaProvider} from 'react-native-safe-area-context';
    import Ionicons from 'react-native-vector-icons/Ionicons';
    import {LocalizationProvider} from './components/Translations';
    import {Home} from './screens/Home';
    import {Settings} from './screens/Settings';
    
    const Tab = createBottomTabNavigator();
    
    const isIOS = Platform.OS === 'ios';
    
    const App = () => (
      <>
        <NavigationNativeContainer>
          <StatusBar barStyle="dark-content" />
          <SafeAreaProvider>
            <LocalizationProvider>
              <ThemeProvider>
                <Tab.Navigator
                  screenOptions={({route}) => ({
                    tabBarIcon: ({focused, color, size}) => {
                      let iconName;
                      if (route.name === 'Home') {
                        iconName = `${isIOS ? 'ios' : 'md'}-information-circle${
                          focused ? '' : '-outline'
                        }`;
                      } else if (route.name === 'Settings') {
                        iconName = `${isIOS ? 'ios' : 'md'}-options`;
                      }
                      // You can return any component that you like here!
                      return <Ionicons name={iconName} size={size} color={color} />;
                    },
                  })}
                  tabBarOptions={{
                    activeTintColor: 'tomato',
                    inactiveTintColor: 'gray',
                  }}>
                  <Tab.Screen name="Home" component={Home} />
                  <Tab.Screen name="Settings" component={Settings} />
                </Tab.Navigator>
              </ThemeProvider>
            </LocalizationProvider>
          </SafeAreaProvider>
        </NavigationNativeContainer>
      </>
    );
    
    export default App;

    This makes sure everything inside the LocalizationProvider component has access to the translations so that we can use them as well as change the language.

    Localization of the Settings Screen

    Now open up Settings.js and paste the following:

    import React, {useContext} from 'react';
    import {StyleSheet, View} from 'react-native';
    import {ListItem, Text} from 'react-native-elements';
    import {useSafeArea} from 'react-native-safe-area-context';
    import {LocalizationContext} from '../components/Translations';
    
    export const Settings = () => {
      const insets = useSafeArea();
      const {
        translations,
        appLanguage,
        setAppLanguage,
        initializeAppLanguage,
      } = useContext(LocalizationContext); // 1
      initializeAppLanguage(); // 2
    
      return (
        <View style={[styles.container, {paddingTop: insets.top}]}>
          <Text h4 h4Style={styles.language}>
            {translations['settings.change_language']} {/* 3 */}
          </Text>
          {translations.getAvailableLanguages().map((currentLang, i) => ( {/* 4 */}
            <ListItem
              key={i}
              title={currentLang}
              bottomDivider
              checkmark={appLanguage === currentLang}
              onPress={() => {
                setAppLanguage(currentLang);
              }}
            />
          ))}
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      language: {
        paddingTop: 10,
        textAlign: 'center',
      },
    });
    1. We import LocalizationContext from ./components/Translations.
    2. We then call React‘s useContext method to get back translations, appLanguage, setAppLanguage, and initializeAppLanguage.
    3. Then, we call initializeAppLanguage. This makes sure our initial language is set. If we have never opened the app before or have not selected a different language before, then the English (en) language gets set by default. If we have already selected a different language, that language will be selected instead of English.
    4. We then employ {translations['settings.change_language']} instead of Change Language inside Text. The key settings.change_language comes from our translation files key.
    5. Next, we utilize translations.getAvailableLanguages() to get all the available languages whose translation files we have specified. So in our case, we get en and ru. We display them in a loop by using ListItem from react-native-elements. Lastly, we set checkmark to true when appLanguage from Context is equal to currentLang.

    Localization of the Home Screen

    Now go ahead and open up Home.js. Paste the following contents inside:

    import React, {useContext, useState} from 'react';
    import {ScrollView, StyleSheet, View} from 'react-native';
    import {Text} from 'react-native-elements';
    import {useSafeArea} from 'react-native-safe-area-context';
    import {Tile} from '../components/Tile';
    import {LocalizationContext} from '../components/Translations';
    
    export const Home = () => {
      const {translations, initializeAppLanguage} = useContext(LocalizationContext);
      const [total, changeTotal] = useState(0);
      const insets = useSafeArea();
      initializeAppLanguage(); // 1
    
      // 2
      const fruits = [
        {
          // 3
          name: translations['fruit.apple'],
          price:
            translations['app.currency'] + translations['fruit.apple.price.value'],
          pic: require('../assets/apple.png'),
        },
        {
          name: translations['fruit.banana'],
          price:
            translations['app.currency'] + translations['fruit.banana.price.value'],
          pic: require('../assets/banana.png'),
        },
        {
          name: translations['fruit.watermelon'],
          price:
            translations['app.currency'] +
            translations['fruit.watermelon.price.value'],
          pic: require('../assets/watermelon.png'),
        },
      ];
    
      // 4
      const addToTotal = price => {
        changeTotal(total + price);
      };
    
      const removeFromTotal = price => {
        changeTotal(total - price);
      };
    
      return (
        <ScrollView>
          <View
            style={[
              styles.container,
              {paddingTop: insets.top, paddingBottom: insets.bottom},
            ]}>
            {/* 5 */}
            <Text h1 h1Style={styles.grocery}>
              {translations['shop.title']}
            </Text>
            <Text style={styles.date}>
              {translations['date.title']}: {translations['date.format']}
            </Text>
            {fruits.map(fruit => {
              return (
                <React.Fragment key={fruit.name}>
                  <Tile
                    fruit={fruit}
                    addToTotal={addToTotal}
                    removeFromTotal={removeFromTotal}
                  />
                </React.Fragment>
              );
            })}
            <Text h3 h3Style={styles.total}>
              {/* 6 */}
              {translations['cart.total.title']}:
              {translations.formatString(
                translations['cart.total.value.currencyStart'],
                {
                  currencyStart: translations['app.currency'],
                  value: total,
                },
              )}
            </Text>
          </View>
        </ScrollView>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        marginTop: 10,
      },
      grocery: {
        textAlign: 'center',
        marginBottom: 10,
      },
      date: {
        textAlign: 'center',
        marginBottom: 20,
        fontSize: 20,
      },
      total: {
        marginTop: 30,
        textAlign: 'center',
        color: 'red',
      },
    });
    1. Firstly, we initialize app language by calling initializeAppLanguage.
    2. We move our fruits array inside the component since we need to use translations.
    3. Then, enable our fruits array to use translations. For apple, we set name to translations['fruit.apple']. We concatenate translations['app.currency'] and translations['fruit.apple.price.value'] for price. Also we keep the pic same as before. The reason we separate currency and value is because different locales use different currencies. We perform the same operations for banana and watermelon.
    4. Lastly, declare {translations['shop.title']}, {translations['date.title']}, and {translations['date.format']}. Later, we loop over our fruits and pass each fruit to our custom made Tile component with addToTotal and removeFromTotal.

    The Tile Screen

    Finally, to stitch it all together we need to internationalize our final file Tile.js:

    import React, {useContext, useState} from 'react';
    import {Dimensions, StyleSheet, View} from 'react-native';
    import {Icon, Image, Text} from 'react-native-elements';
    import {LocalizationContext} from './Translations';
    
    const {width} = Dimensions.get('window');
    
    const translateNumber = (translations, cart) => {
      const key = 'added.items.';
      if (cart === 1) {
        return translations.formatString(translations[key + 'one'], {
          no: cart,
        });
      }
    
      if (
        (cart % 10 === 0 ||
          cart % 10 === 5 ||
          cart % 10 === 6 ||
          cart % 10 === 7 ||
          cart % 10 === 8 ||
          cart % 10 === 9) &&
        (cart % 100 === 11 ||
          cart % 100 === 12 ||
          cart % 100 === 13 ||
          cart % 100 === 14)
      ) {
        return translations.formatString(translations[key + 'endingWithZero'], {
          no: cart,
        });
      }
    
      if (cart % 10 === 1 && cart % 100 !== 11) {
        return translations.formatString(translations[key + 'endingWithOne'], {
          no: cart,
        });
      }
    
      if (
        (cart % 10 === 2 || cart % 10 === 3 || cart % 10 === 4) &&
        (cart % 100 !== 12 || cart % 100 !== 13 || cart % 100 !== 14)
      ) {
        return translations.formatString(
          translations[key + 'endingWithTwoToFour'],
          {
            no: cart,
          },
        );
      }
    
      return translations.formatString(translations[key + 'endingWithOther'], {
        no: cart,
      });
    };
    
    export const Tile = ({fruit, addToTotal, removeFromTotal}) => {
      const {translations} = useContext(LocalizationContext);
    
      const [cart, changeCart] = useState(0); // 1
      const {name, pic, price} = fruit;
      const fruitPrice = +fruit.price.substring(1);
    
      // 2
      const addToCart = () => {
        changeCart(cart + 1);
        addToTotal(fruitPrice);
      };
    
      const removeFromCart = () => {
        if (cart > 0) {
          changeCart(cart - 1);
          removeFromTotal(fruitPrice);
        }
      };
    
      return (
        <>
          <Image
            resizeMode="contain"
            source={pic}
            style={{width, height: width * 0.8}}
          />
          <View style={styles.flex}>
            {/* 3 */}
            <Icon name="pluscircleo" type="antdesign" onPress={addToCart} />
            <Text h4 h4Style={styles.name}>
              {name} ({price})
            </Text>
            <Icon name="minuscircleo" type="antdesign" onPress={removeFromCart} />
          </View>
          {/* 4 */}
          <Text style={styles.cart}>{translateNumber(translations, cart)}</Text>
        </>
      );
    };
    
    const styles = StyleSheet.create({
      flex: {
        display: 'flex',
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
      },
      name: {
        margin: 20,
      },
      cart: {
        textAlign: 'center',
      },
    });
    
    1. Here, we use a local state cart to keep track of the items number for each fruit using React‘s useState hook. Then we destructure name, pic, and price from fruit. Next, we get fruitPrice by removing the currency from the start using substring(). Lastly convert string to number using +.
    2. We have two functions, namely addToCart and removeFromCart. In addToCart, we first increase the value of the cart variable (stored in the local state) by calling changeCart. Then we call addToTotal which delegates to addToTotal (inside the parent component). In removeFromCart, we first check if the items in the cart are greater than zero. If yes, decrease our local state by calling changeCart with cart - 1. Finally, we call removeFromTotal which delegates to the removeFromTotal function.
    3. Later, we display + icon and along with the fruit name and its price. Also display the - icon. Tapping the + icon calls the addToCart function. Tapping the - icon calls the removeFromCart function.
    4. Finally, we call the translateNumber function. It accepts the translations and cart arguments while returning the appropriate translations. This process is known as pluralization. The conditions here are tricky since Russian language has quite complex pluralization rules which you may find at Unicode CLDR website. This process gets easier if you are using a localization service like Lokalise.

    Now Russian Translations should appear as well when you toggle the language.

    React Native Localization - Settings Screen Russian
    React Native Localization - Home Screen Part 1 Russian
    React Native Localization - Home Screen Part 2 Russian

    Make Translations Easy With Lokalise

    So, we have learned how to add support for React Native localization. However, it is quite painful to support different languages while working on a big project. Localization is hard but it shouldn’t be. That’s where Lokalise comes in.

    Lokalise provides a translation editor that is collaborative so different translators can collaborate on a single-project in real-time without having to worry about keep translations file in sync.

    It also allows us to preview translations in real-time without having to wait for the next deployment.

    Moreover, Lokalise has nice tools allowing to integrate with different services like GitHub, Slack, JIRA, Sketch, and others.

    Setting up Lokalise for React Native is really easy. Follow these steps:

    • To get started, grab your free trial.
    • Download and install Lokalise CLIv2 that will be used to upload and download translation files.
    • Open your personal profile page, navigate to the “API tokens” section, and generate a read/write token.
    • Create a new project, give it some name, and set English as a base language.
    • On the project page click the “More” button and choose “Settings”. On this page, you should see the project ID.
    • To upload your translations files, run lokalise2 file upload --token <token> --project-id <project_id> --lang-iso en --file localization/en.json while providing your generated token and project ID. Note that on Windows you may also need to provide the full path to the file. This should upload English translation to Lokalise. Run the same command for the Russian locale (while providing the proper --lang-isoand --file options).
    • Navigate back to the project overview page. You should see all your translation keys and values there. Of course, it is possible to edit, delete them, as well as add new ones. Here you may also filter the keys and, for example, find the untranslated ones which is really convenient.
    • After you are done editing the translations, download them back by running lokalise2 file download --token <token> --project-id <project_id> --bundle_structure %LANG_ISO%.json --unzip_to ~/TranslateRNApp/localization/.

    That’s it. Check out the official documentation for the command line interface to learn about other commands and options.  As you see, Lokalise reduces the number of complex tasks and make translation a piece of cake.

    Conclusion

    In this tutorial, we’ve seen React Native localization example by building a Shopping Cart app and by supporting two languages, English and Russian. We have also learned how to perform React Native internationalization. Later, we have added support for different currencies and different date formats. Finally, we learned about interpolation and pluralization.

    Localization is tricky to get right. But once it’s done, your business, reach and sales will increase exponentially.

    Further reading

    Make translation easy with Lokalise

    Grab a FREE Lokalise trial and collaborate in real-time without having to worry about keeping translation files in sync
    Start now

    Related articles
    Stop wasting time with manual localization tasks. 

    Launch global products days from now.