Localizing the app in React Native

During the development of one of our applications, we needed to make multilingual support. The task was to give the user the ability to change the language (Russian and English) of the application interface. At the same time, text and content should be translated on the fly.



To do this, we had to solve 2 problems:



  1. Determine the current language of the application.
  2. Using global state for on-the-fly translation.


In this article I will try to describe in detail how we solved these problems. And so we went.



Determine the current device language



To determine the current language, you can, of course, use the react-native-i18n library, but we decided to do without it, since you can do this without third-party libraries. To do this, write the following:



import {NativeModules, Platform} from 'react-native';

let deviceLanguage = (Platform.OS === 'ios'
        ? NativeModules.SettingsManager.settings.AppleLocale ||
          NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13
        : NativeModules.I18nManager.localeIdentifier


For ios, we retrieve the application language via SettingsManager, and for android via the native I18nManager.



Now that we have received the current device language, we can save it to AsyncStorage and proceed to the second task.



We translate "on the fly"



We use MobX to manage global state, but you can use a different solution.



And so we have to create a class (I like to call it "model") that will be responsible for the global state of the current localization. We create:



//   ,      lang
const STORE = '@lang-store';
//    
const RU_LANGS = [
  'ru',
  'az',
  'am',
  'by',
  'ge',
  'kz',
  'kg',
  'md',
  'tj',
  'tm',
  'uz',
  'ua',
];

class LangModel {
  @observable
  lang = 'ru'; //  

  constructor() {
    this.init();
  }

  @action
  async init() {
    const lang = await AsyncStorage.getItem(STORE);
    if (lang) {
      this.lang = lang;
    } else {
      let deviceLanguage: string = (Platform.OS === 'ios'
        ? NativeModules.SettingsManager.settings.AppleLocale ||
          NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13
        : NativeModules.I18nManager.localeIdentifier
      ).toLowerCase();

      if (
        RU_LANGS.findIndex((rulang) => deviceLanguage.includes(rulang)) === -1
      ) {
        this.lang = 'en';
      }
      AsyncStorage.setItem(STORE, this.lang);
    }
}

export default new LangModel();


When initializing our model, we call the init method, which takes the locale either from the AsyncStorage, if there is one, or extract the current device language and puts it in the AsyncStorage.



Next, we need to write a method (action) that will change the language:



  @action
  changeLang(lang: string) {
    this.lang = lang;
    AsyncStorage.setItem(STORE, lang);
  }


I think everything is clear here.



Now comes the fun part. We decided to store the translations themselves in a simple dictionary. To do this, create a js file next to our LangModel, in which we will put our translations:



// translations.js
// ,     . 
export default const translations = {
  ", !": {en: "Hello, World!"},
}


Next, we will implement one more method in LangModel, which will accept text as input and return the text of the current localization:



import translations from './translations';

  ...
  rk(text) {
    if (!text) {
      return text;
    }
    //   ru,    
    if (this.lang === 'ru') {
      return text;
    }
    //   ,   
    if (translations[text] === undefined || translations[text][this.lang] === undefined) {
      console.warn(text);
      return text;
    }
    return translations[text][this.lang];
  }


That's it, our LangModel is ready.



Complete LangModel Code
import {NativeModules, Platform} from 'react-native';
import {observable, action} from 'mobx';
import AsyncStorage from '@react-native-community/async-storage';
import translations from './translations';

const STORE = '@lang-store';
//  ru  
const RU_LANGS = [
  'ru',
  'az',
  'am',
  'by',
  'ge',
  'kz',
  'kg',
  'md',
  'tj',
  'tm',
  'uz',
  'ua',
];

class LangModel {
  @observable
  lang = 'en';

  constructor() {
    this.init();
  }

  @action
  async init() {
    //     AsyncStorage
    const lang = await AsyncStorage.getItem(STORE);
    if (lang) {
      this.lang = lang;
    } else {
      let deviceLanguage: string = (Platform.OS === 'ios'
        ? NativeModules.SettingsManager.settings.AppleLocale ||
          NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13
        : NativeModules.I18nManager.localeIdentifier
      ).toLowerCase();

      if (
        RU_LANGS.findIndex((rulang) => deviceLanguage.includes(rulang)) > -1
      ) {
        this.lang = 'ru';
      }
      AsyncStorage.setItem(STORE, this.lang);
  }

  @action
  changeLang(lang: string) {
    this.lang = lang;
    AsyncStorage.setItem(STORE, lang);
  }

  rk(text) {
    if (!text) {
      return text;
    }
    //   ru,    
    if (this.lang === 'ru') {
      return text;
    }
    //   ,   
    if (translations[text] === undefined || translations[text][this.lang] === undefined) {
      console.warn(text);
      return text;
    }
    return translations[text][this.lang];
  }
}
export default new LangModel();




We can now use the rk method to localize the text:



<Text>{LangModel.rk(", !")}</Text>


You can see how it works in our application in the AppStore and Google Play (Click on the icon (!) In the upper right, scroll down)



Bonus



Of course, writing LangModel.rk every time is not cool. Therefore, we can create our own Text component and already use LangModel.rk in it



//components/text.js
import React from 'react';
import {Text} from 'react-native';
import {observer} from 'mobx-react';
import {LangModel} from 'models';

export const MyText = observer((props) => (
   <Text {...props}>{props.notTranslate ? props.children : LangModel.rk(props.children)}</Text>
));


We may also need, for example, to change the application logo depending on the current localization. To do this, you can simply change the content depending on LangModel.lang (do not forget to wrap your component in the observer (MobX))



PS: Perhaps this approach will seem not a standard, but we liked it more than the one offered by react-native-i18n



On that's all for me. Thanks to all!)



All Articles