Feb 18, 2020 | 5 min read

Multilingual Architecture with React and Redux

When it comes to internationalization, we need to think not only about translations, but also about pluralization, formatting for dates and currencies, and a handful of other things. Here are some of the most popular libraries that can help in dealing with it:

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

Why i18next?

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

Getting Started

Let's start by using a react-redux implementation of the todomvc as the base for our application then we will add internationalization 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, unopinionated React project with a minimal boilerplate, useful for starting out fresh.

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

npm install i18next react-i18next i18next-xhr-backend

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

// 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;

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:

npm install i18next-browser-languagedetector

And the updated i18n.js would be:

// 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;

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

// 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") );

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

- public/ --- locales/ ----- de ------- translation.json ----- en ------- translation.json

Our translation files would have the following content:

// de/translation.json { "title": "todos", "placeholder": "Was muss getan werden?" } // en/translation.json { "title": "todos", "placeholder": "What needs to be done?" }

NB: All translation files are loaded asynchronously.

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

// 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;

The useTranslation hook returns an object which 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:

// 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> ); }

NB: The useTranslation hook will trigger a Suspense if not ready (eg. pending load of translation files).

The react-i18n library has a Trans component in which we can use to interpolate inner HTML elements, but in majority of the time, we might not 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 post on code-splitting-your-redux-application

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

- public/ --- locales/ ----- de ------- translation.json ------- footer.json ----- en ------- translation.json ------- footer.json

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

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;

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

const { t } = useTranslation(['translation', 'footer"]); // usage {t('footer:item')

The final working version of our internationation app can be find here

Summary

i18next is really interesting and powerful solution for localizing our application. It allows heavy customizations and tuning. Has saturated infrastructure around with set of Plugins and Tools.

Here are some useful links