Franco Berton - September 7th, 2019

Implementing multi-language (i18n) without any library in react native

Index

Introduction: This article wants to be an introductory guide on how to build multi-language without any library in react native without external modules, describing the necessary steps and concluding through reflective analysis of the proposed solution.

Requirements: The precondition for this guide is the integration of react-redux to store the state and the translation.

1. Translation file

Create a folder (named i18n) to store all the internationalization file. This folder will contain JSON files, where each one of them will present different locale support. In this case, we define two JSON files to support the English language and the Italian language. Below, we submit the structure of the root folder:

ios
android
src
└── i18n
    ├── en.json // English
    └── it.json // Italian

Inside each file, we define the identifying properties of translations:

//en.json
{
  "selectLanguage": "Select language",
  "languageEn": "English",
  "languageIt": "Italian",
  "welcome": "Welcome",
  "hello": "Hello",
  "howareyou": "How are you?"
}

 //it.json
{
  "selectLanguage": "Seleziona il linguaggio",
  "languageEn": "Inglese",
  "languageIt": "Italiano",
  "welcome": "Benvenuto",
  "hello": "Ciao",
  "howareyou": "Come stai?"
}

2. Implementing an internationalization service

This step provides an internationalization service definition represented by a reducer and an action. The architectural structure of the service will look like this:

ios
android
src
└── services
    └── Intl
    │   ├── reducers.js
    │   ├── actions.js
    │   └── index.js
    ├── mapDispatchToProps.js
    ├── mapStateToProps.js
    └── index.js

The reducer will provide a method to set the translation of selected language. This method will be invoked by an action to handle the language translations.

Below, we submit the method to set the translations:

const setLanguage = language => {
  let messages = {};
  switch (language) {
    case 'it':
      messages = Object.assign(messages, require(`../../i18n/it.json`));
      break;
    default:
    case 'en':
      messages = Object.assign(messages, require(`../../i18n/en.json`));
      break;
  }
  return messages;
};

The previous method will be invoked by another method to handle the action to update the language translations:

const intlData = (state = initialState, action) => {
  if (action === undefined) return state;
  switch (action.type) {
    case 'UPDATE_LANGUAGE':
      return {
        locale: action.language,
        messages: setLanguage(action.language)
      };
    default:
      return state;
  }
};

In summary, the reducer will look like this:

//reducers.js
const setLanguage = language => {
  let messages = {};
  switch (language) {
    case 'it':
      messages = Object.assign(messages, require(`../../i18n/it.json`));
      break;
    default:
    case 'en':
      messages = Object.assign(messages, require(`../../i18n/en.json`));
      break;
  }
  return messages;
};

const initialState = {
  locale: 'it',
  messages: setLanguage('it')
};

const intlData = (state = initialState, action) => {
  if (action === undefined) return state;
  switch (action.type) {
    case 'UPDATE_LANGUAGE':
      return {
        locale: action.language,
        messages: setLanguage(action.language)
      };
    default:
      return state;
  }
};
export default intlData;

The action to update a language will be look this:

//actions.js
const updateLanguage = language => {
  return dispatch => {
    dispatch({
      type: 'UPDATE_LANGUAGE',
      language
    });
  };
};

export default {
  updateLanguage
};

Finally, inside an index file we export the reducer and the action:

//index.js
export { default as reducers } from './reducers';
export { default as actions } from './actions';

and we register the reducer inside the combineReducers function of redux to catch actions dispatched:

//services/index.js
import { combineReducers } from 'redux';
import { reducers as IntlReducers } from './Intl';

// Functions to catch actions dispatched
export default combineReducers({
  IntlReducers
});

3. Component definition for redux connection

So that the internationalization service can be shared among the various features, It is necessary to make a component to facilitate the connection to redux of the features. This component provides the sharing of actions and states about the invocation of actions.

It will be look this:

//ConnectedComponent/index.js
import { connect } from 'react-redux';

import mapStateToProps from '../../services/mapStateToProps';
import mapDispatchToProps from '../../services/mapDispatchToProps';

export default InputComponent => {
  // High Order Component
  return connect(
    mapStateToProps,
    mapDispatchToProps
  )(InputComponent);
};

The mapping of actions and states is defined in two distinct files:

//services/mapDispatchToProps.js
import { actions as IntlAction } from './Intl';
const mapDispatchToProps = {
  IntlAction
};
export default mapDispatchToProps;
//services/mapStateToProps.js
const mapStateToProps = state => {
  return {
    intlData: state.IntlReducers
  };
};
export default mapStateToProps;

4. Change language - component definition

Now, let's make the component to change language within the components folder, defining a structure like this:

ios
android
src
└── components
    └── LanguageSwitcher
    │   └── index.js
    └── index.js

In the component class, we define the method to change the language. It will call the action, extracted from props, to update the global state containing the language and the translations. The method will be look this:

_updateLanguage = lang => {
  const { updateLanguage } = this.props;

  updateLanguage(lang);
};

The change language component will be represented with Picker component to select the desired language. The class will look like this:

//LanguageSwitcher/index.js
import React, { Component } from 'react';
import { View, Picker } from 'react-native';

class LanguageSwitcher extends Component {
  constructor(props) {
    super(props);
  }

  _updateLanguage = lang => {
    const { updateLanguage } = this.props;

    updateLanguage(lang);
  };

  render() {
    const { intlData } = this.props;
    const languages = [
      { code: 'en', name: intlData.messages['languageEn'] },
      { code: 'it', name: intlData.messages['languageIt'] }
    ];
    const options = languages.map(language => {
      return (
        <Picker.Item
          value={language.code}
          key={language.code}
          label={language.name}
        />
      );
    });

    return (
      <View>
        <Text>{intlData.messages['selectLanguage']}</Text>
        <Picker
          selectedValue={intlData.locale}
          onValueChange={itemValue => this._updateLanguage(itemValue)}
        >
          {options}
        </Picker>
      </View>
    );
  }
}

export default LanguageSwitcher;

5. Connecting a feature to redux to share the translated contents

In this step, we make a feature called Settings, where it will be used the change language component and the translations. The feature will be connected to redux so that it can have the state containing the translations.

Below, the implementation of Settings component:

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { ConnectedComponent as connect } from '../../components';

class Settings extends Component {
  render() {
    const { intlData } = this.props;
    return (
      <View>
        <LanguageSwitcher {...this.props} />
        <Text>{intlData.messages['welcome']}</Text>
        <Text>{intlData.messages['hello']}</Text>
        <Text>{intlData.messages['howareyou']}</Text>
      </View>
    );
  }
}

export default connect(Settings);

6. Result

7. Conclusion

The proposed solution want to highlight, how is it possible, in just a few simple steps, to make an internationalization service self-made, able to satisfy standard exigencies of this feature.

This approach must not be seen as an alternative to module react-native-localize,but rather a basic solution self-made.

The source code of the proposed solution you can see on this Github Repository.

If you like my article, share it.

Your support and feedback mean so much to me.