Internationalisation in React
Tags: react, i18n, i18next, internationalisation
Published: 2020-03-01 20:18:10

Internationalisation, also referred as i18n, allows you to translate your website in multiple languages. In this demo we persist the language through the local storage. Some other popular options are, the session storage and the cookies.

## Table Of Contents
 * [Template Project](#chapter-1)
 * [Integrate with i18next](#chapter-2)
 * [Serve Dynamic Content](#chapter-3)
 * [Multi-page Experience with Routing](#chapter-4)
 * [Bonus: Tricks for Boosting the App](#chapter-5)

The code for this demo could be found here:
{% github psideris89/react-i18n-demo %}

## Template Project <a name="chapter-1"></a>
Let's start by creating a react application with *create-react-app* and navigate to the project directory.

```
$ npx create-react-app react-i18n-demo
$ cd react-i18n-demo
```

The idea is to build 4 components:
- <u>*App*</u> the main component
- <u>*Language*</u> drop-down for language selection
- <u>*Intro*</u> displays text
- <u>*Flag*</u> displays an image.

### <u>Language</u>
For the *Language* component we are going to use the *material-ui* library and more specifically the *select* component which creates a drop-down.

Install material-ui/core.

```
$ npm install --save @material-ui/core
```

Create a *components* folder and inside it a *Language* folder to host the *Lanugage.js*
- src -> components -> Language -> Language.js

```javascript
import { FormControl, MenuItem, Select } from '@material-ui/core';
import React, { useState } from 'react';

const Language = () => {
  const [dropdownLang, setDropdownLang] = useState('en');

  const languageHandler = event => {
    const newLanguage = event.target.value;
    if (dropdownLang !== newLanguage) {
      setDropdownLang(newLanguage);
    }
  };

  return (
    <FormControl style={{ minWidth: 120 }}>
      <Select value={dropdownLang} onChange={languageHandler}>
        <MenuItem value="en">English</MenuItem>
        <MenuItem value="el">Ελληνικά</MenuItem>
        <MenuItem value="es">Español</MenuItem>
        <MenuItem value="it">Italiano</MenuItem>
      </Select>
    </FormControl>
  );
};

export default Language;
```

The selection is persisted in the state through the *useState* hook and the *languageHandler* allows us to update our selection.


### <u>Intro</u>
Same as before create an *Intro* folder and inside that the *Intro.js*.
- src -> components -> Intro -> Intro.js

```javascript
import React from 'react';

const Intro = () => {
  return <h1>Welcome to React!</h1>;
};

export default Intro;
```

### <u>Flag</u>
Similarly create a *Flag* folder and inside that the *Flag.js*. We also added the *en.svg*, which is a flag, in the same directory. Bear in mind that the name of the svg matters so please try to use the same one.
- src -> components -> Flag -> Flag.js
- src -> components -> Flag -> en.svg

```javascript
import React from 'react';
import flagSvg from './en.svg';

const Flag = () => {
  return <img style={{ maxWidth: '50%' }} src={flagSvg} alt="flag" />;
};

export default Flag;
```

### <u>App</u>
Finally we have the *App* which hosts all the above components.

```javascript
import React from 'react';
import Intro from './components/Intro/Intro';
import Flag from './components/Flag/Flag';
import Language from './components/Language/Language';
import './App.css';

const App = () => {
  return (
    <div className="App">
      <nav style={{ height: '60px', backgroundColor: 'rgb(253, 117, 19)' }}>
        <div
          style={{ float: 'right', marginTop: '20px', marginRight: '20px' }}
        >
          <Language />
        </div>
      </nav>
      <div style={{ marginTop: '50px' }}>
        <Intro />
      </div>
      <div style={{ display: 'inline-block', width: '500px' }}>
        <Flag />
      </div>
    </div>
  );
};

export default App;
```

At this point the template project is completed and you will be able to update the language from the drop-down. However, neither the message or the flag are updated, as they are hardcoded. The next step will be to integrate with i18next in order to render the content dynamically. Let's go ahead and run the application.

```
$ npm start
```

## Integrate with i18next <a name="chapter-2"></a>
### <u>Step 1</u>: Create the configuration for i18next
In order to implement i18n (internationalisation) in our application we need the following libraries:

- i18next
- react-i18next
- i18next-http-backend
- i18next-browser-languagedetector

```
$ npm install --save i18next react-i18next i18next-http-backend i18next-browser-languagedetector
```

Initially we need to configure/initialise **i18next**. The **i18next-http-backend** is responsible for loading the properties from the translation files. The **i18next-browser-languagedetector**, as the name implies, detects the language. We will come back to this library later on, as there are some interesting features we are going to use. Finally **react-18next** will be used widely in our project as it allows to read and update the i18next language.

In the root directory of the application create the file *i18n.js*. This is the configuration of react-i18next.

```javascript
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    preload: ['en'],
    load: 'languageOnly',
    fallbackLng: 'en',
    debug: true,
    lowerCaseLng: true,
    detection: {
      order: ['localStorage']
    }
  });

export default i18n;
```

We chose to initialise with English which is also the fallback language but you could specify another, as well as, pre-load multiple languages. Additionally we want to save the language in the local storage, hence we gave priority to that detector which is supported from the library. Bear in mind that you can have multiple detectors (even custom) and the order is descending (first will take precedence).

<u>Note</u>: Regarding the *Backend*, when a new language is set in i18next the relevant properties will be loaded. It's up to you if you want to preload more that one languages, or even not preload any. If you don't preload a language, then only if it's requested the properties will be loaded. You can view the relevant logs in the console, given that you have debug enabled.

Next import i18n.js in the index.js

```javascript
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import './i18n';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();
```

At this point you will be able see the i18next being initialised in the console.

![i18next debug log](https://dev-to-uploads.s3.amazonaws.com/i/j4az7bagng5xo0wkp1zj.png)

But how can you verify that the language is persisted with i18next? In your browser's developer console navigate to the Application (Storage if using Firefox) tab and then select Local Storage. You will see that the property i18nextLng is added there (the name can be overwritten in i18next configuration).

![local_storage_log](https://dev-to-uploads.s3.amazonaws.com/i/ydi9h1ki3ehmhelufhvz.png)

### <u>Step 2</u>: Create json files to host the translation messages.
By default i18next will attempt to parse the **translation.json** files under the *public/locales/{language}/* directories but you can overwrite that in the 18next configuration.

- public/locale/en/translation.json

```javascript
{
  "intro-title": "Welcome to React!"
}
```

Go ahead and create the files for the remaining languages.
- public/locale/el/translation.json
- public/locale/es/translation.json
- public/locale/it/translation.json

## Serve Dynamic Content <a name="chapter-3"></a>
At this point we are going to modify the *Language* component to update the i18next language based on selection. Then we will remove the hardcoded values for the message in *Input.js* and the image in *Flag.js*.

Let's start with *Language.js*. In order to update the i18next language we can use the *useTranslation* hook from react-i18next library, which provides the i18n interface. In case that you are using classes instead of functional components the equivalent will be the *withTranslation* higher order component.

**changeLanguage** function updates the i18next language, whereas **language** function returns the i18next language.

```javascript
import { FormControl, MenuItem, Select } from '@material-ui/core';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';

const Language = () => {
  const { i18n } = useTranslation();
  const [dropdownLang, setDropdownLang] = useState(i18n.language || 'en');

  const languageHandler = event => {
    const newLanguage = event.target.value;
    if (dropdownLang !== newLanguage) {
      setDropdownLang(newLanguage);
      i18n.changeLanguage(newLanguage);
    }
  };

  return (
    <FormControl style={{ minWidth: 120 }}>
      <Select value={dropdownLang} onChange={languageHandler}>
        <MenuItem value="en">English</MenuItem>
        <MenuItem value="el">Ελληνικά</MenuItem>
        <MenuItem value="es">Español</MenuItem>
        <MenuItem value="it">Italiano</MenuItem>
      </Select>
    </FormControl>
  );
};

export default Language;
```
Notice that we also updated the **dropdownLang** to be initialised from *i18next* language if defined. If you skip that configuration whenever you refresh the page the drop-down will display English even if the i18next language is different.

After implementing the previous step the application should throw an error. This is due to i18next requiring Suspense in order to work. The Suspense should be added one level higher than the component using it, so we are going to add it in App.js.

```javascript
import React, { Suspense } from 'react';
import Intro from './components/Intro/Intro';
import Flag from './components/Flag/Flag';
import Language from './components/Language/Language';
import './App.css';

const App = () => {
  return (
    <Suspense fallback={<p>Loading Translations ...</p>}>
      <div className="App">
        <nav style={{ height: '60px', backgroundColor: 'rgb(253, 117, 19)' }}>
          <div
            style={{ float: 'right', marginTop: '20px', marginRight: '20px' }}
          >
            <Language />
          </div>
        </nav>
        <div style={{ marginTop: '50px' }}>
          <Intro />
        </div>
        <div style={{ display: 'inline-block', width: '500px' }}>
          <Flag />
        </div>
      </div>
    </Suspense>
  );
};

export default App;
```

Now the application should be back up and running but how do you test that the language is actually updated? For that you can check the local storage in your browser!

Let's move on and remove the hardcoded content from *Intro.js*. Similarly we are going to use the *useTranslation* hook and invoke the **t** function which allows you to map to a property instead of specifying a value directly.

```javascript
import React from 'react';
import { useTranslation } from 'react-i18next';

const Intro = () => {
  const { t } = useTranslation();
  
  return <h1>{t('intro-title')}</h1>;
};

export default Intro;
```

In the *Flag.js* we are going to use the **language** function to retrieve the i18next language and display the relevant image.

```javascript
import React from 'react';
import { useTranslation } from 'react-i18next';

const Flag = () => {
  const { i18n } = useTranslation();

  const flagSvg = require(`./${i18n.language}.svg`);

  return <img style={{ maxWidth: '50%' }} src={flagSvg} alt="flag" />;
};

export default Flag;
```

At this point the application should allow you to update the language from the drop-down and the relevant message and image should be displayed.

In case the message is being replaced by *intro-title*, this is an indication that the i18next failed to read the value of this property or load that language. If a fallback language is specified, i18next will display the message from that language.

![error on load](https://dev-to-uploads.s3.amazonaws.com/i/crm81m88pmkk7tbg02p9.png)

## Multi-page Experience with Routing <a name="chapter-4"></a>
For that we will use the react-router-dom library.

```
$ npm install --save react-router-dom
```

The first step is to use the *BrowserRouter* component from react-router-dom and create a *Route* for the App component. This gives us access to the history object which will be used to manipulate the url when we select a language from the drop-down.

```javascript
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter, Route } from 'react-router-dom';

import * as serviceWorker from './serviceWorker';

import './i18n';

ReactDOM.render(
  <BrowserRouter>
    <Route path="/" component={App} />
  </BrowserRouter>,
  document.getElementById('root')
);

serviceWorker.unregister();
```

Next step is to update the *Language* component to modify the url on language switching. For that we need to use the *useHistory* hook from react-router-dom that allows us to access the history object. Then we update the handler to *push* the new url in history.

```javascript
import { FormControl, MenuItem, Select } from '@material-ui/core';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';

const Language = () => {
  const history = useHistory();
  const { i18n } = useTranslation();
  const [dropdownLang, setDropdownLang] = useState(i18n.language || 'en');

  const languageHandler = event => {
    const newLanguage = event.target.value;
    if (dropdownLang !== newLanguage) {
      setDropdownLang(newLanguage);
      i18n.changeLanguage(newLanguage);
      history.push("/" + newLanguage);
    }
  };

  return (
    <FormControl style={{ minWidth: 120 }}>
      <Select value= {dropdownLang} onChange={languageHandler}>
        <MenuItem value="en">English</MenuItem>
        <MenuItem value="el">Ελληνικά</MenuItem>
        <MenuItem value="es">Español</MenuItem>
        <MenuItem value="it">Italiano</MenuItem>
      </Select>
    </FormControl>
  );
};

export default Language;
```

Let's go back to our browser to test the application. You will notice that whenever you switch the drop-down language you get a new url! This is great but what if we want to prioritise the url language over the local storage. That means, whenever you use a url containing a language, i18next will be initialised accordingly. We can achieve that by adding another *detector* in the i18next configuration. The name of the detector is **path** and is responsible for extracting the language from the url (from path - not from query parameters).

```javascript
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    preload: ['en'],
    load: 'languageOnly',
    fallbackLng: 'en',
    debug: true,
    lowerCaseLng: true,
    detection: {
      order: ['path', 'localStorage']
    }
  });

export default i18n;
```

Now if you try the previous scenario you will notice that the language is updated!

*<u>Note</u>*: You might want to refactor the way you retrieve the svg in the Flag component, otherwise you will get an error for non supported language urls (e.g. localhost:3000/pt).

## Bonus: Tricks to boost your application <a name="chapter-5"></a>
- For root url (/), check if the local storage contains a value and if yes add a *Redirect* inside the *BrowserRouter* to reconcile the url. That covers the case where you previously visited the website (local storage contains a language) but you have bookmarked the root url of the application.

- Create your own custom detector and add it in the i18next configuration. The i18next-browser-languagedetector supports a vast majority of detectors but in case you need additional customisation you can have it.