next-i18next vs react-intl

Choosing the right i18n library for Next.js

Before we start...

Are you using the Next 13+ App Router?

The next-i18next library is not compatible with the App Router. If you are using the App Router or plan to upgrade to it, then you will want to read this tutorial for App Router internationalization.

Are you using the Pages Router?

Then this post is for you! We're going to take a look at the differences between using next-i18next and react-intl with the Pages Router. This is not meant to be a full tutorial, just a comparison between these two popular libraries.

If you're instead looking for a full walkthrough on setting up next-i18next in a Next.js app, check out this tutorial. If you want a full walkthrough for setting up react-intl with Next.js, check out this tutorial.

With that out of the way... Let's go! 🚀

Introduction

Next.js is a rising star in the constantly evolving world of Javascript frameworks. But even with this great framework, we still run into the question of “What is the best way to internationalize my app?”.

After the release of Next.js 10, we were introduced to the fantastic feature of internationalized routing. This feature provides us with the ability to not only have dedicated routing for each of our supported languages, but also automatic detection of a user’s preferred language based on browser settings. But the feature was not designed to have any functionality for rendering translations to the end user. This is where the need for an i18n library comes in.

There are quite a few i18n libraries out there that we can use with Next.js, but next-i18next and react-intl are the most popular choices.

Background

next-i18next is part of the i18next open source ecosystem. You may have heard of react-i18next as well, but the i18next community has decided to build and adopt next-i18next specifically for integration with Next.js. While react-i18next can be used with Next.js, next-i18next is more streamlined for SSR and more directly integrated with Next’s internationalized routing.

react-intl is part of the Format.js collection of i18n libraries. It has over a million weekly downloads on NPM and used in both Next.js apps and standard React apps. Similar to react-i18next, react-intl was started before Next.js existed, but its unopinionated and simple nature allows it to be used with SSR and Next.js apps with no problem.

Bundle Sizes

next-i18next minified and gzipped is about 5kb, but it is dependent on both react-i18next (7kb) and i18next (15kb). This gives it a total bundle size of about 27kb.

On the other hand, react-intl has no other dependencies and sits at about 17kb. i18next is certainly more feature-packed, but if bundle size is a big deal for you then you may want to stick with react-intl since its almost 40% lighter.

Formatting

One of the big reasons many people like react-intl is that it uses ICU Message Format for string formatting. This is a standardized i18n format used across many i18n platforms and libraries in all different types of programming languages. It therefore has a lot of interoperability. On the other hand, i18next has its own specific formatting and syntax. It can use ICU format if you use an add-on, but it is not the common way to use i18next.

Let’s take a look at how strings are written in next-i18next vs react-intl:

next-i18next
{
  // interpolation
  "new_tag": "{{name}} tagged you in a post!",

  // pluralization
  "comments_one": "You have one new comment.",
  "comments_other": "You have {{count}} new comments."
}
react-intl
{
  // interpolation
  "new_tag": "{name} tagged you in a post!",

  // pluralization
  "comments": "{count, plural,
    one {You have one new comment.}
    other {You have # new comments.}
  }"
}

Pluralization can be a tricky concept. A lot of developers today use auto-translation tools like i18nexus to automatically handle pluralization for them. It’s easy to see why. Trying to manage which of the 6 possible plural forms each language uses is not fun no matter what library you’re using. For example, English has two plural forms, Japanese has one, and Arabic has six!

But if you are doing pluralization manually, i18next appears to handle it in a more readable and straightforward way than ICU Message Format does. So you will have to weigh the benefits of readability in i18next with the benefits of interoperability in react-intl. But, again, if you're using a tool like i18nexus, you don't have to worry about it either way.

Loading and rendering translations

Let’s take a quick look at the code required to load and render translations in each library. We’ll start with react-intl.

If you’ve used react-intl in the past on a React app, you will find that using it with Next.js isn’t all that different. It’s very simple and requires no other configuration than what is shown here:

react-intl
import Head from 'next/head';
import { FormattedMessage, IntlProvider } from 'react-intl';

export async function getStaticProps({ locale }) {
  return {
    props: {
      locale: locale,
      messages: (await import(`../../messages/${locale}.json`)).default
    }
  };
}

export default function Home(props) {
  return (
    <IntlProvider messages={props.messages} locale={props.locale}>
      <Head>
        <title>My App</title>
      </Head>

      <main>
        <h1>
          <FormattedMessage id="header" />
        </h1>
      </main>
    </IntlProvider>
  );
}

As you can see, using react-intl is very straightforward to set up and use. The IntlProvider only needs to be used on page level components.

On the other hand, next-i18next requires a bit more configuration and requires a good read of the documentation to get set up correctly. First, it requires a next-i18next.config.js file at the root of your project, then requires a HOC (Higher-Order Component) to be implemented in your _app.js file. For a full tutorial, check out this post.

After the configuration is done, we are able to load and render our translations:

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

export async function getStaticProps({ locale }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, ["contact-us"]))
    }
  };
}

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

  return (
    <>
      <Head>
        <title>My App</title>
      </Head>

      <main>
        <h1>
         {t("header")}
        </h1>
      </main>
    <>
  );
}

This requires a bit more setup, but you only need to do the configuration work one time. After that, the rendering of the translations isn’t all that different from react-intl.

Namespaces

One of the great built-in features of the i18next library is namespacing. It is an especially great feature when it comes to Next.js. When our app loads, we don’t want to load into memory every string for every page in our app. We only want to load the strings required for the current page. Namespaces allow us to do this a lot more easily.

You can also implement namespacing in react-intl, but it is not built in. Do not fear, it is extremely easy to implement. Let’s check out how to use namespacing in each library.

In next-i18next, we fetch translations for a specific namespace by passing in the namespace name as an argument in the serverSideTranslations function:

next-i18next
export async function getStaticProps({ locale }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, ["contact-us"]))
    }
  };
}
...

In this case, we’re loading strings from our "contact-us" namespace. next-i18next will automatically fetch and load our translations from public/locales/{locale}/contact-us.json. Awesome!

If we want to implement namespaces with react-intl, all we really need to do is change how we organize our JSON files in our “messages” directory. Unlike next-i18next, react-intl is unopinionated as to what directory we store our translations in or how we name them. To implement namespacing like we did in next-i18next, let’s just use the same directory structure:

react-intl
export async function getStaticProps({ locale }) {
  return {
    props: {
      locale: locale,
      messages: (await import(`../../messages/${locale}/contact-us.json`)).default
    }
  };
}

That’s all it takes!

next-i18next does allow you to pass an array of namespaces at once, while our react-intl implementation above only allows loading one namespace. But it should be rather trivial to write a helper function that loads multiple namespaces and returns a single object that contains each namespace. You can see how to we can create and use a helper function in this tutorial.

And the winner is…

Well, it’s not that easy 🙂. The answer comes down to your personal needs and preferences. react-intl is simpler, less opinionated, and more standardized, while next-i18next is more feature-packed with a multitude of add-ons and growing at a faster pace. There are countless features in both of these libraries that we have not gotten into, so I recommend reading both the next-i18next docs and react-intl docs. Either library will do the job just fine, so don't stress too much about picking right one!

Level up your localization

It only takes a few minutes to streamline your translations forever.

Get started