NextJS i18n/Internationalization
Tags: webdev, react, nextjs, javascript
Published: 2022-03-23 12:31:35

## Table of Contents
- <a href="#tldr" target="_self">TL;DR Source and Demo</a>
- <a href="#introduction" target="_self">Introduction</a>
- <a href="#setup" target="_self">Project Setup</a>
- <a href="#i18n-routing" target="_self">Internationalized Routing</a>
- <a href="#content-translation" target="_self">Content translation</a>
- <a href="#formatting" target="_self">Built-in Formatting</a>
- <a href="#fetching-backend" target="_self">Fetching from backend</a>


## TL;DR <a name="tldr"></a>

> Check the demo [here](https://next-i18n-example-cg.vercel.app/)
> Check the source code [here](https://github.com/codegino/next-i18n-example)

## Introduction

Internationalization (i18n) is the process of preparing software so that it can support local languages and cultural settings. An internationalized product supports the requirements of local markets around the world, functioning more appropriately based on local norms and better meeting in-country user expectations. Copy-pasted from [here](https://lingoport.com/what-is-i18n)

<br />

In my early days of development, I find i18n to be a tedious task. However, in [NextJS](https://nextjs.org/), it is relatively simple to create such as challenging feature.

<br />

## Project Setup <a name="setup"></a>

### Initialize a NextJS project

Let's start by creating a new NextJS project. The simplest way is to use these commands:

```sh
npx create-next-app@latest
# or
yarn create next-app
```

For more information, check this [Create Next App docs](https://nextjs.org/docs/api-reference/create-next-app)

<br />

#### Remove boilerplate code

Let's simplify the project by removing unused code.

```jsx
// pages/index.jsx
export default function Home() {
  return <main>Hello world</main>;
}
```

Check the changes [here](https://github.com/codegino/next-i18n-example/commit/83f5f6a04f81ce3bf430f6deeb6a2ec1be62ef48)

<br />

### Create another route/page

> This step is not related to i18n. It is mainly for easy demonstration.

Update the Home page to display the current locale.

```jsx
// pages/index.jsx
import { useRouter } from "next/router";

export default function Home() {
  const { locale } = useRouter();

  return <main>Hello world: {locale}</main>;
}
```

Let's create an About page with the same content as the Home page.

```jsx
// pages/about.jsx
import { useRouter } from "next/router";

export default function About() {
  const { locale } = useRouter();

  return <main>About page: {locale}</main>;
}
```

Without any configuration changes, the pages will be rendered as:

<Img width={680} height={635} src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8fvgs0o141eglfmwoic5.png" />

As you can see, `localhost:3000` shows `Hello world: `. This is because `useRouter` is not aware of the value of `locale`.

<br />

`localhost:3000/zh-CN` and `localhost:3000/sv` obviously will not exist because we have not created `pages/zh-CN.jsx` and `pages/sv.jsx`

<br />

## Internationalized Routing <a name="i18n-routing"></a>

### Built-in NextJS i18n routing

Let's add this simple `i18n` configuration to our `next.config.js` file and see what happens.

```js
// next.config.js
const nextConfig = {
  // other stuff
  i18n: {
    defaultLocale: "en",
    locales: ["en", "sv", "zh-CN"],
  },
};
```

With the configuration above, we automagically get the `locale` value and the following routes:

#### Home page

<Img width={680} height={564} src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ze3r6h54doxu0tc097ke.png" />

<br />

#### About page

<Img width={680} height={559} src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ww0xto6xf2zj41esnt4u.png" />

<br />

#### Not defined locale

If you try to access `localhost:3000/fr`, you will still get a 404 error. This is because we did not add `fr` to our `locale` values

<Img width={680} height={432} src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v93dzxk9p8f3dpdcgu0u.png" />

<br />

### Create a header component 

To further simplify our demo, let's create a header component that can:

- Navigate to home and about pages
- Change the locale values using a dropdown

```jsx
// components/Header.jsx
import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";

const Header = () => {
  const router = useRouter();

  const handleLocaleChange = (event) => {
    const value = event.target.value;

    router.push(router.route, router.asPath, {
      locale: value,
    });
  };

  return (
    <header>
      <nav>
        <Link href="/">
          <a className={router.asPath === "/" ? "active" : ""}>Home</a>
        </Link>
        <Link href="/about">
          <a className={router.asPath === "/about" ? "active" : ""}>About</a>
        </Link>
      </nav>

      <select onChange={handleLocaleChange} value={router.locale}>
        <option value="en">🇺🇸 English</option>
        <option value="zh-CN">🇨🇳 中文</option>
        <option value="sv">🇸🇪 Swedish</option>
      </select>

      <style jsx>{`
        a {
          margin-right: 0.5rem;
        }

        a.active {
          color: blue;
        }

        nav {
          margin-bottom: 0.5rem;
        }
      `}</style>
    </header>
  );
};

export default Header;
```

Let's add the `Header` component to our `pages/_app.js` file.

```jsx
// pages/_app.jsx
import Header from "../components/Header";
import "../styles/globals.css";

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

export default MyApp;
```

Now we can see clearly the power of NextJS built-in i18n support. We can now access the `locale` value in our `useRouter` hook, and the URL is updated based on the `locale`.

<Img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6clh33761tlx7wit99vx.gif" width={500} height={250}/>


<br />

To learn more about NextJS i18n routing, check this [link](https://nextjs.org/docs/advanced-features/i18n-routing).

<br />

## Content translation <a name="content-translation"></a>

Unfortunately, there is no NextJS built-in support for content translation so we need to do it on our own.

<br />

However, there are a libraries that can help to not reinvent the wheel. In this blog post, we will use [next-i18next](https://github.com/isaachinman/next-i18next).

<br />

Let's support content translation by setting up `next-i18next` in our app.

<br />

### Install next-i18next

```sh
npm install next-i18next
```

### Create a `next-i18next.config.js` and update `next.config.js`

```js
// next-i18next.config.js
module.exports = {
  i18n: {
    defaultLocale: "en",
    locales: ["en", "sv", "zh-CN"],
    localePath: "./locales",
  },
};
```

`localePath` is optional and will default to `./public/locales`.

```js
// next.config.js
const { i18n } = require("./next-i18next.config");

const nextConfig = {
  // other stuff
  i18n,
};

module.exports = nextConfig;
```

### Create translation files

```txt
.
└── locales
    ├── en
    |   └── common.json
    |   └── home.json
    └── zh-CH
    |   └── common.json
    |   └── home.json
    └── se
        └── common.json
        └── home.json
```

#### English Translations

```json
// locales/en/common.json
{
  "greeting": "Hello world!"
}
```

```json
// locales/en/home.json
{
  "home": "Home",
  "about": "About"
}
```

> Please forgive me for any translation mistakes. I only used Google Translate to translate the content. 🤣

#### Chinese translations

```json
// locales/zh-CN/common.json
{
  "greeting": "世界您好"
}
```

```json
// locales/zh-CN/home.json
{
  "home": "主页",
  "about": "关于页面"
}
```

#### Swedish translations

```json
// locales/sv/common.json
{
  "greeting": "Hej världen!"
}
```

```json
// locales/sv/home.json
{
  "home": "Hem",
  "about": "Om"
}
```

> There are three functions that `next-i18next` exports, which you will need to use to translate your project:

#### appWithTranslation

This is a HOC which wraps your `_app`. This HOC is primarily responsible for adding a [I18nextProvider](https://react.i18next.com/latest/i18nextprovider).

```jsx
// pages/_app.jsx
import { appWithTranslation } from "next-i18next";
import Header from "../components/Header";
import "../styles/globals.css";

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

export default appWithTranslation(MyApp);
```

<br />

##### serverSideTranslations

This is an async function that you need to include on your page-level components, via either [getStaticProps](https://nextjs.org/docs/basic-features/data-fetching/overview#getstaticprops-static-generation) or [getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/overview#getserversideprops-server-side-rendering).

```jsx
// pages/index.jsx
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

// export default function Home...

export async function getStaticProps({ locale }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, ["common", "home"])),
      // Will be passed to the page component as props
    },
  };
}
```

<br />

##### useTranslation

This is the hook which you'll actually use to do the translation itself. The useTranslation hook comes from [react-i18next](https://react.i18next.com/latest/usetranslation-hook), but can be imported from next-i18next directly:

```jsx
// pages/index.jsx
// other imports
import { useTranslation } from "next-i18next";

export default function Home() {
  // We want to get the translations from `home.json`
  const { t } = useTranslation("home");

  // Get the translation for `greeting` key
  return <main>{t("greeting")}</main>;
}

// export async function getStaticProps...
```

Let's also translate the links in the `Header` component.

```jsx
// components/Header.jsx
// other imports
import { useTranslation } from "next-i18next";

const Header = () => {
  // ...

  // If no argument is passed, it will use `common.json`
  const { t } = useTranslation();

  return (
    <header>
      <nav>
        <Link href="/">
          <a className={router.asPath === "/" ? "active" : ""}>{t("home")}</a>
        </Link>
        <Link href="/about">
          <a className={router.asPath === "/about" ? "active" : ""}>
            {t("about")}
          </a>
        </Link>
      </nav>
      {/* Other code */}
    </header>
  );
}
```

The changes above will yield the following output:

<Img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/94h6kl9kdj9stl7qya8v.gif" width={500} height={250}/>

The `home` page is translated properly; however, the `about` page is not. It is because we need to use `serverSideTranslations` in every route.


```jsx
// pages/about.jsx
// other imports
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

// export default function About...

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

Now both routes are translated
<Img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m1r3a9cbrrwirq71vxhh.gif" width={600} height={250}/>

We only specified `common` in the `serverSideTranslations` because we don't plan on using anything in `home.json` in the About page.

<br />

> I will fetch the translations of the About page's content from the backend. But before that, let's first check some cool stuff we can do with our translation library.

### Nested translation keys and default translation

We are not limited to a flat JSON structure.

```json
// locales/en/newsletter.json
{
  "title": "Stay up to date",
  "subtitle": "Subscribe to my newsletter",
  "form": {
    "firstName": "First name",
    "email": "E-mail",
    "action": {
      "signUp": "Sign Up",
      "cancel": "Cancel"
    }
  }
}
```

We can omit some translation keys if we want it to use the default locale value(`en` in our case).

```json
// locales/zh-CN/newsletter.json
{
  "title": "保持最新状态",
  "form": {
    "email": "电子邮箱",
    "action": {
      "cancel": "取消"
    }
  }
}
```

Let's create a component which use the translations above.

```jsx
// components/SubscribeForm.jsx
import { useTranslation } from "next-i18next";
import React from "react";

const SubscribeForm = () => {
  const { t } = useTranslation("newsletter");

  return (
    <section>
      <h3>{t("title")}</h3>
      <h4>{t("subtitle")}</h4>

      <form>
        <input placeholder={t("form.firstName")} />
        <input placeholder={t("form.email")} />
        <button>{t("form.action.signUp")}</button>
        <button>{t("form.action.cancel")}</button>
      </form>

      {/* For styling only */}
      <style jsx>{`
        form {
          max-width: 300px;
          display: flex;
          flex-direction: column;
        }

        input {
          margin-bottom: 0.5rem;
        }
      `}</style>
    </section>
  );
};

export default SubscribeForm;
```

Render the form in `pages/index.jsx` and add `newsletter` in `serverSideTranslations`.

```jsx
// pages/index.jsx
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useTranslation } from "next-i18next";
import SubscribeForm from "../components/SubscribeForm";

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

  return (
    <main>
      <div>{t("greeting")}</div>
      {/* Render the form here */}
      <SubscribeForm />
    </main>
  );
}

export async function getStaticProps({ locale }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, [
        "common",
        "home",
        "newsletter", // Add newsletter translations
      ])),
    },
  };
}
```

And now, we have this!
<Img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1amjqh6qvtpy6rfyudmp.gif" width={600} height={514}/>

<br />

### Built-in Formatting <a name="formatting"></a>

It is very easy to format most of our data since `next-i18next` is using [i18next](https://www.i18next.com/) under the hood.

- [Number](https://www.i18next.com/translation-function/formatting#number)
- [Currency](https://www.i18next.com/translation-function/formatting#currency)
- [DateTime](https://www.i18next.com/translation-function/formatting#datetime)
- [RelativeTime](https://www.i18next.com/translation-function/formatting#relativetime)
- [List](https://www.i18next.com/translation-function/formatting#list)

<br />

Let's use the translation files below to showcase the formatting features.

```json
// locales/en/built-in-demo.json
{
  "number": "Number: {{val, number}}",
  "currency": "Currency: {{val, currency}}",
  "dateTime": "Date/Time: {{val, datetime}}",
  "relativeTime": "Relative Time: {{val, relativetime}}",
  "list": "List: {{val, list}}",
  "weekdays": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
}
```

```json
// locales/zh-CN/built-in-demo.json
{
  "number": "数: {{val, number}}",
  "currency": "货币: {{val, currency}}",
  "dateTime": "日期/时间: {{val, datetime}}",
  "relativeTime": "相对时间: {{val, relativetime}}",
  "list": "列表: {{val, list}}",
  "weekdays": ["星期一", "星期二", "星期三", "星期四", "星期五"]
}
```

```json
// locales/sv/built-in-demo.json
{
  "number": "Nummer: {{val, number}}",
  "currency": "Valuta: {{val, currency}}",
  "dateTime": "Datum/tid: {{val, datetime}}",
  "relativeTime": "Relativ tid: {{val, relativetime}}",
  "list": "Lista: {{val, list}}",
  "weekdays": ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag"]
}
```

Let's create a component which use the translations above.

```jsx
import { useTranslation } from "next-i18next";
import React from "react";

const BuiltInFormatsDemo = () => {
  const { t } = useTranslation("built-in-demo");

  return (
    <div>
      <p>
        {/* "number": "Number: {{val, number}}", */}
        {t("number", {
          val: 123456789.0123,
        })}
      </p>
      <p>
        {/* "currency": "Currency: {{val, currency}}", */}
        {t("currency", {
          val: 123456789.0123,
          style: "currency",
          currency: "USD",
        })}
      </p>

      <p>
        {/* "dateTime": "Date/Time: {{val, datetime}}", */}
        {t("dateTime", {
          val: new Date(1234567890123),
          formatParams: {
            val: {
              weekday: "long",
              year: "numeric",
              month: "long",
              day: "numeric",
            },
          },
        })}
      </p>

      <p>
        {/* "relativeTime": "Relative Time: {{val, relativetime}}", */}
        {t("relativeTime", {
          val: 12,
          style: "long",
        })}
      </p>

      <p>
        {/* "list": "List: {{val, list}}", */}
        {t("list", {
          // https://www.i18next.com/translation-function/objects-and-arrays#objects
          // Check the link for more details on `returnObjects`
          val: t("weekdays", { returnObjects: true }),
        })}
      </p>
    </div>
  );
};

export default BuiltInFormatsDemo;
```

The more you look, the more you'll be amazed
<Img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ojfg949500yj9ystfyyc.gif" width={502} height={468}/>

<br />

#### Other translation functions to check

- [More info about Interpolation](https://www.i18next.com/translation-function/interpolation)
- [Nesting](https://www.i18next.com/translation-function/nesting)
- [Pluralization](https://www.i18next.com/translation-function/plurals)
- [Context](https://www.i18next.com/translation-function/context)

<br />

### Fetching translations from backend <a name="fetching-backend"></a>

The work here is mainly done on the backend side or your CMS. On the frontend, we simply fetch the translations and pass a parameter to distinguish the language we want.

<br />

I created a simple endpoint to fetch the content of the about page. The result will change based on query param `lang` value.

```js
// pages/api/about.js
export default function handler(req, res) {
  const lang = req.query.lang || "en";

  if (lang === "sv") {
    return res.status(200).json({ message: "Jag är Code Gino" });
  } else if (lang === "zh-CN") {
    return res.status(200).json({ message: "我是代码吉诺" });
  } else {
    return res.status(200).json({ message: "I am Code Gino" });
  }
}
```

#### Sample usage

- `/api/about`: English
- `/api/about?lang=zh-CN`: Simplified Chinese 
- `/api/about?lang=sv`: Svenska
- `/api/about?lang=invalid`: English

<br />

We can consume the API as usual (e.g. inside `getServerSideProps`, `getStaticProps`, `useEffect`, etc.).

<br />

In this example, let's fetch the translation inside the `getStaticProps`. We can get the `locale` value from the context, then append `?lang=${locale}` to our request URL. 

```jsx
// pages/about.jsx
// This import is not related to fetching translations from backend.
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

export default function About({ message }) {
  return <h1>{message}</h1>;
}

export async function getStaticProps({ locale }) {
  const { message } = await fetch(
    // forward the locale value to the server via query params
    `https://next-i18n-example-cg.vercel.app/api/about?lang=${locale}`
  ).then((res) => res.json());

  return {
    props: {
      message,
      // The code below is not related to fetching translations from backend.
      ...(await serverSideTranslations(locale, ["common"])),
    },
  };
}
```

The code above will yield the following result:

<Img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qiqwndvaoxogaghs1jmw.gif" width={500} height={300}/>

<br />

## Conclusion

Internationalization is a complex requirement simplified in Next.js due to the built-in i18n routing support and the easy integration of next-i18next. And because `next-i18next` is using `i18next`, we can perform better translations with less code.