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.
There are several advantages to using the i18next library:
create-react-app
and goLet'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:
1npm 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.
1// src/i18n.js
2import i18n from "i18next";
3import Backend from "i18next-xhr-backend";
4import { initReactI18next } from "react-i18next";
5
6i18n
7 // load translation using xhr -> see /public/locales
8 // learn more: https://github.com/i18next/i18next-xhr-backend
9 .use(Backend)
10 // pass the i18n instance to react-i18next.
11 .use(initReactI18next)
12 // init i18next
13 // for all options read: https://www.i18next.com/overview/configuration-options
14 .init({
15 fallbackLng: "en",
16 debug: false,
17
18 interpolation: {
19 escapeValue: false // not needed for react as it escapes by default
20 }
21 });
22
23export 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:
1npm install i18next-browser-languagedetector
And the updated i18n.js
would be:
1// src/i18n.js
2import i18n from "i18next";
3import Backend from "i18next-xhr-backend";
4import LanguageDetector from "i18next-browser-languagedetector";
5import { initReactI18next } from "react-i18next";
6
7i18n
8 .use(Backend)
9 // detect user language
10 // learn more: https://github.com/i18next/i18next-browser-languageDetector
11 .use(LanguageDetector)
12 .use(initReactI18next)
13 .init({
14 fallbackLng: "de",
15 debug: true,
16
17 interpolation: {
18 escapeValue: false
19 }
20 });
21
22export default i18n;
We then include the i18n.js
in the src/index.js
of our app:
1// src/index.js
2import React, { Suspense } from "react";
3import { render } from "react-dom";
4import { Provider } from "react-redux";
5
6import App from "./components/App";
7import configureStore from "./store";
8import "todomvc-app-css/index.css";
9import "./i18n";
10
11render(
12 <Provider store={configureStore()}>
13 <App />
14 </Provider>,
15 document.getElementById("root")
16);
The default folder structure for our translation files looks like the following:
1- public/
2--- locales/
3----- de
4------- translation.json
5----- en
6------- translation.json
Our translation files would have the following content:
1// de/translation.json
2{
3 "title": "todos",
4 "placeholder": "Was muss getan werden?"
5}
6
7// en/translation.json
8{
9 "title": "todos",
10 "placeholder": "What needs to be done?"
11}
Note: All translation files are loaded asynchronously.
Finally, we connect the translations into our component via the useTranslation
hook.
1// src/components/Header.js
2import React from "react";
3import PropTypes from "prop-types";
4import { useTranslation } from "react-i18next";
5
6import TodoTextInput from "./TodoTextInput";
7
8const Header = ({ addTodo }) => {
9 const { t } = useTranslation();
10
11 return (
12 <header className="header">
13 <h1>{t("title")}</h1>
14 <TodoTextInput
15 newTodo
16 onSave={text => {
17 if (text.length !== 0) {
18 addTodo(text);
19 }
20 }}
21 placeholder={t("placeholder")}
22 />
23 </header>
24 );
25};
26
27Header.propTypes = {
28 addTodo: PropTypes.func.isRequired
29};
30
31export default Header;
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// src/components/LanguageSelector.js
2import React from "react";
3import { useTranslation } from "react-i18next";
4
5export default function LanguageSelector() {
6 const { i18n } = useTranslation();
7
8 const changeLanguage = lng => {
9 i18n.changeLanguage(lng);
10 };
11
12 return (
13 <div className="LanguageSelector">
14 <button onClick={() => changeLanguage("de")}>de</button>
15 <button onClick={() => changeLanguage("en")}>en</button>
16 </div>
17 );
18}
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.
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- public/
2--- locales/
3----- de
4------- translation.json
5------- footer.json
6----- en
7------- translation.json
8------- footer.json
And it can be accessed as usual using the t()
function from the useTranslation hook.
1import React from "react";
2import PropTypes from "prop-types";
3import { useTranslation } from "react-i18next";
4import FilterLink from "../containers/FilterLink";
5import {
6 SHOW_ALL,
7 SHOW_COMPLETED,
8 SHOW_ACTIVE
9} from "../constants/TodoFilters";
10
11const FILTER_TITLES = {
12 [SHOW_ALL]: "all",
13 [SHOW_ACTIVE]: "active",
14 [SHOW_COMPLETED]: "completed"
15};
16
17const Footer = props => {
18 const { t } = useTranslation(["footer"]);
19
20 const { activeCount, completedCount, onClearCompleted } = props;
21 const itemWord = activeCount === 1 ? t("item") : t("items");
22
23 return (
24 <footer className="footer">
25 <span className="todo-count">
26 <strong>{activeCount || t("no")}</strong> {itemWord} {t('left')}
27 </span>
28 <ul className="filters">
29 {Object.keys(FILTER_TITLES).map(filter => (
30 <li key={filter}>
31 <FilterLink filter={filter}>{t(FILTER_TITLES[filter])}</FilterLink>
32 </li>
33 ))}
34 </ul>
35 {!!completedCount && (
36 <button className="clear-completed" onClick={onClearCompleted}>
37 {t("clearCompleted")}
38 </button>
39 )}
40 </footer>
41 );
42};
43
44Footer.propTypes = {
45 completedCount: PropTypes.number.isRequired,
46 activeCount: PropTypes.number.isRequired,
47 onClearCompleted: PropTypes.func.isRequired
48};
49
50export default Footer;
We can load more than one namespace at a time and use them independently with a namespace separator (:) like this:
1const { t } = useTranslation(['translation', 'footer"]);
2
3// usage
4{t('footer:item')
The final working version of our internationalized app can be found here.
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: