Author avatar

Solomon Ayoola

Architecture of a Multilingual React Redux Application

Solomon Ayoola

  • Feb 22, 2020
  • 10 Min read
  • 390 Views
  • Feb 22, 2020
  • 10 Min read
  • 390 Views
Web Development
React

Introduction

When it comes to designing web apps for international users, we need to think not only about translations, but also translating text in plural form, formats for dates and currencies, and a handful of other things. Here are some of the most popular libraries that can help deal with these issues:

In this guide, we will talk about setting up a multilingual (international) web application with React, react-i18next, and React Redux.

Why i18next?

There are several advantages to using the i18next library:

  • Fast adoption rate when it comes to new React features
  • Simplicity: no need to change your webpack configuration or add additional babel transpilers, just use create-react-app and go
  • Effective and efficient API
  • Beyond i18n comes with locize, bridging the gap between development and translations

Getting Started

Let's start by using a react-redux implementation of the todomvc as the base for our application, and then we will add internationalization features as we go through this guide. For a quick recap on how to get started with react-redux, check out the documentation. This project uses create-react-app, which is a typical, un-opinionated React project with a minimal boilerplate, useful for starting out fresh.

Before we get started, we will have to install the libraries we need:

1
npm install i18next react-i18next i18next-xhr-backend
node

Next, we will create a file called i18n.js in the src folder, where we will keep the configuration for our localization process.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/i18n.js
import i18n from "i18next";
import Backend from "i18next-xhr-backend";
import { initReactI18next } from "react-i18next";

i18n
  // load translation using xhr -> see /public/locales
  // learn more: https://github.com/i18next/i18next-xhr-backend
  .use(Backend)
  // pass the i18n instance to react-i18next.
  .use(initReactI18next)
  // init i18next
  // for all options read: https://www.i18next.com/overview/configuration-options
  .init({
    fallbackLng: "en",
    debug: false,

    interpolation: {
      escapeValue: false // not needed for react as it escapes by default
    }
  });

export default i18n;
javascript

The i18next-xhr-backend expects all translation files to be served from the public/ folder of our app.

We could also dynamically fetch the user language in the browser using an i18n plugin:

1
npm install i18next-browser-languagedetector
node

And the updated i18n.js would be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/i18n.js
import i18n from "i18next";
import Backend from "i18next-xhr-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";

i18n
  .use(Backend)
  // detect user language
  // learn more: https://github.com/i18next/i18next-browser-languageDetector
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: "de",
    debug: true,

    interpolation: {
      escapeValue: false
    }
  });

export default i18n;
javascript

We then include the i18n.js in the src/index.js of our app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/index.js
import React, { Suspense } from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";

import App from "./components/App";
import configureStore from "./store";
import "todomvc-app-css/index.css";
import "./i18n";

render(
  <Provider store={configureStore()}>
    <App />
  </Provider>,
  document.getElementById("root")
);
javascript

The default folder structure for our translation files looks like the following:

1
2
3
4
5
6
- public/
--- locales/
----- de
------- translation.json
----- en
------- translation.json
javascript

Our translation files would have the following content:

1
2
3
4
5
6
7
8
9
10
11
// de/translation.json
{
  "title": "todos",
  "placeholder": "Was muss getan werden?"
}

// en/translation.json
{
  "title": "todos",
  "placeholder": "What needs to be done?"
}
javascript

Note: All translation files are loaded asynchronously.

Finally, we connect the translations into our component via the useTranslation hook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/components/Header.js
import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";

import TodoTextInput from "./TodoTextInput";

const Header = ({ addTodo }) => {
  const { t } = useTranslation();

  return (
    <header className="header">
      <h1>{t("title")}</h1>
      <TodoTextInput
        newTodo
        onSave={text => {
          if (text.length !== 0) {
            addTodo(text);
          }
        }}
        placeholder={t("placeholder")}
      />
    </header>
  );
};

Header.propTypes = {
  addTodo: PropTypes.func.isRequired
};

export default Header;
javascript

The useTranslation hook returns an object that contains the following properties:

  • t() - The t function accepts a mandatory parameter as the translation key (public/locales/en/translation.json), and the second optional parameter is the so-called working text. Whenever there is no translation, it defaults to the working text or to the translation key if there is no working text in the first place.

  • i18n - This is the initialized i18n instance. It contains several functions, one of which we can use to change the currently selected language. See example below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/components/LanguageSelector.js
import React from "react";
import { useTranslation } from "react-i18next";

export default function LanguageSelector() {
  const { i18n } = useTranslation();

  const changeLanguage = lng => {
    i18n.changeLanguage(lng);
  };

  return (
    <div className="LanguageSelector">
      <button onClick={() => changeLanguage("de")}>de</button>
      <button onClick={() => changeLanguage("en")}>en</button>
    </div>
  );
}
javascript

Note: The useTranslation hook will trigger a Suspense if not ready (e.g., pending load of translation files).

The react-i18n library has a Trans component that we can use to interpolate inner HTML elements, but the majority of the time, we probably won't need it.

Namespaces

One way react-i18n really shines is its ability to load translations on demand to avoid loading all translations upfront, which would result in bad load times. To learn more about loading applications on demand, please check out my guide Code-Splitting Your Redux Application.

We can include separate translations onto multiple files within one language like this:

1
2
3
4
5
6
7
8
- public/
--- locales/
----- de
------- translation.json
------- footer.json
----- en
------- translation.json
------- footer.json
javascript

And it can be accessed as usual using the t() function from the useTranslation hook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import FilterLink from "../containers/FilterLink";
import {
  SHOW_ALL,
  SHOW_COMPLETED,
  SHOW_ACTIVE
} from "../constants/TodoFilters";

const FILTER_TITLES = {
  [SHOW_ALL]: "all",
  [SHOW_ACTIVE]: "active",
  [SHOW_COMPLETED]: "completed"
};

const Footer = props => {
  const { t } = useTranslation(["footer"]);

  const { activeCount, completedCount, onClearCompleted } = props;
  const itemWord = activeCount === 1 ? t("item") : t("items");

  return (
    <footer className="footer">
      <span className="todo-count">
        <strong>{activeCount || t("no")}</strong> {itemWord} {t('left')}
      </span>
      <ul className="filters">
        {Object.keys(FILTER_TITLES).map(filter => (
          <li key={filter}>
            <FilterLink filter={filter}>{t(FILTER_TITLES[filter])}</FilterLink>
          </li>
        ))}
      </ul>
      {!!completedCount && (
        <button className="clear-completed" onClick={onClearCompleted}>
          {t("clearCompleted")}
        </button>
      )}
    </footer>
  );
};

Footer.propTypes = {
  completedCount: PropTypes.number.isRequired,
  activeCount: PropTypes.number.isRequired,
  onClearCompleted: PropTypes.func.isRequired
};

export default Footer;
javascript

We can load more than one namespace at a time and use them independently with a namespace separator (:) like this:

1
2
3
4
const { t } = useTranslation(['translation', 'footer"]);

// usage
{t('footer:item')
javascript

The final working version of our internationalized app can be found here.

Conclusion

i18next is a really interesting and powerful solution for localizing our applications. It allows heavy customizations and tuning and has saturated infrastructure with a set of plugins and tools.

Here are some useful links if you'd like to explore more on this topic:

6