Implement internationalization (i18n) in Next 13 (actually working)

Since the release of Next 13 and the app directory feature, implementing internationalization have changed, but documentations didn't... Let's figure this out.

Implement internationalization (i18n) in Next 13 (actually working)

Introduction

I know you've been looking hard for a solution, so I'll make this article quick and straightforward.

We'll be using the next-translate package to handle translations for the following locales: English (en), Spanish (es) and French (fr).

Our app will handle translations by applying the lang parameter as a sub-path (/<LOCALE>/<PAGE>)

You can find the final code in the Github repo here.

 

Setting up next-translate

Start by installing the next-translate package: yarn add next-translate && yarn add next-translate-plugin -D

Next, re-adapt your next.config.js file:

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

const nextTranslate = require("next-translate-plugin");

module.exports = nextTranslate(nextConfig);

And create a i18n.js file in the root of the project (don't worry about the "default" locale, I'll explain):

module.exports = {
  locales: ["default", "en", "es", "fr"],
  defaultLocale: "default",
  localeDetection: false,
  pages: {
    "*": ["common"],
  },
};

Create a directory named locales in the root of your project, and add the needed translation files (/locales/<LANG>/<NAMESPACE>.json) along the wished keys.

 

Setting up Next 13 app directory

We'll need to re-organize our app structure to handle internationalization. Wrap your pages and layout in a [lang] directory inside your /app directory.

Modify your root layout to pass it the lang parameter in the html lang attribute:

// app/[lang]/layout.tsx

export default function RootLayout({
  children,
  params: { lang },
}: {
  children: React.ReactNode;
  params: { lang: string };
}) {
  return (
    <html lang={lang}>
      <body>{children}</body>
    </html>
  );
}

 

Setting up the middleware

next-translate uses the ?lang search param to detect the wished language. For this, we're going to need a middleware that will apply this parameter to each page.

The middleware will also redirect the user to the default locale if he lands on a malformed page, so that if the user visits /page, he will be redirected to /en/page.

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import i18n from "../i18n";

/** Regex to check if current path equals to a public file */
const PUBLIC_FILE = /\.(.*)$/;
/** */

/** Ignore non-page paths */
const shouldProceed = (pathname: string) => {
  if (
    pathname.startsWith("/_next") ||
    pathname.includes("/api/") ||
    PUBLIC_FILE.test(pathname)
  ) {
    return false;
  }
  return true;
};

export async function middleware(request: NextRequest) {
  const { locale, pathname } = request.nextUrl;

  /** Ignore non-page paths */
  if (!shouldProceed(pathname)) return;
  /** */

  if (request.nextUrl.locale === "default") {
    /** Get user's locale */
    const storedLocale = request.cookies.get("NEXT_LOCALE")?.value;
    /** */

    const response = NextResponse.redirect(
      new URL(
        `/${storedLocale || "en"}/${request.nextUrl.pathname}`,
        request.url
      )
    );

    /** Store default locale in user's cookies */
    if (!storedLocale)
      response.cookies.set("NEXT_LOCALE", "en", {
        path: "/",
      });
    /** */

    /** Redirect user to default locale */
    return response;
  }
  /** Adds ?lang={locale} for next-translate package */

  request.nextUrl.searchParams.set("lang", locale);

  /** */

  return NextResponse.rewrite(request.nextUrl);
}

Now, each time you visit a page, the middleware will apply the sub-path routing. It will also save a default language in the NEXT_LOCALE cookie. Let's move forward and create a component to switch language.

 

Switching language

This component is really simple. It does the following:

  • Get the current pathname
  • Replace the lang param with the chosen language
  • Redirect the user to the same page but in a different language

Here you go for some copy-pasta:

"use client";

// src/components/LangSwitcher/index.tsx
import i18n from "../../../i18n";
import { useParams, usePathname, useRouter } from "next/navigation";

export const LangSwitcher = () => {
  const { lang } = useParams();
  const pathname = usePathname();
  const router = useRouter();

  return (
    <select
      onChange={(e) => {
        const value = e.target.value;
        const correctPathname = pathname.replace(`/${lang}`, `/${value}`);
        router.push(correctPathname);
      }}
      defaultValue={lang}
    >
      {i18n.locales
        .filter((x) => x !== "default")
        .map((res) => (
          <option key={res} value={res}>
            {res}
          </option>
        ))}
    </select>
  );
};

 

Navigating with translation

The final step is to implement a way to navigate to different internal pages with the same language. For this, we will have to prepend our locale param to every link. So let's create a simple component that acts like the Link component from Next but prepends our locale to the href attribute:

// src/components/TLink/index.tsx

"use client"; import Link from "next/link"; import { useParams } from "next/navigation"; import { PropsWithChildren, Ref } from "react"; export const TLink = ( props: React.ComponentProps<typeof Link> & PropsWithChildren ) => { const { lang } = useParams(); const { children, href, ...linkProps } = props; return ( <Link href={`/${lang}${href}`} {...linkProps}> {children} </Link> ); };

 

Wrapping the whole thing up

You successfully implemented internationalization in your Next 13 app with next-translate. Let's test this implementation by creating two pages (don't forget to create the keys in your common.json files):

// src/app/[lang]/page.tsx

export default function Home() { const { t } = useTranslation(); return ( <main> <h2 style={{ textAlign: "center", padding: "20px 0px" }}> {t("common:hello")} </h2> <LangSwitcher /> <TLink style={{ marginTop: "20px", display: "block" }} href="/second"> Go to second page </TLink> </main> ); }

 

// src/app/[lang]/second/page.tsx

import { LangSwitcher } from "@/components/LangSwitcher"; import { TLink } from "@/components/TLink"; import useTranslation from "next-translate/useTranslation"; const SecondPage = () => { const { t } = useTranslation(); return ( <main> <h2 style={{ textAlign: "center", padding: "20px 0px", color: "greenyellow", }} > {t("common:welcome")} </h2> <LangSwitcher /> <TLink style={{ marginTop: "20px", display: "block" }} href="/"> Go to home </TLink> </main> ); }; export default SecondPage;

It just works. Congrats!

 

Problems with i18n and Next 13

Link doesn't support the locale props

For some mysterious reason, passing the locale prop to the Link component doesn't work. You can raise an issue or a discussion in the NextJS repo is you wish.

 

Lack of documentation

There's definitely something off with Next 13 and internationalization. I literally spent a whole day to set this up, and the Web didn't help me at all. I even saw a few people mentioning those issues, but absolutely no one answered. Really really frustrating

For example, Next cuts the defaultLocale value from your pathname, meaning that you can't catch it in the middleware. But this isn't mentioned in the documentation, and asking the question in their Github seems to be useless.

 

Conclusion

I hope this article helped you, I wish someone wrote something similar but couldn't find any, so here I am. If you know any one struggling with this, don't hesitate to send them this article. Again, you can find the final code in my Github repo here.