React Native Multi-Language Support

React Native is the next best thing in cross-platform mobile development. Being under Facebook’s wings it is rapidly gaining the attention of developers of all backgrounds, be it native developers or web developers, resulting in a quite large community already. I am one of those web developers who wants to contribute to that community by writing this tutorial. It is a tutorial on how to support different languages and locales in your React Native app, additionally as a user-specific setting.

React Native

Needed requirements

First initialize a React Native app with:

  1. React Native >= 0.40 (At the time of writing this post React Native’s Current Version was 0.42 and is also the version being used in this tutorial.)
  2. Redux (redux, react-redux, reduxsauce, redux-saga, redux-persist, redux-logger and seamless-immutable)
  3. react-native-i18n
  4. rn-translate-template
  5. React Navigation (At the time of writing this post React Navigation’s Current Version was 1.0.0-beta.7 and is also the version being used in this tutorial.)

You can also use this sample repo to follow the tutorial.

First Steps

Import the custom I18n.js file (which came with rn-translate-template and initializes the I18n configuration) in the most top-level application file (or at least in an application file above all other files - files where redux and react-native-i18n are used). Also consider importing the I18n.js file before importing other components, to prevent those other components from crashing when they use the react-native-i18n plugin.

// App.js
import '../I18n/I18n' // import this before RootContainer as RootContainer is using react-native-i18n, and I18n.js needs to be initialized before that!
import RootContainer from './RootContainer'
I import I18n.js in my App.js file because App.js is used in index.android.js and in index.ios.js, which makes it a top-level application file, and because it does not use redux and react-native-i18n. As you can see, I import it before the RootContainer component. I do this because I want the I18n configuration to be available at the root of my app before I make use of react-native-i18n’s translate function in components (such as RootContainer).

Define I18n.fallbacks = true in the custom I18n.js file. When the device’s locale is not available in the defined translations, it will fall back to the default locale. This is ‘en’ (English) by default but we can adjust this by, for example, defining I18n.defaultLocale = 'nl'.

If your app does not support English, it is imperative to change the defaultLocale’s value!

Adding translations

Now that we have I18n initialized in our app, we can add translations. We can do this in our I18n.js file by requiring the translation files:

// I18n.js
I18n.translations = {
  en: require('./english.json'),
  nl: require('./nl.json')
}

After adding these translations, we can delete the remaining code in the I18n.js template file.

That remaining code (a switch based on the locale) does almost the same as the translations object we just defined, the difference being the remaining code would require the one translation file of the device’s language (if available). When we would want to switch the language in our app we would have to import translations at that moment, and our code would become a mess. With the translations object we can keep things clear, keep the configuration in one place and have the translations ready to use.

Language as a user-specific setting

At this point our app will be able to take over the device’s language on startup, given the according translation file is added in the translations object. If not, it will use the language defined in I18n’s defaultLocale parameter. In order to let the user switch the language in the app and successfully render the newly chosen language we will be using redux and sagas. This part of the tutorial explains how to define the app’s language as a user setting.

We will use a ‘Settings’ reducer connected to a saga to set I18n.locale to the newly chosen language.

// ../Redux/SettingsRedux.js
import { createReducer, createActions } from 'reduxsauce'
import Immutable from 'seamless-immutable'
import I18n from 'react-native-i18n'

/* ------------- Types and Action Creators ------------- */
const {Types, Creators} = createActions({
  changeLanguage: ['language']
})

export const SettingsTypes = Types
export default Creators

/* ------------- Initial State ------------- */
export const INITIAL_STATE = Immutable({
  language: I18n.locale.substr(0, 2) // take over the recognized, or default if not recognized, language locale as initial state
})

/* ------------- Reducers ------------- */
export const changeLanguage = (state, {language}) => state.merge({
  language
})

/* ------------- Hookup Reducers To Types ------------- */
export const reducer = createReducer(INITIAL_STATE, {
  [Types.CHANGE_LANGUAGE]: changeLanguage,
})

Create the saga that will update I18n.locale:

// ../Sagas/SettingsSaga.js
import I18n from 'react-native-i18n'

export function* updateLanguage(action) {
  const {language} = action
  I18n.locale = language
}

Connect the action from the reducer with the saga:

// ../Sagas/index.js
import { takeLatest } from 'redux-saga/effects'

/* ------------- Types ------------- */
import { StartupTypes } from '../Redux/StartupRedux'
import { SettingsTypes } from '../Redux/SettingsRedux'

/* ------------- Sagas ------------- */
import { startup } from './StartupSagas'
import { updateLanguage } from './SettingsSaga'

/* ------------- Connect Types To Sagas ------------- */
export default function* root() {
  yield[
    takeLatest(StartupTypes.STARTUP, startup),
    takeLatest(SettingsTypes.CHANGE_LANGUAGE, updateLanguage)
  ]
}

In the application’s ‘Startup’ saga we will have to call the ‘changeLanguage’ action, which uses the language parameter from the ‘Settings’ reducer, to enforce the correct language.

// ../Sagas/StartupSagas.js
import { put, select } from 'redux-saga/effects'
import SettingsActions from '../Redux/SettingsRedux'

// get the language from the settings reducer
export const selectLanguage = state => state.settings.language 

// process STARTUP actions
export function* startup(action) {
  const language = yield select(selectLanguage)

  // Always set the I18n locale to the language in the settings, or the views would render in the language of the device's locale and not that of the setting.
  yield put(SettingsActions.changeLanguage(language))
}

Next create a view where the user can choose an other language. Use the I18n.translations object to construct the options for the language picker. Connect the picker’s onValueChange property with the ‘changeLanguage’ action and the connected saga will take care of re-defining I18n.locale with the chosen language. Finally, declare the picker’s selectedValue with the current language property, to set the current language as the default chosen option when the user enters this view.

// ../Containers/SettingsContainer.js
render(){
  const {language, changeLanguage} = this.props
  const {setParams} = this.props.navigation
  const languageOptions = Object.keys(I18n.translations).map((lang, i) => {
    return (<Picker.Item key={ i }
                         label={ I18n.translations[lang].id }
                         value={ lang } />)
  })

  return(<Picker selectedValue={ language }
                 onValueChange={ this._languageChanged(changeLanguage, setParams) }>
           { languageOptions }
         </Picker>)
}
_languageChanged = (changeLanguage, setParams) => (newLang) => {
  changeLanguage(newLang)
}

We can fill in the labels of the picker’s options by using an ‘id’ parameter in the language files defining the name of the language, e.g. "id": "English" in en.json and "id": "Nederlands" in nl.json.

There is now one more thing left to do: each view that implements I18n and is rendered on startup needs to have the language setting connected to the view. We will need to use this language property in our I18n.translate function like so: I18n.t(‘my.word’, { locale: this.props.settings.language }). (Take note of the locale parameter being set in the translation function.)

This is necessary because the views rendered on startup will not be aware of the locale being set in the startup saga. These views will have rendered before that (it’s a race condition) so by mapping the language setting to the props of those views, we can trigger their translation and show the views in the correct language.

If you’re not sure if the view will be rendered on startup you can connect the view and map the property just in case, there should not be any performance issues.

React Navigation Specific Code

In this tutorial and sample we use React Navigation’s StackNavigator which means we will have to write additional code to update the titles of the views.

Set up the stack navigation and set up your views:

// ../Containers/RootContainer.js
const AppNavigator = StackNavigator({
  Home: {
    screen: WelcomeContainer,
    navigationOptions: {
      title: 'Multi Language Sample App' // we advice using something static like your app's name or company name on the startup screen
    }
  },
  Settings: {
    screen: SettingsContainer,
    navigationOptions: {
      title: (navigation) => {
        return navigation.state.params.title
      }
    }
  },
  About:{
    screen: About,
    navigationOptions: {
      title: (navigation) => {
        return navigation.state.params.title
      }
    }
  }
})

...

  render() {
    return (
      <AppNavigator />
    )
  }

We declare the titles of the views as a parameter which will be passed when navigating to a certain screen.

Unfortunately I have not yet found a way how to update the title of the root view. For now I use a static title such as a company name or the application’s name. If you have an idea let us know in the comments below!

Pass the title as an extra parameter in the navigate function:

// ../Containers/WelcomeContainer.js
  <MyButton buttonTitle={ I18n.t('home.go_to', { locale: language }).toUpperCase() + " " + I18n.t('settings.title', { locale: language }).toUpperCase() }
            onButtonPress={ () => navigate('Settings', {
                              title: I18n.t('settings.title', { locale: language }) // <- passing the title here
                            })
                          } />
  <MyButton buttonTitle={ I18n.t('home.go_to', { locale: language }).toUpperCase() + " " + I18n.t('about.title', { locale: language }).toUpperCase() }
            onButtonPress={ () => navigate('About', {
                              title: I18n.t('about.title', { locale: language }) // <- passing the title here
                            })
                          } />

One last thing, by using React Navigation’s setParams you can update the title of the ‘Settings’ view to a specific language. Note that we’re passing newLang as an extra parameter to force the translation function to use the newly chosen language.

// hljs
// ../Containers/SettingsContainer.js
_languageChanged = (changeLanguage, setParams) => (newLang) => {
  changeLanguage(newLang)
  // Add this:
  setParams({
    // switch language of the 'Settings' view's title
    title: I18n.t('settings.title', { locale: newLang }) 
  })
}

Sample

A sample project with the multi-language setup as described above can be found here.

Summary

An app that can adjust its language to the target system, and gives users the possibility to switch languages, increases the accessibility of the app. Such an app will be ranked higher in the app stores and consequently gain more attention of the smartphone users than an app with a fixed language. I hope this blog post has been informative enough to show you how to implement this in React Native apps and helps you getting your app out there!