Software localization

The Best React Libraries for Internationalization

There may be no built-in solution for React i18n, but these amazing libraries will help you manage your i18n projects in React easily from start to finish.
React l10n and i18n blog post featured image | Phrase

Meta’s React UI framework is so popular that it’s become a de facto foundation for building websites. This ubiquity has led to an expansive ecosystem of libraries that solve common problems. From state management to forms to routing, there’s a React library for nearly any problem you’ll encounter. Of course, internationalization (i18n) is no exception, and is probably what you’re here for. In this article, we’ll compare and contrast a handful of React i18n libraries, to hopefully help you choose the best one for your project.

We’ll also localize a simple demo app with each of the libraries we cover here, giving you a tiny taste of coding and working with the libraries.

Here are the best React i18n libraries in our opinion:

  • react-i18next (i18next)
  • react-intl (FormatJS)
  • LinguiJS
  • next-i18next

🗒️ Note » The first 3 libraries help localize general React apps. The last is for Next.js i18n, and we thought it good to include it since Next.js has become an increasingly popular framework for working with React.

Rating criteria

So what do we mean by “the best React i18n libraries” anyway? Well, we used the following criteria to filter through the noise and narrow down our list.

  • Downloads per week (according to NPM)
  • Frequency of updates
  • Quality of documentation
  • Bundle size (according to Bundlephobia)

We also looked at some advanced features:

  • Complex plural support for languages like Arabic and Russian
  • Localized number and date formatting
  • TypeScript support
  • Message extraction: the ability to create a base translation file automatically from your code
  • ICU (International Components for Unicode) standard syntax support

🤿 Go deeper » “What’s ICU?” you may be wondering. The Missing Guide to the ICU Message Format answers that and more.

NPM trends graph | Phrase

NPM Trends graph showing NPM downloads of React i18n libraries in 2022.

A couple of notes

  • All frameworks covered here support basic message translation and interpolation.
  • We’ve used NPM bundle sizes from Bundlephobia as part of our rating criteria. These sizes do not account for tree shaking. Moreover, some libraries have developer tooling that wouldn’t normally get bundled in your production builds. So our recommendation is to try out the library you like and see what the actual production bundle weighs in for you. Take the bundle sizes in this article as a relative comparison metric.

🔗 Resource » If you want a comparison of the best overall JavaScript i18n libraries, covering vanilla JS, Angular, Vue, and more, check out our roundup, The Best JavaScript I18n Libraries.

Our React demo app

For those who want to see how the i18n libraries work hands-on, we’ll build a very simple demo app and localize it using each library. The code won’t be exhaustive by any stretch but should serve to show you how to set up and get started. We’ll link to more detailed tutorials as we go.

🗒️ Note » We’re using Node v18.12 and React 18.2 in this article.

Our demo is very simple, and based on the stock Create React App boilerplate. Let’s spin it up from the command line:

$ npx create-react-app simple-i18n-demoCode language: Bash (bash)

Once the React app has been created, we can strip down the App.js component to the following.

// src/App.js

import logo from './logo.svg';

// ...

function App() {
  return (
    <div>
      <header>
        <img src={logo} alt="logo" />
      </header>

      <main>
        <p>This demo React app will be internationalized by
           various i18n libraries.</p>
      </main>
    </div>
  );
}

export default App;Code language: JavaScript (javascript)

That’s about it for our starter app. Nice and simple.

Demo react app | Phrase

Our demo app rendered in the browser, waiting for localization.

🔗 Resource » We’ve omitted CSS code here for brevity. Get the CSS along with the rest of the code we cover in this article from our GitHub repo’s start branch.

Alright, let’s get to the i18n libraries, shall we?

react-i18next (i18next)

With 2.1 million weekly downloads at the time of writing, react-i18next is by far the most popular React i18n solution on our list. The framework was made specifically for React and built on top of the incredibly popular i18next. Almost any i18n problem has an i18next plugin that solves it: detecting the user’s language, async loading of translation files, and a lot more.

i18next also has built-in complex plural support and uses JavaScript’s native Intl objects for localized date and number formatting.

The price for all these features is some heft in bundle size. i18next weighs in at 15.1 kB minified + gzipped and react-i18next adds another 7.1 kB. That’s a total of 22.2 kB added to your bundle. However, given that i18next is feature-complete, you might be happy to take this tradeoff. If you do want a slimmer solution, take a look at LinguiJS, which we’ll cover a bit later.

On the flip side, very good documentation and a rapid update cycle round out the react-i18next package. You can even add TypeScript definitions for your translations for type safety. And if your team wants a more “enterprise” workflow, i18next offers ICU format support and message extraction through add-ons.

All in all, react-i18next is an easy recommendation for React i18n. So what’s it like to use the library? Let’s localize our demo app with react-i18next to find out.

We need to install both react-i18next and i18next via the command line.

$ npm install react-i18next i18nextCode language: Bash (bash)

🗒️ Note » We’re using i18next 22.4 and react-i18next 12.1 for our React demo app.

Libraries installed, let’s update our demo app to localize with i18next.

// src/App.js

import i18next from "i18next";
import { initReactI18next, useTranslation } from "react-i18next";
import logo from "./logo.svg";
// ...

// Initialize the library. In production, we would do this in a
// separate file and import it into the root index.js file.
// (We only need to initialize i18next once).
i18next
  // Plug in i18next React extensions.
  .use(initReactI18next)
  .init({
    // Add translations. We're adding English (USA) and 
    // Arabic (Egypt) here. In a production app, these
    // translations would likely be in separate files.
    resources: {
      "en-US": {
        // i18next uses the `translation` namespace by default.
        translation: {
          logo: "logo",
          demo: "This demo app was internationalized by i18next",
          // Interpolated datetime
          now: "Current date and time are {{currentDateTime, datetime}}",
        },
      },
      "ar-EG": {
        translation: {
          logo: "رمز التطبيق",
          demo: "تم تدويل هذا التطبيق التجريبي بواسطة آي ايتين نكست",
          now: "التاريخ والوقت الحاليان هما {{currentDateTime, datetime}}",
        },
      },
    },
    // Set the default language to English.
    lng: "en-US",
    // Disable i18next's default escaping, which prevents XSS
    // attacks. React already takes care of this.
    interpolation: {
      escapeValue: false,
    },
  });

// Our React component
function App() {
  // The useTranslation() hook will pull in a translation function 
  // and i18n instance. These have been initialized above and
  // are aware of the active locale. useTranslation() can be used
  // in any component regardless of nesting.
  const { t, i18n } = useTranslation();

  return (
    <div>
      <header>
        {/* Use the t() function to localize the alt text. */}
        <img src={logo} alt={t("logo")} />

        {/* Simple locale switcher */}
        <button onClick={() => i18n.changeLanguage("en-US")}>
          English
        </button>
        <button onClick={() => i18n.changeLanguage("ar-EG")}>
          Arabic
        </button>
      </header>

      <main>
        {/* Localize content */}
        <p>{t("demo")}</p>

        {/* Format localized datetime. Many formatting options
            are available. */}
        <p>
          {t("now", {
            currentDateTime: Date.now(),
            formatParams: {
              currentDateTime: { 
                dateStyle: "full",
                timeStyle: "long",
              },
            },
          })}
        </p>
      </main>
    </div>
  );
}

export default App; Code language: JavaScript (javascript)

And with that, our app is localized!

i18next gif | Phrase

i18next’s t() function causes a re-render when the active locale is changed.

🤿 Go deeper » We’ve skimmed over a lot here. Our Guide to React Localization with i18next goes over things a bit more thoroughly: It also looks at how to place translations in separate files, async loading of those files, and much more.

🔗 Resource » Get the complete react-i18next localization code from the react-i18next branch of our GitHub repo.

react-intl (FormatJS)

FormatJS is a set of i18n libraries with a heavy focus on standards, namely the ICU Message syntax and Unicode CLDR (Common Locale Data Repository). For React apps, react-intl extends FormatJS, providing components, hooks, and more for using FormatJS in your React apps. Many organizations use react-intl. In fact, at the time of writing, react-intl is enjoying 1.1 million weekly downloads, second on our list only to react-i18next. And much like i18next, react-intl receives frequent updates.

Unlike i18next, however, react-intl doesn’t provide solutions for language detection or translation file loading. That said, we’ve found that implementing our own solutions for these common problems wasn’t too difficult, so not having these features is not a dealbreaker for us.

On the plus side, given its first-class implementation of the ICU Message syntax, react-intl provides excellent support for complex plurals, date formatting, and number formatting. react-intl also provides a first-party CLI for message extraction and Translation Management System (TMS) support. You can also ensure type safety of your messages and locales with TypeScript declarations.

We found the library’s documentation, while comprehensive, a bit tricky to navigate. On the flip side, react-intl’s bundle size is a bit smaller than react-i18next’s, coming in at a total of 17.8 kB minified + gzipped.

Overall, react-intl seems to us to be focused on bigger teams that are using a TMS (like Phrase) and rely on ICU and CLDR standards. If that sounds like you, you’ll be in good company using FormatJS: Yahoo, Mozilla, Dropbox, and other large organizations use the library for their i18n.

OK, time for code. Let’s use react-intl to localize our demo app.

🗒️ Note » We’re using react-intl 6.2 in this article.

Installation is via the command line as usual:

$ npm install react-intlCode language: Bash (bash)

react-intl doesn’t have a built-in way to switch locales, so let’s roll a little <I18n> wrapper component to handle that. We’ll declare our translation messages in the wrapper module as well.

// src/I18n.js

import { useState } from "react";
import { IntlProvider } from "react-intl";

// Declare our translation messages. In a production app,
// these would be in separate files.
const messages = {
  // English (USA)
  "en-US": {
    logo: "logo",
    demo: "This demo app was internationalized by react-intl",

    // Interpolated date using ICU syntax. Many formatting
    // options are available.
    now: "Current date and time are {currentDateTime, date, ::EEE, MMM d, yyyy h:mm a}",
  },
  // Arabic (Egypt)
  "ar-EG": {
    logo: "رمز التطبيق",
    demo: "تم تدويل هذا التطبيق التجريبي بواسطة رياكت إنتل",
    now: "التاريخ والوقت الحاليان هما {currentDateTime, date, ::EEE, MMM d, yyyy h:mm a}",
  },
};

// Our wrapper component, using a React render prop to 
// expose the setLocale() function to child components.
function I18n({ render }) {
  const [locale, setLocale] = useState("en-US");

  return (
    // Use the key prop to force react-intl's IntlProvider
    // to re-render its children on locale change.
    <IntlProvider 
       messages={messages[locale]}
       locale={locale}
       key={locale}>
      {render(setLocale)}
    </IntlProvider>
  );
}

export default I18n;Code language: JavaScript (javascript)

Now let’s use this component to wrap our <App> in the root index.js.

// src/index.js

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import I18n from "./I18n";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    {/* Use the render prop to set the locale in <I18n> when
        it changes in the outer <App>. In a production app, we
        might want to handle this with React Context. */}
    <I18n 
       render={(setLocale) => <App onLocaleChanged={setLocale} />}
    />
  </React.StrictMode>
);Code language: JavaScript (javascript)

Finally, we localize our <App> and provide a locale switcher.

// src/App.js

import { FormattedMessage, useIntl } from "react-intl";
import logo from "./logo.svg";
// ...

// onLocaleChanged notifies outer components.
function App({ onLocaleChanged }) {
  // The useIntl() hook provides an object with localization
  // functions. Since our component tree is wrapped in an 
  // <IntlProvider>, these functions are aware of the active
  // locale.
  const intl = useIntl();

  return (
    <div>
      <header>
        {/* Use intl.$t() to localized logo alt text. intl.$t()
            is an alias for intl.formatMessage(). */}
        <img src={logo} alt={intl.$t({ id: "logo" })} />

        {/* Simple language switcher. */}
        <button onClick={() => onLocaleChanged("en-US")}>
          English
        </button>
        <button onClick={() => onLocaleChanged("ar-EG")}>
          Arabic
        </button>
      </header>

      <main>
        <p>
          {/* Use the FormattedMessage component to display
              translated messages. */}
          <FormattedMessage id="demo" />
        </p>
        <p>
          {/* Use values prop to pass in interpolated values. */}
          <FormattedMessage
             id="now"
             values={{ currentDateTime: Date.now() }}
          />
        </p>
      </main>
    </div>
  );
}

export default App;Code language: JavaScript (javascript)

🔗 Resource » Get all the code for our react-intl localization from our GitHub repo’s react-intl branch.

And with that, our app is localized 😊

React-intl | Phrase

Our app, localized with react-intl.

🤿 Go deeper » While it takes a bit of work to get started with react-intl, our React I18n Tutorial with FormatJS can ease you into it. The guide goes over setup and usage step-by-step.

LinguiJS

The LinguiJS i18n library is the underdog in our list. At the time of writing, the library’s React package, @lingui/react, is getting around 80 thousand downloads per week. That’s orders of magnitude less than react-i18next and react-intl. So why include LinguiJS in this list at all?

For one, LinguiJS has a small bundle footprint compared to the two previous libraries. @lingui/react comes in at 2.5 kB minified + gzipped, and @lingui/core at 7.9 kB. That’s a total of 10.4 kB, about half the size of react-i18next or react-intl. This svelte size is likely because LinguiJS seems to front-load much of its work, using macros and precompiling messages before your app sees the production server.

Otherwise, LinguiJS is quite similar to react-intl, using the ICU Message format for its localization. However, it extends the ICU format to provide intuitive inlining of translation messages with its <Trans> macro. We’ll see this in action when we get to the code.

🤿 Go deeper » The official documentation has a candid comparison between LinguiJS and react-intl.

Because it implements the ICU format, complex plural support comes out of the box. LinguiJS also offers functions to format localized dates and numbers using JavaScript’s Intl objects.

The library comes with a good CLI (command-line interface), offering message extraction and compilation. So like react-intl, LinguiJS seems a good choice for larger teams who want to automate their localization process. In fact, one potential downside of LinguiJS for smaller projects is that the library seems to require message extraction and compilation for locale testing.

The library’s documentation is serviceable if a bit hard to navigate. On the flip side, TypeScript support is included. And while LinguiJS’s community is smaller than the previous two libraries in this list, it’s a dedicated one, frequently updating the library.

Now to the code. Let’s install the library from the command line. We’ll need a couple of dev dependencies and one production dependency.

$ npm install --save-dev @lingui/cli @lingui/macro
$ npm install @lingui/reactCode language: Bash (bash)

🗒️ Note » We’re using version 3.15 of @lingui/cli, @lingui/macro, and @lingui/react in this article.

After we’ve installed our packages, let’s create a .linguirc config file at the root of our project. This will define our supported locales (American English and Egyptian Arabic), as well as the path to our translation message files.

// .linguirc

{
  "locales": [
    "en-US",
    "ar-EG"
  ],
  "sourceLocale": "en-US",
  "catalogs": [
    {
      "path": "src/locales/{locale}/messages",
      "include": [
        "src"
      ]
    }
  ]
}Code language: JSON / JSON with Comments (json)

Now let’s add two CLI scripts to our package.json file. These will help us extract translation messages out of our components and compile them for production.

// package.json

{
  "name": "simple-i18n-demo",
  "version": "0.1.0",

  // ...

  "scripts": {
    // ...

    // The extract and compile scripts.
    "extract": "lingui extract",
    "compile": "lingui compile"
  },

  // ...
}Code language: JSON / JSON with Comments (json)

Let’s run the scripts to initialize our message catalogs. First, we’ll run the extraction script.

$ npm run extractCode language: Bash (bash)

This should have created two PO files:

  • src/locales/en-US/messages.po
  • src/locales/ar-EG/messages.po

🗒️ Note » PO (Portable Object) files are a common format for translation files.

The files will just include metadata at the moment. Let’s compile them into JavaScript that we can use in our app.

$ num run compileCode language: Bash (bash)

Two new files should have been created:

  • src/locales/en-US/messages.js
  • src/locales/ar-EG/messages.js

Now let’s load these files into an <I18n> wrapper component that handles the active locale state and locale loading for our app.

// src/I18n.js

import { useEffect, useState } from "react";
import { I18nProvider } from "@lingui/react";
import { i18n } from "@lingui/core";

// We're not using plurals in this app, but LinguiJS will
// spit out console errors if we don't configure them.
import { en as enPlurals, ar as arPlurals } 
  from "make-plural/plurals";

// Import our translation messages.
import { messages as enMessages } from "./locales/en-US/messages";
import { messages as arMessages } from "./locales/ar-EG/messages";

const localeConfig = {
  "en-US": { messages: enMessages, plurals: enPlurals },
  "ar-EG": { messages: arMessages, plurals: arPlurals },
};

const defaultLocale = "en-US";

// Loads plurals and translation messages for the given
// locale and sets this locale as the active one.
function loadLocale(locale) {
  const { plurals, messages } = localeConfig[locale];

  i18n.loadLocaleData(locale, { plurals });
  i18n.load(locale, messages);
  i18n.activate(locale);
}

// Load our default locale initially.
loadLocale(defaultLocale);

// Our wrapper component. We use a React render prop to
// expose the setLocale() function.
function I18n({ render }) {
  const [locale, setLocale] = useState(defaultLocale);

  useEffect(() => loadLocale(locale), [locale]);

  // LinguiJS's I18nProvider component wraps our app component
  // tree, ensuring that all of our nested components use the 
  // same i18n instance and are aware of the active locale.
  return (
    <I18nProvider i18n={i18n}>
       {render(setLocale)}
    </I18nProvider>
  );
}

export default I18n;Code language: JavaScript (javascript)

OK, let’s wrap our <App> with the <I18n> component we just wrote. We’ll do this in the root index.js file.

// src/index.js

import React from "react";
import ReactDOM from "react-dom/client";
// ...
import App from "./App";
import I18n from "./I18n";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    {/* When a new locale is selected in our App component,
        we call setLocale() in I18n to load the new locale
        and activate it. */}
    <I18n 
       render={(setLocale) => <App onLocaleChanged={setLocale} />} 
    />
  </React.StrictMode>
);Code language: JavaScript (javascript)

Alright, that was quite a bit of setup. Let’s localize our app now.

// src/App.js

import { Trans, t } from "@lingui/macro";
import { useLingui } from "@lingui/react";
import logo from "./logo.svg";
// ...

// onLocaleChanged is called back when a new locale is 
// selected, triggering the wrapping <I18n> component to
// load the new locale's message and re-rendering the
// component hierarchy.
function App({ onLocaleChanged }) {
  // The useLingui() hook exposes the i18n object, with its
  // imperative functions.
  const { i18n } = useLingui();

  return (
    <div>
      <header>
        {/* Use the t() macro to localize attributes. */}
        <img
          src={logo}
          alt={t({ id: "logo", message: "logo" })}
        />

        {/* A simple language switcher. */}
        <button onClick={() => onLocaleChanged("en-US")}>
          English
        </button>
        <button onClick={() => onLocaleChanged("ar-EG")}>
          Arabic
        </button>
      </header>

      <main>
        <p>
          {/* The intuitive Trans macro marks a string for
              localization. Notice that we don't need an
              explicit ID here to track the string here. */}
          <Trans>This demo app was internationalized 
                 using LinguiJS</Trans>
        </p>

        <p>
          {/* Interpolated date. Many formatting options are
              available here. */}
          <Trans>
            Current date and time are{" "}
            {i18n.date(Date.now(), {
              dateStyle: "medium",
              timeStyle: "medium",
            })}
          </Trans>
        </p>
      </main>
    </div>
  );
}

export default App;Code language: JavaScript (javascript)

The <Trans> and t macros have marked our strings for localization. Now we need to translate them. Let’s run our extraction script to pull the strings into our language files.

$ npm run extractCode language: Bash (bash)

This will populate our PO language files with the marked strings.

# src/locales/en-US/messages.po

msgid ""
msgstr ""
"POT-Creation-Date: 2022-12-14 17:49+0100\n"
"MIME-Version: 1.0\n"
# ...

#: src/App.js:31
msgid "Current date and time are {0}"
msgstr "Current date and time are {0}"

#: src/App.js:27
msgid "This demo app was internationalized using LinguiJS"
msgstr "This demo app was internationalized using LinguiJS"

#: src/App.js:15
msgid "logo"
msgstr "logo"Code language: PowerShell (powershell)

Notice how the file is tracking where each message is in the component file, and how each message is using its source text as a default ID.

We can now add our translations to the ar-EG/messages.po file.

# src/locales/ar-EG/messages.po

msgid ""
msgstr ""
"POT-Creation-Date: 2022-12-14 17:49+0100\n"
# ...

#: src/App.js:31
msgid "Current date and time are {0}"
msgstr "التاريخ والوقت الحاليان هما {0}"

#: src/App.js:27
msgid "This demo app was internationalized using LinguiJS"
msgstr "تم تدويل هذا التطبيق التجريبي باستخدام لنجوي"

#: src/App.js:15
msgid "logo"
msgstr "رمز التطبيق"Code language: PowerShell (powershell)

Finally, we run the compile command to create production-ready message files.

$ npm run compileCode language: Bash (bash)

Now when we npm start our app again, we’ll find it localized.

🔗 Resource » Get all the code for our LinguiJS localization from the lingui branch of our GitHub repo.

LinguiJS gif | Phrase

LinguiJS requires a bit of work to set up but provides an intuitive message syntax in our components.

🤿 Go deeper » Localizing JavaScript & React Apps with LinguiJS covers dynamic message catalog loading, plurals, and much more LinguiJS goodness.

Our Next.js demo app

Next.js has become the de facto React framework for many developers; the library is seeing over 3.4 million downloads per week as we write this. With its easy server-side rendering (SSR), static site generation (SSG), and file-based routing, Next.js solves many complex problems common to modern React apps. While Next does support internationalized routing out of the box, we still need external solutions for translating our content. next-i18next is one library that takes care of this. We’ll cover next-i18next here, giving it the same treatment as we did with previous React i18n libraries.

Of course, we’ll need a separate demo app for Next, so let’s build one. Again, this app will be quite simple and a riff off the create-next-app boilerplate. Speaking of which, let’s spin the app from the command line:

$ npx create-next-app i18n-demoCode language: Bash (bash)

🗒️ Note » We’re using Next.js 13.1 in this article.

Once our Next.js app has been installed, we can modify the Home page code to look like the following.

// pages/index.js

import Head from "next/head";
import Link from "next/link";
// ...

export default function Home() {
  return (
    <>
      <Head>
        <title>i18n demo</title>
        <meta
          name="description"
          content="A simple internationalization (i18n) demo"
        />
        {/* ... */}
      </Head>
      <main>
        <nav>
          <Link href="/">Home</Link>

          {/* We'll build the about page in a moment. */}
          <Link href="/about">About</Link>
        </nav>

        <p>
          This app will be internationalized by a couple of
          Next.js i18n libraries.
        </p>
      </main>
    </>
  );
}Code language: JavaScript (javascript)

🔗 Resource » Styling code has been removed from code listings here for brevity. You can find all the code for this demo in our GitHub repo.

Let’s add a simple About page and link it to our home page.

// pages/about.js

import Head from "next/head";
import Link from "next/link";
// ...

export default function About() {
  return (
    <>
      <Head>
        <title>About us | i18n demo</title>
        <meta
          name="description"
          content="A simple internationalization (i18n) demo"
        />
        {/* ... */}
      </Head>
      <main>
        <nav>
          <Link href="/">Home</Link>
          <Link href="/about">About</Link>
        </nav>

        <p>
          A simple about page. Today is {new Date().toString()}.
        </p>
      </main>
    </>
  );
}Code language: JavaScript (javascript)

With that, our two-pager is ready for i18n.

Pre localization demo | Phrase

next-i18next (i18next)

next-i18next is the i18next’s team official Next.js solution, taking react-i18next and extending it to work with Next and SSR. So all the benefits of react-i18next and its parent i18next come along with this one. Complex plurals, date and number formatting, TypeScript support, ICU message formats, and message extraction: It’s all here.

The library is straightforward to set up and use (we’ll do that in a minute). Its documentation is quite good at getting you started. You will want to look at the docs for react-i18next and i18next for more advanced i18n features, however.

As is the case with react-i18next, bundle size is a bit large with next-i18next. The library comes in at 4.7 kB minified + gzipped, and that’s on top of the 22 kB that react-i18next and i18next bring, adding a total of ~25 kB to your app bundle.

At the time of writing, next-i18next is getting 290 thousand weekly downloads and is updated frequently by its team. So the library is a reliable choice for Next.js i18n.

Alright, let’s get to the code. It’s worth noting that Next.js offers built-in internationalized routing that libraries like next-i18next can tap into. Let’s set that up first. We’ll start by adding an i18n entry to the Next.js config file.

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,

  // We're supporting English (USA) and Arabic (Egypt) 
  // here. English is our default locale, which 
  // means it will be used when no locale is provided
  // in the route.
  i18n: {
    defaultLocale: "en-US",
    locales: ["en-US", "ar-EG"],
  },
};

module.exports = nextConfig;Code language: JavaScript (javascript)

🗒️ Note » Next.js will automatically detect the user’s locale from the Accept-Language header and redirect them to the corresponding localized URL.

Now let’s add a simple language switcher to allow each visitor to select her locale.

// pages/index.js
// ...
import Link from "next/link";

// ...

export default function Home() {
  return (
    <>
      <Head>
        {/* ... */}
      </Head>
      <main>
        <nav >
          <div>
            <Link href="/">Home</Link> |
            <Link href="/about">About</Link>
          </div>

          {/* The language switcher. */}
          <div>
            <span>🌐</span>
            
            {/* Next's Link component supports a locale prop
                for explicit language switching.  */}
            <Link href="/" locale="en-US">
              English
            </Link>{" "}
            |
            <Link href="/" locale="ar-EG">
              Arabic (العربية)
            </Link>
          </div>
        </nav>

        {/* ... */}
      </main>
    </>
  );
}Code language: JavaScript (javascript)

With that in place, clicking the language links switches the locale segment of the URL.

Loc links gif | Phrase

Of course, this only really handles the routing. We still need to localize the app content. That’s where next-i18next comes in. Let’s install it along with its peer dependencies from the command line.

$ npm install next-i18next react-i18next i18nextCode language: Bash (bash)

🗒️ Note » We’re using next-i18next 13.0, react-i18next 12.1, and i18next 22.4 for this demo.

With the libraries installed, let’s configure next-i18next to work with our app. First, we’ll add a next-i18next.config.js file at the root of our project.

// next-i18next.config.js

module.exports = {
  i18n: {
    defaultLocale: "en-US",
    locales: ["en-US", "ar-EG"],
  },
};Code language: JavaScript (javascript)

Note that these entries are very similar to what we added to next.config.js earlier. In fact, let’s reuse these in a quick refactor of Next’s own config file.

// next.config.js

/** @type {import('next').NextConfig} */

// We're sharing the same i18n config
// between next-i18next and Next.js.
const { i18n } = require("./next-i18next.config");

const nextConfig = {
  reactStrictMode: true,
  i18n,
};

module.exports = nextConfig;Code language: JavaScript (javascript)

next-i18next will create an i18next instance under the hood, and we need to make sure this instance is available to our component hierarchy. We do so by wrapping our App in a higher-order component that next-i18next provides.

// pages/_app.js

import { appWithTranslation } from "next-i18next";
import "../styles/globals.css";

function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default appWithTranslation(App);Code language: JavaScript (javascript)

For SSR, we need to make sure our pages use next-i18next’s serverSideTranslations function in their getServerSideProps or getStaticProps.

// pages/index.js

// ...
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

// ...

export default function Home() {
  // Unchanged for now.
}

// Next.js provides the active locale as a 
// property of the context parameter given
// to getStaticProps.
export async function getStaticProps({ locale }) {
  return {
    props: {
      // This allows us to use next-i18next's
      // translation functions during SSR.
      ...(await serverSideTranslations(locale)),
    },
  };
}Code language: JavaScript (javascript)

🗒️ Note » The getServerSideProps case is almost identical to the getStaticProps case above.

That’s setup done. Let’s add our translations. By default, next-i18next will expect them to be at public/locales/{locale}/common.json.

// public/locales/en-US/common.json

{
  "title": "next-i18next demo",
  "metaDescription": "A simple internationalization (i18n) demo",
  "home": "Home",
  "about": "About",
  
  // ...

  // We can interpolate dynamic values. We're adding a
  // datetime here.
  "aboutContent": "This about page was last rendered on {{val, datetime}}."
}Code language: JSON / JSON with Comments (json)
// public/locales/ar-EG/common.json

{
  "title": "عرض نكست أي أيتين نكست",
  "metaDescription": "عرض تدويل بسيط",
  "home": "الرئيسية",
  "about": "نبذة عنا",

  //...

  "aboutContent": "تم عرض هذه الصفحة حول آخر مرة في {{val, datetime}}."
}Code language: JSON / JSON with Comments (json)

We can now use next-i18next’s useTranslation hook to pull in our translations. Here’s the About page as an example:

// pages/about.js

import Head from "next/head";
// ...
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

// ...

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

  return (
    <>
      <Head>
        {/* t() will return the translation in the 
            active locale. */}
        <title>{t("title")}</title>
        <meta name="description" content={t("metaDescription")} />
        {/* ... */}
      </Head>
      <main>
        <nav>
          <div>
            <Link href="/">{t("home")}</Link> |
            <Link href="/about">{t("about")}</Link>
          </div>

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

        {/* Interpolated values are injected via the
            second parameter to t(). */}
        <p>{t("aboutContent", { val: new Date() })}</p>
      </main>
    </>
  );
}

export async function getServerSideProps({ locale }) {
  return {
    props: {
      ...(await serverSideTranslations(locale)),
    },
  };
}Code language: JavaScript (javascript)

And with that, our app is localized!

Next localized | Phrase

🔗 Resource » Get the complete code for this demo from our GitHub repo.

🤿 Go deeper » Our standalone next-i18next guide covers using the library in much more detail.

Signing off

We hope you’ve enjoyed this round-up of React i18n libraries. And if you’re looking to take your i18n game to the next level, check out Phrase. With over-the-air translations, tons of tooling to automate your translation workflow, and integrations with GitHub, GitLab, Bitbucket, and more, Phrase takes care of the heavy lifting for i18n. So you can get back to the creative code you love. Sign up for a free trial to see why developers love working with Phrase.