A Guide to React Localization with i18next

Phrase
Software Localization Tutorials
18 min readSep 17, 2020
Beginner to advanced localization with React and i18next.

There is a host of internationalization (i18n) libraries when it comes to React localization. One of the most popular is i18next, and for good reason: the library is robust, feature-complete, and often maintained. i18next is also “learn once — translate everywhere”: you can use it with many front- and back-end frameworks. In this article, we walk through localizing our React apps with i18next, covering everything from library installation and setup to the ins and outs of real-world localization. Come along for the ride 😎

Library Versions

We’re using the following NPM packages (versions in parentheses) in this article:

Our Demo App: Grootbasket

To make things concrete as we’re exploring various i18n topics, we’ll build a tiny demo app that we’ll localize. The app, Grootbasket, will be a storefront for a fictional organic farm startup that sends its customers a weekly basket of organic produce. Here’s what it will look like.

Delicious and nutritious

🗒 Note » Thanks to Kiran Shastry for providing the logo we’re using for our demo app for free on the Noun Project.

🗒 Note » You can skip ahead to the i18n/l10n (section titled Installing & Setting Up i18next) if you want to. You can also start with the demo app already built and ready for i18n.

OK, let’s go through the demo app quickly so we can get to the i18n. We’ll start from scratch by spinning up a React app using Create React App.

$ npx create-react-app grootbasket🔗 Resource » Get the complete code for the demo app from GitHub.

🔗 Resource » Get the complete code for the demo app from GitHub.

Let’s replace the boilerplate code with our own hot sauce.

import React from "react";
import Navbar from "./components/Navbar";
import Header from "./components/Header";
import WeeklyBasket from "./components/WeeklyBasket";
import "./App.scss";

function App() {
return (
<>
<Navbar />

<main role="main" className="pt-5 px-3">
<Header />
<WeeklyBasket />
</main>
</>
);
}

export default App;

Our <App> component provides high-level layout and comprises a <Navbar>, a <Header> and a <WeeklyBasket>. We’ll cover the first two and skip the third, so that we can get to the i18n as quickly as possible.

<Navbar> is a relatively basic Bulma navbar that isn’t doing too much yet.

import React from "react";
import logo from "../logo.png";

function Navbar() {
return (
<nav
className="navbar"
role="navigation"
aria-label="main navigation"
>
<div className="navbar-brand">
<a className="navbar-item" href="/">
<img className="navbar-logo" src={logo} alt="logo" />

<strong>Grootbasket</strong>
</a>
</div>

<div className="navbar-menu">
<div className="navbar-start">
<a className="navbar-item" href="/">
Weekly Basket
</a>
</div>
</div>
</nav>
);
}

export default Navbar;
Looking pretty, though

Similarly, <Header> is a presentational component that’s just waiting to be localized.

import React from "react";

class Header extends React.Component {
render() {
return (
<div className="header">
<h1 className="title is-4 has-text-centered mb-5">
In this Week's Grootbasket — 17 Aug 2020
</h1>

<p>2,342 baskets delivered</p>
</div>
);
}
}

export default Header;

Notice that <Header> is a class-based, not a functional, component. We’ll cover how to localize both types of components a bit later.

Our rendered header

As mentioned before, we’ll skip the <WeeklyBasket> component here. Feel free to check out its code on our GitHub repo.

And that’s about it for our starter demo app. We’re now ready to localize.

🔗 Resource » If you want to start at this point and just focus on the i18n code, you can grab a starter snapshot of the app from our GitHub repo.

Installing & Setting Up i18next

We’ll start our i18n by pulling i18next and react-i18next into our project. react-i18next is a set of components, hooks, and plugins that sit on top of i18next, and is specifically designed for React.

$ npm install --save i18next react-i18next

With the libraries installed, let’s create an i18n.js file to bootstrap an i18next instance.

import i18next from "i18next";
import { initReactI18next } from "react-i18next";

// "Inline" English and Arabic translations.
// We can localize to any language and any number of languages.
const resources = {
en: {
translation: {
app_name: "Grootbasket",
},
},
ar: {
translation: {
app_name: "جروتباسكت",
},
},
};

i18next
.use(initReactI18next)
.init({
resources,
lng: "en",
interpolation: {
escapeValue: false,
},
});

export default i18next;

We use() the initReactI18next plugin provided by react-i18next. initReactI18next is responsible for binding our i18next instance to an internal store, which makes the instance available to our React components. We’ll see how we can access the i18next instance in our React components a bit later.

🔗 Resource » Check out the official documentation for a list of prebuilt i18next plugins.

We’ve also “inlined” our translations (resources), and hard-coded the active language as English (lng: "en"). We’ll see how we can load in resources asynchronously, and how to detect the user’s language from the browser, a bit later.

The interpolation config option is used to disable i18next’s escaping of values that we inject into translation messages at runtime. This is a good thing since it protects us from cross-site scripting (XSS) attacks. However, we don’t need it for React apps, since React escapes dynamic values in its components by default.

Debugging

Another handy configuration option that i18next provides is debug. We can set it to true to get console logs in our browser when certain events occur within i18next, such as initialization complete or language change.

import i18next from "i18next";

// ...

i18next
//...
.init({
// ...
debug: true,
});

export default i18next;
i18next’s console debug output can be useful for troubleshooting

🔗 Resource » Read all about i18next’s rich configuration options in the official documentation.

To finish our setup, we just need to import our initialized i18next instance into our index.js file. This ensures that the file is bundled into our app and its code is run.

import React from "react";
import ReactDOM from "react-dom";
import "./services/i18n";

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root"),
);

The t() Function & useTranslation() React Hook

That’s it for initial setup. We can now use i18next’s t() function to localize our app’s name using the translation resources we provided during setup. t() takes a string key, and returns the corresponding string from the active language’s translations.

We access t() via react-i18next’s useTranslation() react hook. The hook ensures that our components get the t() associated with our i18next instance. We’ll explore t() and useTranslations() more throughout this article.

import React from "react";
import { useTranslation } from "react-i18next";
// ...

export default function () {
const { t } = useTranslation();

return (
<nav>
<div className="navbar-brand">
<a className="navbar-item" href="/">

<strong>{t("app_name")}</strong>

</a>
</div>
</nav>
);
}

If we load our app now we shouldn’t notice any changes. However, our app’s name, “Grootbasket”, is now being loaded from our English resources. Let’s switch the active language to Arabic to see this in action.

import i18next from "i18next";
// ...

const resources = {
en: {
translation: {
app_name: "Grootbasket",
},
},
ar: {
translation: {
app_name: "جروتباسكت",
},
},
};

i18next
// ...
.init({
resources,
lng: "ar", // Active language will be Arabic
// ...
});

export default i18next;

When we save our i18n.js file and our app reloads, we can see our Arabic app name rendered in our navbar.

Our first localization! Hooray!

Asynchronous (Lazy) Loading of Translation Files

Of course, having a resources object in our i18n.js file doesn’t exactly scale well. As we add more translations and more languages, we’ll want to split up our translations into multiple files.

We may also want to only load the translation file(s) associated with the currently active language. This can speed up our app when have large translation files and/or when we have many of them.

i18next provides a mechanism for this lazy loading of translation files via back-end plugins. The official i18next-http-backend plugin will do well here. Let’s install it.

$ npm install --save i18next-http-backend

Next, let’s use() the back-end plugin in our i18next instance.

import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import HttpApi from "i18next-http-backend";

i18next
.use(initReactI18next)
.use(HttpApi) // Registering the back-end plugin
.init({
// Remove resources from here
lng: "en",
interpolation: {
escapeValue: false,
},
debug: process.env.NODE_ENV === "development",
});

export default i18next;

Notice that we’ve also removed our resources object: we will now load our translations from the server as we need them. We’ll need to put our translation files in the location the back-end plugin expects them to be by default: public/locales/{{lng}}/{{ns}}.json—where {{lng}} is the language code, and {{ns}} is the namespace.

📖 Go deeper » Namespaces is a feature of i18next that allows for granular organization and loading of translations. They’re a bit outside the scope of this guide, and we’ll just use i18next’s default namespace for all our translations in this article. Read more about namespaces in the official i18next docs.

{
"app_name": "Grootbasket"
{
"app_name": "جروتباسكت"
}

We’re using the default, translation, namespace here.

If we were to load our app at this point, we would get an error telling us that “A React component suspended while rendering, but no fallback UI was specified.” This is because, by default, react-i18next uses React Suspense for async loading, and we’re not handling that in our code.

To fix this, let’s add a Suspense boundary around our entire <App>.

import React from "react";
import ReactDOM from "react-dom";
import "./services/i18n";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(
<React.StrictMode>
<React.Suspense fallback="Loading...">
<App />
</React.Suspense>
</React.StrictMode>,
document.getElementById("root"),
);

serviceWorker.unregister();

📖 Go deeper » React Suspense is, at time of writing, an experimental feature in React that allows components to work with asynchronous data in a more optimized and natural way. Read more about Suspense in the official documentation.

With this code in place, our app will now load normally. We’ve told React to suspend rendering of the <App> component until i18next has initialized, which now depends on the first language file completing its download.

In fact, if you look at your browser’s development tools Network tab, you’ll notice a new file being downloaded.

Our active translation is now being loaded asynchronously from the network

Of course, switching the language in i18n.js to Arabic (ar), will cause the /locales/ar/translation.json file to load instead.

🔗 Resource » You can customize the location of translation files using the backend configuration option. Check out all of i18next-http-backend’s config options in the library’s GitHub repo readme.

✋🏽 Heads up » You may have noticed that a request for locales/dev/translation.json is being made behind the scenes. This file doesn’t exist. In fact, dev is i18next’s default fallback locale. If you want to stop this request from being made, set the fallbackLng config option to be a language your app supports. In our demo app, we could set fallbackLng: "en". Alternatively, of course, you can provide the public/locales/dev/translation.json as a fallback language. We’ll cover fallback in more detail later in this article.

Getting & Setting the Active Language

We get the active language via the i18n.language property. We set it via the i18n.changeLanguage() method.

i18n.language // => "fr" when active language is French

// Changes active language to Hebrew. Components with translations
// will re-render to show Hebrew translations.
i18n.changeLanguage("he")

i18n.language // => Now "he"

Of course, this begs the question: how do we access the i18n (i18next) instance in our components?

Accessing the i18next Instance in React Components

If we’re using the react-i18next useTranslation() hook, we can just destructure the i18n object out of it.

const { i18n } = useTranslation() // i18n is the i18next instance

const { t, i18n } = useTranslation() // Also works 😉

We’ll see how to access the i18next instance when we’re not using useTranslation() later in this article.

🔗 Resource » Read the official documentation on useTranslation() to see everything you can do with the hook.

Building a Language Switcher

We often want our users to be able to change our app’s language themselves. Let’s give them a <LanguageSwitcher> component to facilitate that.

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

function LanguageSwitcher() {
const { i18n } = useTranslation();

return (
<div className="select">
<select
value={i18n.language}
onChange={(e) =>
i18n.changeLanguage(e.target.value)
}
>
<option value="en">English</option>
<option value="ar">عربي</option>
</select>
</div>
);
}

export default LanguageSwitcher;

A simple <select>, our <LanguageSwitcher> uses the i18n instance to change the active language to the one our user selects.

We can plop our <LanguageSwitcher> into our <Navbar> to reveal it in our app.

import React from "react";
import LanguageSwitcher from "./LanguageSwitcher";
// ...

function Navbar() {
// ...

return (
<nav>
{/* ... */}

<div className="navbar-menu">
{/* ... */}

<div className="navbar-end">
<div className="navbar-item">
<LanguageSwitcher />
</div>
</div>
</div>
</nav>
);
}

export default Navbar;import React from "react";
import LanguageSwitcher from "./LanguageSwitcher";
// ...

function Navbar() {
// ...

return (
<nav>
{/* ... */}

<div className="navbar-menu">
{/* ... */}

<div className="navbar-end">
<div className="navbar-item">
<LanguageSwitcher />
</div>
</div>
</div>
</nav>
);
}

export default Navbar;
I should stop playing with this and get back to writing

✋🏽 Heads up » When we switch to a language that we’ve never loaded before, i18next will asynchronously load the language’s translation file(s) from the network before switching. It will then cache the language’s files to avoid loading the file(s) again.

📖 Go deeper » We can eager load language translation files using the preload configuration option. Read more in the official documentation.

Supported Languages & Fallback

We often want to maintain a list of allowed languages that our app supports. i18next provides a way to set and get supported languages through config options.

// At initialization
i18next
.init({
lng: "en",

// Allowed languages
supportedLngs: ["en", "ar"],
});

// In our components
const { i18n } = useTranslation();

i18n.options.supportedLngs // => ["en", "ar", "cimode"]

✋🏽 Heads up » The “cimode” element present when we read the supportedLngs array is meant to be used for end-to-end (e2e) tests.

supportedLngs only affects what language translations can be loaded. Our app’s active language, set via the lng config option or i18n.changeLanguage() can be anything we want, regardless of the set supportedLngs. This may be best demonstrated with an example.

const resources = {
en: {
translation: {
app_name: "Grootbasket",
},
},
es: {
translation: {
app_name: "Grootcesta",
},
},
ar: {
translation: {
app_name: "جروتباسكت",
},
},
}

i18next.init({
resources,
lng: "es", // Set lng to unsupported language
supportedLngs: ["en", "ar"]
})

i18next.language // => "es", was allowed

// Spanish (es) translations were lot loaded, so i18next will not find
// the Spanish message
i18next.t("app_name") // => "app_name"

Notice that while we were able to change the active language to Spanish (es) above, its translations were never loaded from the resources object, because "es" is not in our supportedLngs.

If we were using a back-end to asynchronously load our translations in the example above, the Spanish (es) translations would never get loaded from the network.

Fallback

What happens when i18next does not find a translation message for a given key? This might happen because the active language is not in our supportedLngs array, or because we forgot to provide the message in the active language’s translations. As we’ve seen earlier, i18next will return the key of the message in this case.

const resources = {
en: {
translation: {
app_name: "Grootbasket",
signup_button: "Sign up",
},
},
ar: {
translation: {
app_name: "جروتباسكت",
},
},
}

i18next.init({
resources,
lng: "ar",
supportedLngs: ["en", "ar"]
})

// We forgot to provide this message in Arabic (ar)
i18next.t("signup_button") // => "signup_button", key is returned

However, we can explicitly set a fallback language for i18next to use when it cannot find a message in the active language. We use the fallbackLng config option for this.

const resources = {
en: {
translation: {
app_name: "Grootbasket",
signup_button: "Sign up",
},
},
ar: {
translation: {
app_name: "جروتباسكت",
},
},
es: {
translation: {
app_name: "Grootcesta",
signup_button: "Registrarse",
},
},
}

i18next.init({
resources,
lng: "ar",
supportedLngs: ["en", "ar"],
fallbackLng: "en",
})

// Message not found in Arabic (ar), will use English (en) as fallback
i18next.t("signup_button") // => "Sign up"

i18next.changeLanguage("es")

i18next.language // => es

// Spanish (es) is not supported, so its translations were not loaded
// Will use English (en) as fallback
i18next.t("signup_button") // => "Sign up"

In case you’re coding along with us, here’s the i18n.js file in our demo app at this point.

import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import HttpApi from "i18next-http-backend";

i18next
.use(initReactI18next)
.use(HttpApi)
.init({
lng: "en",
supportedLngs: ["en", "ar"],

// Allows "en-US" and "en-UK" to be implcitly supported when "en"
// is supported
nonExplicitSupportedLngs: true,

fallbackLng: "en",
interpolation: {
escapeValue: false,
},
debug: process.env.NODE_ENV === "development",
});

export default i18next;

Automatically Detecting the User’s Language

To provide the best UX we can, it’s often a good idea to see what language the user prefers — whether through her browser or her previous visit to our app — and to show her the language closest to that. i18next provides an official browser detection plugin that we can use to make this job a lot easier. Let’s install it.

$ npm install --save i18next-browser-languagedetector

We’ll need to use() it in our i18next instance. We’ll also need to remove the lng config option, otherwise it will override any auto-detection.

import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import HttpApi from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18next
.use(initReactI18next)
.use(HttpApi)
.use(LanguageDetector) // Registering the detection plugin
.init({
// ...

// Remove the lng option from here

// ...
});

export default i18next;

That’s basically all we need to start detecting the user’s preferred language. Here’s what the detector will do by default:

  1. Attempt to find a ?lng=en query string parameter in the request URL. If this fails,
  2. Attempt to find a domain cookie called "i18next" with a stored language. If this fails,
  3. Attempt to find an entry in the domain’s localStorage called "i18nextLng" with a stored language. If this fails,
  4. Attempt to find an entry in session storage called "i18nextLng" with a stored language. If this fails,
  5. Attempt to determine the user’s first preferred language from her browser settings (navigator object). If this fails,
  6. Attempt to determine the language from the lang attribute of the page’s <html> tag.

At this point, a language should have been detected. The detected language will get saved in a cookie, localStorage, or session storage, so that the user will see this language on her next visit to our site.

✋🏽 Heads up » It’s a good idea to have a fallback language configured in case no language is detected. It might also be a good idea to set nonExplicitSupportedLngs: true, so that an ar-EG (Egyptian Arabic) user will see the ar (Arabic) version of our app. See the Supported Languages & Fallback section above for more info on fallbacks.

✋🏽 Heads up » The detected language does not need to be a supported language ( in supportedLngs), and will become the active language ie. i18n.language . Be aware of this when writing code that relies on i18n.language.

If the active language is changed manually via i18n.changeLanguage(code), the detector will store this language for the user’s next visit.

📖 Go deeper » A lot of i18next-browser-languagedetector’s behaviour is configurable. Check out the official documentation for all the juicy details.

The withTranslation High Order Component (HOC)

If you’re anything like me, you’ll likely use the useTranslation() hook for most of your components. There are times, however, where the alternative withTranslation HOC comes in handy.

🗒 Note » If you’ve been working with React for a bit, you’ll know that a HOC, or high order component, is a component function that takes a component parameter and wraps it, returning another component.

import React from "react";
import { withTranslation } from "react-i18next";

class Header extends React.Component {
render() {
return (
<div className="header">
<h1 className="...">
{this.props.t("weekly_basket_title")}
</h1>

{/* ... */}
</div>
);
}
}

// Here's where the magic happens
export default withTranslation()(Header);

We’ve used the withTranslation HOC to wrap our <Header> component. Notice that our <Header> is not a functional component, it’s class-based. So we couldn’t use the useTranslation() hook to access i18next’s t() function as usual here.

Instead, withTranslation makes a this.props.t() available to our <Header>. This t() works exactly the same way as useTranslation’s.

🗒 Note » We also have the i18next instance available in <Header> via this.props.i18n. We’re not using it in the code above, but it’s there if we need it.

🔗 Resource » Read the official documentation on the withTranslation HOC.

The Translation Render Prop

Alternatively, we can use the <Translation> render prop to access t() in our components.

🗒 Note » Another React pattern, a render prop is a component with a function prop, where the function handles the rendering instead of the component itself.

import React from "react";
import { Translation } from "react-i18next";

class Header extends React.Component {
render() {
return (
<Translation>
{(t) => (
<div className="header">
<h1 className="...">
{t("weekly_basket_title")}
</h1>

{/* ... */}
</div>
)}
</Translation>
);
}
}

export default Header;

As with the withTranslation HOC, <Translation>’s t() is exactly the same as the one useTranslation() provides.

📖 Go deeper » The i18next instance can be accessed within <Translation> as well. Find out how in the official documentation on the Translation render prop.

Basic Translation Messages

We’ve covered basic translation messages in previous sections, so we’ll go over them quickly for completeness.

// In our translation resources
{
en: {
translation: {
"weekly_basket": "Weekly Basket"
}
},
es: {
translation: {
"weekly_basket": "Cesta Semanal"
}
}
}

// In our components
const { t, i18n } = useTranslation()

i18n.changeLanguage("en")

<p>{t("weekly_basket")}</p> // => <p>Weekly Basket</p>

i18n.changeLanguage("es")

<p>{t("weekly_basket")}</p> // => <p>Cesta Semanal</p>

HTML in Translation Messages with the Trans Component

On occasion we will have HTML within our translation messages. This can be a bit tricky to deal with. It’s tempting to just plop the HTML in our translation resources and use React’s dangerouslySetInnerHTML prop to bypass escaping. The prop gets its name for a reason, however: using it can expose our site to XSS attacks.

This is why react-i18next provides a <Trans> component that allows us to keep translation message HTML in our components, where we can control it. Let’s see this in action. We’ll add a footer with links to our app, and use <Trans> to localize it.

import React from "react";
import { Trans } from "react-i18next";

function Footer() {
return (
<footer className="footer">
<p className="has-text-centered">
<Trans i18nKey="footer">
Demo for a{" "}
<a href="https://phrase.com/blog">Phrase blog</a>{" "}
article.
<br />
Created with React, i18next, and Bulma.
</Trans>
</p>
</footer>
);
}

export default Footer;

Notice the i18nKey prop on the <Trans> component. i18nKey is exactly the same key we would give to the t() function to reference a translation message. What the <Trans> component allows us to do is mark parts of our messages where we want to inject HTML.

{
// ...
"footer": "Demo for <2>Phrase blog</2> article.<5/>Created with React, i18next, and Bulma."
}
{
// ...
"footer": "عرض لمقالة في <2>مدونة فرايز</2>.<5/>بني بواسطة ريأكت و أي إيتين نكست و بولما."
}

In our translation messages, we designate where we want to inject HTML based on the element/component order that we’ve passed to <Trans>.

<Trans i18nKey="footer">
Demo for a // => 0
{" "} // => 1
<a href="https://phrase.com/blog">Phrase blog</a> // => 2
{" "} // => 3
article. // => 4
<br /> // => 5
Created with React, i18next, and Bulma. // => 6
</Trans>

Notice how the <2>...</2> and <5/> tags in our messages correspond to locations where we want to inject HTML tags when we render the messages.

🔗 Resource » Read the official documentation on the Trans component.

With that in place, our saucy demo app looks all saucy now.

Get your weekly organic basket in any language

🔗 Resource » Get the complete code for the demo app from GitHub.

Toodles

We hope that you’ve learned a few new things when it comes to React localization with i18next.

And if you want to take your i18n game to the next level, check out Phrase. Phrase is a professional i18n/l10n solution built by developers for developers. Featuring a robust CLI and API, GitHub, Bitbucket, and GitLab sync, machine learning translations, and a great web console for your translators, Phrase will do the heavy lifting in your i18n/l10n process to keep you focused on the creative code you love. Not only that, Phrase has an In-Context Editor that works beautifully with i18next. Check out all of Phrase’s features, and sign up for a free 14-day trial.

🗒 Note » If you’re wondering why we haven’t covered things like interpolation, plurals, and date formatting in this guide, don’t worry! This article is a work in progress and we’ll be adding these topics and more in the coming weeks. Is there something in particular you’d like to use to write about? Let us know in the comments below!

Originally published on The Phrase Blog.

--

--