
Translation management
Software localization
There is a host of React internationalization (i18n) libraries out there. One of the most popular ones is i18next—and for good reason: The library is robust, feature-complete, and regularly maintained. I18next is also “learn once—translate everywhere”: You can use it with many front-end and back-end frameworks.
In this tutorial, we'll walk you through localizing 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 😎
🔗 Resource » Learn everything you need to make your JS applications accessible to international users with our Ultimate Guide to JavaScript Localization.
We’re using the following NPM packages (versions in parentheses) in this article:
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.
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.
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.
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"), );
That’s it for the 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!
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 we 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 the 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 respective 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.
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?
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.
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;
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.
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.
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;
To provide the best UX we can, it’s often a good idea to see what language the user prefers—whether through her browser preferences 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:
?lng=en
query string parameter in the request URL. If this fails,"i18next"
with a stored language. If this fails,"i18nextLng"
with a stored language. If this fails,"i18nextLng"
with a stored language. If this fails,navigator
object). If this fails,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.
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.
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.
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>
We often want to inject values into our translations messages at runtime. Let’s demonstrate this by adding a greeting to “logged-in” users to our app.
{ // ... "hello_user": "Hello, {{user}}" }
{ // ... "hello_user": "أهلاً {{user}}" }
import React from "react"; import { useTranslation } from "react-i18next"; // ... function Navbar({ onLanguageChange }) { const { t } = useTranslation(); return ( <nav> {/* ... */} <div className="navbar-menu"> {/* ... */} <div className="navbar-end"> <div className="navbar-item"> {/* Mock logged in user */} <p>{t("hello_user", { user: "Abed" })}</p> </div> {/* ... */} </div> </div> </nav> ); } export default Navbar;
By default, i18next allows us to use the {{placeholder}}
syntax in our translation messages for interpolation. We then need to supply a second argument to t()
: a simple map of {placholder: value}
. We can have as many placeholders, and respective entries in our map as we want.
Personalized greetings always brighten up my day
We can also use data models, name/value objects, for interpolation. Let’s refactor the code above to use a data model.
{ // ... "hello_user": "Hello, {{user.firstName}}", }
{ // ... "hello_user": "أهلاً {{user.firstName}}", }
import React from "react"; import { useTranslation } from "react-i18next"; // ... function Navbar({ onLanguageChange }) { const { t } = useTranslation(); const user = { firstName: "Abed", lastName: "Nadir" }; return ( <nav> {/* ... */} <div className="navbar-menu"> {/* ... */} <div className="navbar-end"> <div className="navbar-item"> <p>{t("hello_user", { user })}</p> </div> {/* ... */} </div> </div> </nav> ); } export default Navbar;
We pass the entire user
object to t()
and refine into the values we want in our translation messages using dots, e.g. user.firstName
.
🔗 Resource » i18next interpolation is extremely configurable. You can change the characters used for placeholders ({{
and }}
by default), as well as provide a formatting function for custom interpolation logic. We’ll make use of the formatting function option when we write our own date and number formatting a bit later. Check out the official documentation for all of the interpolation options available.
📖 Go deeper » Although often unnecessary, i18next does allow the nesting of messages in other messages.
i18next gives us the ability to define both simple (single/plural) and complex (multiple form) pluralization in our translation messages.
Remember our hard-coded “baskets delivered” message?
Many happy customers
// ... class Header extends React.Component { render() { return ( {/* ... */} <div> {/* ... */} <p>2,342 baskets delivered</p> </div> {/* ... */} ); } } // ...
Let’s localize this message and make sure it respects English plural rules.
{ // ... "basket_delivered": "{{count}} basket delivered", "basket_delivered_plural": "{{count}} baskets delivered" }
{ // ... // This is incorrect Arabic pluralization. We'll fix it later. "basket_delivered": "{{count}} سبت تم توصيله", "basket_delivered_plural": "{{count}} سبت تم توصيله" }
Notice how we provided two messages: key
and key_plural
. We can refer to the key
when calling t()
.
// ... class Header extends React.Component { render() { return ( {/* ... */} <div> {/* ... */} <p>{t("basket_delivered", { count: 2342 })}</p> </div> )} {/* ... */} ); } } // ...
When we provide basket_delivered
, and the special value count
to t()
, i18next automatically selects the appropriate plural form from basket_delivered
and basket_delivered_plural
.
i18next displays the correct plural form automatically
✋🏽Heads up » The counter variable must be called count
, otherwise plural selection won’t work.
📖 Go deeper » i18next also provides generalized selection, which can be used for male/female messages, for example. This is called context, and while it’s outside the scope of this guide, you can check it out in the official docs.
While the singular/plural forms work for English, they don’t work for many other languages. A quick glance at the Unicode CLDR Language Plural Rules table reveals that languages vary in the number of plural categories, or forms, they have. So i18next’s key
and key_plural
variants aren’t enough to cover Arabic, for example, which often has five plural forms, depending on the word.
i18next allows us to handle multiple plural forms in our translation messages using key_0
, key_1
, etc., to map messages to plural forms. Let’s demonstrate this by translating our “baskets delivered” message into Arabic.
{ "basket_delivered_0": "لم يتم توصيل سلال", "basket_delivered_1": "تم توصيل سله {{count}}", "basket_delivered_2": "تم توصيل سلتان", "basket_delivered_3": "تم توصيل {{count}} سلال", "basket_delivered_4": "تم توصيل {{count}} سله", "basket_delivered_5": "تم توصيل {{count}} سله" }
Our call to t()
can stay the same:
<p>{t("basket_delivered", { count: 2342 })}</p>
i18next will automatically match the correct Arabic plural form based on the given count
.
baskets_delivered_0 → count = 0 baskets_delivered_1 → count = 1 baskets_delivered_2 → count = 2 baskets_delivered_3 → count is between 3 and 10, inclusive baskets_delivered_4 → count is between 11 and 100, inclusive baskets_delivered_5 → count > 100
Our message is always presented in the correct Arabic plural form
🔗 Resource » Of course, you’re not expected to just know the key_0
, key_1
mappings to a language’s plural forms. In fact, there’s a nice utility on JSFiddle that does that for you.
✋🏽Heads up » If you don’t provide a plural form for a language, i18next will use your fallback language for that message. See the Supported Languages & Fallback section above for more details on fallback.
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.
When it comes to layout direction, the i18next instance provides a dir()
function that returns the direction of the current language: "ltr"
for left-to-right languages, and "rtl"
for right-to-left languages.
i18n.changeLanguage("en") i18n.dir() // => "ltr" i18n.changeLanguage("ar") i18n.dir() // => "rtl"
Let’s use this function to set the direction of the HTML page in our <App>
.
import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; // ... function App() { const { i18n } = useTranslation(); useEffect(() => { document.dir = i18n.dir(); }, [i18n, i18n.language]); return ( {/* ... */} ); } export default App;
The React useEffect
hook comes in handy here. We use it to update the document.dir
after first render, and whenever the active language changes in our i18next instance. document.dir
maps to the <html dir="...">
attribute.
🔗 Resource » Check out the official React documentation on the effect hook.
Now, whichever language our app’s visitor selects, she will be presented with the correct layout direction for that language.
Right then, no direction left out
i18next doesn’t provide any built-in date formatting. Instead, it accepts an interpolation.format
function configuration option. When we provide a custom interpolation.format
, we’re free to use any date formatting logic we want. Let’s see this in action by formatting the date in the “Weekly basket” header in our app.
import React from "react"; import { Translation } from "react-i18next"; class Header extends React.Component { render() { return ( <Translation> {(t) => ( <div> <h1> {t("weekly_basket_title", { today: new Date(), })} </h1> {/* ... */} </div> )} </Translation> ); } } export default Header;
{ // ... "weekly_basket_title": "In this Week's Grootbasket — {{today, date}}", // ... }
{ // ... "weekly_basket_title": "في الجروتباسكت هذا الإسبوع — {{today, date}}", // ... }
import i18next from "i18next"; // ... i18next // ... .init({ // ... interpolation: { // ... // value == today from {{today, date}} == new Date() // format == date from {{today, date}} == "date" // lng == active language format: function (value, format, lng) { if (format === "date") { return new Intl.DateTimeFormat(lng).format(value); } return value; }, }, // ... }); // ...
✋🏽 Heads up » Our custom interpolation.format
function is only called for interpolated values with explicit formats. {{today}}
wouldn’t work: it would use i18next’s default interpolation. {{today, foo}}
does work.
The value
parameter is the value of today
, which we set to new Date()
, a datetime object corresponding to the present date and time.
format
is the second argument in our {{today, date}}
and is passed as a string, "date"
.
lng
is the active language in our i18next instance.
Whatever we return from interpolation.format()
will be injected into our translation message. In the code above, we’re using the standard JavaScript Intl.DateTimeFormat constructor to format our date. We could use any date formatting library here, or roll our own. Intl.DateTimeFormat
seems to be doing a good job for our needs.
Intl.DateTimeFormat even uses Eastern Arabic numerals for Arabic dates
Of course, we often need to present dates in different ways on a case-by-case basis. In some places, we might want “2/11/2020”, in others “Nov 2, 2020”. The Intl.DateTimeFormat
constructor gives us a few formatting options to accomplish this. Let’s expose these options in our translation messages.
🔗 Resource » Check out the MDN docs for Intl.DateTimeFormat for all available formatting options, and their current browser support.
First, let’s move our interpolation.format
logic into its own module and refactor it since we’re about to expand it a bit.
function format(value, format, lng) { if (format === "date") { return formatDate(value, format, lng); } return value; } function formatDate(value, format, lng) { return new Intl.DateTimeFormat(lng).format(value); } export default format;
//... import format from "./i18n-format"; // ... i18next // ... .init({ // ... interpolation: { // ... format, }, // ... }); // ...
With this refactor we can keep our custom formatting work in the i18n-format.js
file. Now let’s get to customizing. We can manipulate our date formatting by passing an options map when we construct our Intl.DateTimeFormat
formatter.
const options = { day: "numeric", month: "short", year: "numeric" }; new Intl.DateTimeFormat("en", options).format(new Date("2020-11-05")); // => "Nov 5, 2020"
🔗 Resource » All Intl.DateTimeFormat
options are documented on the Intl.DateTimeFormat MDN page.
These options can be exposed to our messages through a custom syntax for our format strings.
{ "weekly_basket_title": "In this Week's Grootbasket — {{date, date(day: numeric; month: short; year: numeric)}}", }
Notice how our formatting options map exactly to the ones supported by Intl.DateTimeFormat
. Let’s get this working.
function format(value, format, lng) { if (format.startsWith("date")) { return formatDate(value, format, lng); } return value; } function formatDate(value, format, lng) { const options = toOptions(format); return options === null ? value : new Intl.DateTimeFormat(lng, options).format(value); } function toOptions(format) { // Handle case with no options, e.g. {{today, date}} if (format.trim() === "date") { return {}; } else { try { return JSON.parse(toJsonString(format)); } catch (error) { console.error(error); return null; } } } function toJsonString(format) { const inner = format .trim() .replace("date", "") .replace("(", "") .replace(")", "") .split(";") .map((param) => param .split(":") .map((name) => '"' + name.trim() + '"') .join(":"), ) .join(","); return "{" + inner + "}"; } export default format;
We’re basically converting date(day: numeric; month: short; year: numeric)
to {"day":"numeric","month":"short","year":"numeric"}
, which is a JSON string that can be parsed into a JavaScript object. This object is exactly what we need to pass as the options parameter to Intl.DateTimeFormat
. We can now customize the look of each date in our message on a case-by-case basis.
{ "weekly_basket_title": "In this Week's Grootbasket — {{date, date(day: numeric; month: short; year: numeric)}}" }
{ "weekly_basket_title": "في الجروتباسكت هذا الإسبوع — {{date, date(weekday: short; day: numeric; month: long)}}" }
Total power over date formats. Use wisely.
We’re not currently localizing the numbers in our app, which isn't ideal; not all languages use Western Arabic numerals (1, 2, 3).
We’re incorrectly using Western Arabic numerals for Arabic
Arabic actually uses Eastern Arabic numerals (١، ٢، ٣). In fact, different languages use different numerals, currency symbols, separator characters, and separator placement.
📖 Go deeper » We cover the nuances of number localization in our article A Concise Guide to Number Localization.
We laid the groundwork for number localization when we took care of date formatting. All we have to do is extend our custom format()
function to handle number formatting as well.
In a similar vein to our date formatting solution, we’ll use the standard JavaScript Intl.NumberFormat
constructor. And again, we’ll allow for default and customizable number formatting in our translation messages.
{ // Default number formatting "basket_delivered": "{{count, number}} basket delivered", "basket_delivered_plural": "{{count, number}} baskets delivered", // Customized number formatting "estimated_weight_kg": "{{weight, number(style: unit; unit: kilogram)}}" }
The custom formatting options will get passed to Intl.NumberFormat
, and we’ll handle them just like we did with date formatting.
🔗 Resource » Intl.NumberFormat
also handles currency formatting, among many other options. Get the details from the MDN Intl.Numberformat docs.
function format(value, format, lng) { if (format.startsWith("date")) { return formatDate(value, format, lng); } if (format.startsWith("number")) { return formatNumber(value, format, lng); } return value; } function formatDate(value, format, lng) { const options = toOptions(format, "date"); return options === null ? value : new Intl.DateTimeFormat(lng, options).format(value); } function formatNumber(value, format, lng) { const options = toOptions(format, "number"); return options === null ? value : new Intl.NumberFormat(lng, options).format(value); } function toOptions(format, specifier) { if (format.trim() === specifier) { return {}; } else { try { return JSON.parse(toJsonString(format, specifier)); } catch (error) { console.error(error); return null; } } } function toJsonString(format, specifier) { const inner = format .trim() .replace(specifier, "") .replace("(", "") .replace(")", "") .split(";") .map((param) => param .split(":") .map((name) => `"${name.trim()}"`) .join(":"), ) .join(","); return "{" + inner + "}"; } export default format;
We’ve updated our toOptions()
and toJsonString()
functions to take specifier
parameters. A format specifier can be "date"
or "number"
. Otherwise, everything is exactly the same as our date formatting. We’re just using Intl.NumberFormat
instead of Intl.DateFormat
.
{ // ... "estimated_weight_kg": "{{weight, number(style: unit; unit: kilogram)}}", "basket_delivered": "{{count, number}} basket delivered", // ... }
{ // ... "estimated_weight_kg": "{{weight, number(style: unit; unit: kilogram)}}", // ... "basket_delivered_1": "تم توصيل سله {{count, number}}", // ... }
import React from "react"; import { Translation } from "react-i18next"; class Header extends React.Component { render() { return ( <Translation> {(t) => ( <div> {/* ... */} <p>{t("basket_delivered", { count: 2342 })}</p> </div> )} </Translation> ); } } export default Header;
import React from "react"; import { useTranslation } from "react-i18next"; function Item({ // ... estimatedWeightInKilograms, }) { const { t } = useTranslation(); return ( <div> {/* ... */} <p> {/* ... */} {t("estimated_weight_kg", { weight: estimatedWeightInKilograms, })} </p> </div> ); } export default Item;
By delegating our localized number formatting to Intl.NumberFormat
, we get the correct numerals, separators, and units for the active language.
Right numbers for the right language
🔗 Resource » If you’re localizing units, like we did above with kilograms, you might want to visit the ECMAScript Internationalization API Specification, which lists the units that you can use with Intl.NumberFormat
.
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.
We hope that you’ve learned a few new things when it comes to React localization with i18next. If you want to take your i18n game to the next level, check out Phrase.
A comprehensive localization solution built by developers for developers, the Phrase Localizatuin Suite features 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 code you love. Not only that, Phrase has an in-context editor that works beautifully with i18next.
Check out all Phrase features for developers, and see for yourself how it can streamline your software localization workflows.
Last updated on August 29, 2023.