DEV Community

Panos Sideris
Panos Sideris

Posted on • Updated on

Internationalisation in React

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

The code for this demo could be found here:

GitHub logo psideris89 / react-i18n-demo

React demo project for internationalisation (i18n)

Internationlisation in React

This is a demo application that demonstrates how to implement internationalisation (i18n) in React.

To run the application

$ npm install
$ npm start

The documentation for this demo could be found here: https://dev.to/psideris89/internationalisation-in-react-2ahl

Author Panagiotis Sideris

Twitter: @Psideris_




Template Project

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
Enter fullscreen mode Exit fullscreen mode

The idea is to build 4 components:

  • App the main component
  • Language drop-down for language selection
  • Intro displays text
  • Flag displays an image.

Language

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
Enter fullscreen mode Exit fullscreen mode

Create a components folder and inside it a Language folder to host the Lanugage.js

  • src -> components -> Language -> Language.js
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;
Enter fullscreen mode Exit fullscreen mode

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

Intro

Same as before create an Intro folder and inside that the Intro.js.

  • src -> components -> Intro -> Intro.js
import React from 'react';

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

export default Intro;
Enter fullscreen mode Exit fullscreen mode

Flag

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
import React from 'react';
import flagSvg from './en.svg';

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

export default Flag;
Enter fullscreen mode Exit fullscreen mode

App

Finally we have the App which hosts all the above components.

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Integrate with i18next

Step 1: 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
Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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).

Note: 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

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();
Enter fullscreen mode Exit fullscreen mode

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

i18next debug log

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

Step 2: 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
{
  "intro-title": "Welcome to React!"
}
Enter fullscreen mode Exit fullscreen mode

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

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.

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;
Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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.

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

const Intro = () => {
  const { t } = useTranslation();

  return <h1>{t('intro-title')}</h1>;
};

export default Intro;
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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

Multi-page Experience with Routing

For that we will use the react-router-dom library.

$ npm install --save react-router-dom
Enter fullscreen mode Exit fullscreen mode

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.

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();
Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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).

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;
Enter fullscreen mode Exit fullscreen mode

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

Note: 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

  • 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.

Top comments (2)

Collapse
 
anton_dykyi_c2e9faf972eea profile image
Anton Dykyi

Nice writeup, but I'd just like to interject for a moment. What you're referring to as Linux,
is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux.
Linux is not an operating system unto itself, but rather another free component
of a fully functioning GNU system made useful by the GNU corelibs, shell
utilities and vital system components comprising a full OS as defined by POSIX.

Many computer users run a modified version of the GNU system every day,
without realizing it. Through a peculiar turn of events, the version of GNU
which is widely used today is often called "Linux", and many of its users are
not aware that it is basically the GNU system, developed by the GNU Project.

There really is a Linux, and these people are using it, but it is just a
part of the system they use. Linux is the kernel: the program in the system
that allocates the machine's resources to the other programs that you run.
The kernel is an essential part of an operating system, but useless by itself;
it can only function in the context of a complete operating system. Linux is
normally used in combination with the GNU operating system: the whole system
is basically GNU with Linux added, or GNU/Linux. All the so-called "Linux"
distributions are really distributions of GNU/Linux.

Collapse
 
adrai profile image
Adriano Raiano

If you want to know even more i18next features, have a look at: dev.to/adrai/how-to-properly-inter...