How to internationalize Next.js app directory
Tags: react, nextjs, webdev, tutorial
Published: 2023-11-23 06:00:00

In the ever-expanding digital landscape, reaching a global audience is not just a goal but a necessity. Whether you're building a personal portfolio or a sophisticated web application, making it accessible to users around the world is a game-changer. And that's where the magic of internationalization comes into play!

In this blog, we'll embark on a journey to internationalize your Next.js application with app directory, unlocking the power to speak to users in their preferred languages and creating a more inclusive and user-friendly experience. Don't worry if you're new to the concept – we'll guide you through the process step by step, making it an enjoyable and enlightening adventure.

So, grab your favorite coding beverage ☕, put on your explorer hat 🎩, and let's dive into the exciting world of internationalization with Next.js! 🚀🌐

## Terminology

- Locale: An identifier for a set of language and formatting preferences. This usually includes the preferred language of the user and possibly their geographic region.

  - `en-US`: English as spoken in the United States
  - `de-DE`: Deutsch as spoken in Germany

## Setup

First, let's create a fresh next.js app. Make sure our versions of Next.js match. At the moment, I am using **Next v14**. To sync up with the `package.json` file, make sure you checkout this [repository](http://github.com/developeratul/nextjs-appdir-intl) created just for this tutorial.

```bash
pnpm dlx create-next-app nextjs-appdir-intl
```

Make sure you are using app dir because this is what we will be covering today.

Install necessary packages:

```bash
pnpm add next-intl
```

We are all set 🚀

## Folder structure

```
├── locales
│   ├── en-US.json
│   └── de-DE.json
├── next.config.js
├── middleware.ts
├── i18n.ts
├── formatters.ts
└── app
    └── [locale]
        ├── layout.tsx
        └── page.tsx
```

## Localized routing

Let's sprinkle some magic into the user experience! Instead of making users figure out which language to choose, why not check what language their browser likes and take them straight to the right page?

Here's the plan:

- **English about page**: `/en-US/about`
- **German about page**: `/de-DE/about`

Let's edit the `i18n.ts` file:

```ts
import { getRequestConfig } from "next-intl/server";

export const supportedLocales = ["en-US", "de-DE"];
export const defaultLocale = "en-US";

export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`./locales/${locale}.json`)).default,
}));
```

`next.config.js`

```js
const withNextIntl = require("next-intl/plugin")("./i18n.ts");

/** @type {import('next').NextConfig} */
const nextConfig = withNextIntl({});

module.exports = nextConfig;
```

`middleware.ts`

```ts
import createMiddleware from "next-intl/middleware";
import { defaultLocale, supportedLocales } from "./i18n";

export default createMiddleware({
  // A list of all locales that are supported
  locales: supportedLocales,

  // Used when no locale matches
  defaultLocale,
});

export const config = {
  // Match only internationalized pathnames
  matcher: ["/", "/(de-DE|en-US)/:path*"],
};
```

`app/[locale]/layout.tsx`

```tsx
import { defaultLocale, supportedLocales } from "@/i18n";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { notFound } from "next/navigation";
import { ReactNode } from "react";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

interface Props {
  children: ReactNode;
  params: { locale: string };
}

export default function RootLayout(props: Props) {
  const { children, params } = props;

  if (!supportedLocales.includes(params.locale)) notFound();

  return (
    <html lang={params.locale || defaultLocale}>
      <body className={inter.className}>{children}</body>
    </html>
  );
}
```

It's all it takes. Now if you run the app and refresh, you should be able to see the localized path on the URL.

![Localized Next.js url path](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vwewyrec8uehal2hftd4.png)

## Add translations

Now that we have got localized routing and we can keep track of the current locale, let's go ahead and add the translations that we are gonna need.

`locales/en-US.json`

```json
{
  "home_page": {
    "top_card": "Get started by editing app/page.tsx",
    "cards": {
      "card_1": {
        "title": "Docs",
        "description": "Find in-depth information about Next.js features and API."
      },
      "card_2": {
        "title": "Learn",
        "description": "Learn about Next.js in an interactive course with quizzes!"
      },
      "card_3": {
        "title": "Templates",
        "description": "Explore starter templates for Next.js."
      },
      "card_4": {
        "title": "Deploy",
        "description": "Instantly deploy your Next.js site to a shareable URL with Vercel."
      }
    }
  },

  "common_words": {
    "by": "By"
  }
}
```

`locales/de-DE.json`

```json
{
  "home_page": {
    "top_card": "Beginnen Sie mit der Bearbeitung von app/page.tsx",
    "cards": {
      "card_1": {
        "title": "Dokumente",
        "description": "Hier finden Sie ausführliche Informationen zu den Funktionen und der API von Next.js."
      },
      "card_2": {
        "title": "Learn",
        "description": "Erfahren Sie mehr über Next.js in einem interaktiven Kurs mit Quiz!"
      },
      "card_3": {
        "title": "Vorlagen",
        "description": "Entdecken Sie Starter-Vorlagen für Next.js."
      },
      "card_4": {
        "title": "Einsetzen",
        "description": "Stellen Sie Ihre Next.js-Site sofort mit Vercel unter einer gemeinsam nutzbaren URL bereit."
      }
    }
  },

  "common_words": {
    "by": "Von"
  }
}
```

## Translating

Now that we have all the translations, it's time to apply them in the code. We will have to replace all the hard code texts.

### Translating server components

```tsx
export default async function Home(props: { params: { locale: string } }) {
  const t = await getTranslations({ locale: props.params.locale });
  return (
    <div>
      <h1>{t("home_page.top_card")}</h1>
    </div>
  );
}
```

### Translating client components

```tsx
import { useTranslations } from "next-intl";

export default function Home(props: { params: { locale: string } }) {
  const t = useTranslations();
  return (
    <div>
      <h1>{t("home_page.top_card")}</h1>
    </div>
  );
}
```

Let's apply this into `app/[locale]/page.tsx` file:

```tsx
import { getTranslations } from "next-intl/server";
import Image from "next/image";

export default async function Home(props: { params: { locale: string } }) {
  const t = await getTranslations({ locale: props.params.locale });
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
        <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
          {t("home_page.top_card")}
        </p>
        <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
          <a
            className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
            href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            target="_blank"
            rel="noopener noreferrer"
          >
            {t("common_words.by")}{" "}
            <Image
              src="/vercel.svg"
              alt="Vercel Logo"
              className="dark:invert"
              width={100}
              height={24}
              priority
            />
          </a>
        </div>
      </div>

      <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
        <Image
          className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
          src="/next.svg"
          alt="Next.js Logo"
          width={180}
          height={37}
          priority
        />
      </div>

      <div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
        <a
          href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {t("home_page.cards.card_1.title")}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            {t("home_page.cards.card_1.description")}
          </p>
        </a>

        <a
          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {t("home_page.cards.card_2.title")}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            {t("home_page.cards.card_2.description")}
          </p>
        </a>

        <a
          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {t("home_page.cards.card_3.title")}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            {t("home_page.cards.card_3.description")}
          </p>
        </a>

        <a
          href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {t("home_page.cards.card_4.title")}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            {t("home_page.cards.card_4.description")}
          </p>
        </a>
      </div>
    </main>
  );
}
```

![English version](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/30i9btzylofdekivdia6.png)

![German version](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfyswbg2ohw2l547tir5.png)

## Translating Dates and Numbers

Now we have all the content localized but we still have to localize the dates and numbers. For that, we can use builtin [JavaScript Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl).

`formatters.ts`

```ts
export class NumberFormatter {
  private readonly formatter: Intl.NumberFormat;

  constructor(locale: string, options?: Intl.NumberFormatOptions) {
    this.formatter = new Intl.NumberFormat(locale, options);
  }

  format(value: number): string {
    return this.formatter.format(value);
  }
}

export class DateFormatter {
  private readonly formatter: Intl.DateTimeFormat;

  constructor(locale: string, options?: Intl.DateTimeFormatOptions) {
    this.formatter = new Intl.DateTimeFormat(locale, {
      month: "long",
      year: "numeric",
      day: "numeric",
      hour: "numeric",
      minute: "numeric",
      ...options,
    });
  }

  format(value: Date): string {
    return this.formatter.format(value);
  }
}
```

This is how they have to be initialized:

```ts
const dateFormatter = new DateFormatter(props.params.locale);
const numFormatter = new NumberFormatter(props.params.locale);
```

Let's put them into action in the `app/[locale]/page.tsx` file:

```tsx
import { DateFormatter, NumberFormatter } from "@/formatters";
import { getTranslations } from "next-intl/server";
import Image from "next/image";

export default async function Home(props: { params: { locale: string } }) {
  const t = await getTranslations({ locale: props.params.locale });
  const dateFormatter = new DateFormatter(props.params.locale);
  const numFormatter = new NumberFormatter(props.params.locale);
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
        <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
          {t("home_page.top_card")}
        </p>
        <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
          <a
            className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
            href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            target="_blank"
            rel="noopener noreferrer"
          >
            {t("common_words.by")}{" "}
            <Image
              src="/vercel.svg"
              alt="Vercel Logo"
              className="dark:invert"
              width={100}
              height={24}
              priority
            />
          </a>
        </div>
      </div>

      <div className="relative gap-8 flex flex-col place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
        <Image
          className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
          src="/next.svg"
          alt="Next.js Logo"
          width={180}
          height={37}
          priority
        />
        <div>
          <h2>The current time is {dateFormatter.format(new Date())}</h2>
          <p>It takes {numFormatter.format(0)} to start learning</p>
        </div>
      </div>

      <div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
        <a
          href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {t("home_page.cards.card_1.title")}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            {t("home_page.cards.card_1.description")}
          </p>
        </a>

        <a
          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {t("home_page.cards.card_2.title")}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            {t("home_page.cards.card_2.description")}
          </p>
        </a>

        <a
          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {t("home_page.cards.card_3.title")}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            {t("home_page.cards.card_3.description")}
          </p>
        </a>

        <a
          href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {t("home_page.cards.card_4.title")}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
            {t("home_page.cards.card_4.description")}
          </p>
        </a>
      </div>
    </main>
  );
}
```

## Conclusion

And that's a wrap 🚀
<br />
Kudos to your patience for making it until the end. Hope you found it helpful, and it was a value for your time. If you have something to add or correct or got any question, please feel free to comment. That would help all of us out. Until then, see you in the next one.

## References

- [The Github Repo](https://github.com/developeratul/nextjs-appdir-intl)
- [Next.js official internationalization guide](https://nextjs.org/docs/app/building-your-application/routing/internationalization)
- [Next-intl docs](https://next-intl-docs.vercel.app/docs/getting-started)

## Connect with me

Follow me on socials to consume the different types of content.

### 👤 Minhazur Rahman Ratul

- Twitter ([@developeratul](https://twitter.com/developeratul))
- Github ([@developeratul](http://github.com/developeratul))
- Instagram ([@developeratul](http://instagram.com/developeratul))