DEV Community

Cover image for How to internationalize Next.js app directory
Minhazur Rahman Ratul
Minhazur Rahman Ratul

Posted on

How to internationalize Next.js app directory

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 created just for this tutorial.

pnpm dlx create-next-app nextjs-appdir-intl
Enter fullscreen mode Exit fullscreen mode

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

Install necessary packages:

pnpm add next-intl
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

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,
}));
Enter fullscreen mode Exit fullscreen mode

next.config.js

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

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

module.exports = nextConfig;
Enter fullscreen mode Exit fullscreen mode

middleware.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*"],
};
Enter fullscreen mode Exit fullscreen mode

app/[locale]/layout.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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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

{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

locales/de-DE.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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Translating client components

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

English version

German version

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.

formatters.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);
  }
}
Enter fullscreen mode Exit fullscreen mode

This is how they have to be initialized:

const dateFormatter = new DateFormatter(props.params.locale);
const numFormatter = new NumberFormatter(props.params.locale);
Enter fullscreen mode Exit fullscreen mode

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

And that's a wrap πŸš€


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

Connect with me

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

πŸ‘€ Minhazur Rahman Ratul

Top comments (1)

Collapse
 
arsenii profile image
Arsenii Kozlov

Great tutorial! Let me add a few tips:

For formatting dates and numbers you can use next-intl as well
next-intl-docs.vercel.app/docs/usa...
next-intl-docs.vercel.app/docs/usa...

Also, I can recommend a free tool Gitloc, that can help you to create translation files, as easy as possible.