Multi-Language Website with Next.js 14 App Router and i18next

We already have an article titled “Multi-Language Next.Js 12 Website Using I18next – RTL Support” that explains how to create a multi-language website with the help of Next.js 12 page router and i18next – RTL support. However, this article will discuss how to create a multi-language website using Next.js 14 App Router and i18next with RTL Support.

Prerequisites

I assume that the reader has a fundamental knowledge of the following aspects:

  • Building an application using the Next.js framework
  • Working with the Next.js 14 App Router

What we will learn

In this article, we will learn to:-

  • Create a Next.js 14 app with an App router
  • Install and setup i18next in the app
  • Adding the locales/language strings
  • Translating server components (Home page, About page)
  • Translating client components (Navigation component)
  • Switching between the locales
  • Implementing RTL

After completing the article, we will learn to create a Multi-language Next.js app with the workflow given below.

Create a Next.js app

Creating a Next.js application is a straightforward process that can be accomplished using the NPX tool. Simply execute the following command:

npx create-next-app@latest

We are using TypeScript, ESLint rules, and App Router for this Next.js project. So select the options as per that. You can refer to the screenshot below.

The above command will create a new Next.js app with the name nextjs-14-i18n-multi-language-demo. Now direct to the project directory and open it with Visual Studio Code.

cd nextjs-14-i18n-multi-language-demo
code .

Install Required Packages for Internationalization (i18n)

We’ll be using i18next and i18next-resources-to-backend plugin, along with next-i18n-router and react-i18next. These tools will enable us to seamlessly integrate multiple languages into our application, providing a better user experience for a diverse audience.”

npm i i18next i18next-resources-to-backend next-i18n-router react-i18next

Create a Config File for i18n

Add a file to the root directory of our project, /i18nConfig.ts.

import { Config } from "next-i18n-router/dist/types";

const i18nConfig: Config = {
  locales: ["en", "ar"],
  defaultLocale: "en",
};

export default i18nConfig;
  • The locales property is an array of languages we want our app to support.
  • The defaultLocale property is the language visitors will fall back on if our app does not support their language.

Set up a dynamic segment

Now we need to add a dynamic segment inside our /app directory to contain all pages and layouts. This can be achieved by creating a directory named inside square brackets. I am creating a directory /app/[locale] as shown below.

Create a Middleware

At the root of our project, add a /middleware.ts file.

import { i18nRouter } from "next-i18n-router";
import i18nConfig from "./i18nConfig";
import { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  return i18nRouter(request, i18nConfig);
}

// only applies this middleware to files in the app directory
export const config = {
  matcher: "/((?!api|static|.*\\..*|_next).*)",
};
  • The i18nRouter function will take the request, detect the user’s preferred language using the accept-language header, and then redirect them to the path with their preferred language.
  • If we don’t support their language, it will fall back to the default language.
  • next-i18n-router also lets us customize the detection logic if you wish.

Create Language/ Locale Files for Each language

We need to store language strings for both English (en) and Arabic (ar) in our app. To do so, we should create a directory and two subdirectories within it – one for Arabic and one for English. The Arabic subdirectory should be named ar and the English subdirectory should be named en Here’s an example of what the file structure should look like:

Inside /locales/ar/common.json, add the below language strings.

{
  "navigation": {
    "Home": "مسكن",
    "About": "حول"
  },
  "home": {
    "Home title": "عنوان المنزل",
    "Home description": "وصف المنزل"
  },
  "about": {
    "About title": "حول العنوان",
    "About description": "حول الوصف"
  }
}

In the same manner, create /locales/en/common.json as below.

{
  "navigation": {
    "Home": "Home",
    "About": "About"
  },
  "home": {
    "Home title": "Home title",
    "Home description": "Home description"
  },
  "about": {
    "About title": "About title",
    "About description": "About description"
  }
}

Create an initializeTranslations Function to Generate an i18next Instance

I’m going to create a file in my /app directory  i18n.ts that contains a function to generate an i18next instance.

import { Resource, createInstance, i18n } from "i18next";
import { initReactI18next } from "react-i18next/initReactI18next";
import resourcesToBackend from "i18next-resources-to-backend";
import i18nConfig from "@/i18nConfig";

export default async function initializeTranslations(
  locale: string,
  namespaces: string[],
  i18nInstance?: i18n,
  resources?: Resource
) {
  i18nInstance = i18nInstance || createInstance();

  i18nInstance.use(initReactI18next);

  if (!resources) {
    i18nInstance.use(
      resourcesToBackend(
        (language: string, namespace: string) =>
          import(`@/locales/${language}/${namespace}.json`)
      )
    );
  }

  await i18nInstance.init({
    lng: locale,
    resources,
    fallbackLng: i18nConfig.defaultLocale,
    supportedLngs: i18nConfig.locales,
    defaultNS: namespaces[0],
    fallbackNS: namespaces[0],
    ns: namespaces,
    preload: resources ? [] : i18nConfig.locales,
  });

  return {
    i18n: i18nInstance,
    resources: i18nInstance.services.resourceStore.data,
    t: i18nInstance.t,
  };
}

Add Translation Function Inside the Home Page (Server Component)

We can now use this initializeTranslations function to generate an i18next instance that will translate content on our home page /app/[locale]/page.tsx file.

import initializeTranslations from "../i18n";

const i18nNamespaces = ["common"];

async function Home({ params: { locale } }: { params: { locale: string } }) {
  const { t, resources } = await initializeTranslations(locale, i18nNamespaces);

  return (
      <div className="container">
        <div className="mt-5">
          <h1>{t("home.Home title")}</h1>
          <p>{t("home.Home description")}</p>
        </div>
      </div>
  );
}

export default Home;
  • In our page, we are reading the locale from our params and passing it into initTranslations.
  • We are also passing in an array of all of the namespaces required for this page. In our case, we just have only one namespace called “common”.
  • We then call the t function with the key of the string we want to render.

Add Translation Function Inside the About Page (Server Component)

In the same manner, we will add a translation function inside the About page, which is also a server component.

import TranslationsProvider from "@/components/TranslationsProvider";
import Navigation from "@/components/Navigation";
import initializeTranslations from "@/app/i18n";

const i18nNamespaces = ["common"];

async function About({ params: { locale } }: { params: { locale: string } }) {
  const { t, resources } = await initializeTranslations(locale, i18nNamespaces);

  return (
    <TranslationsProvider
      namespaces={i18nNamespaces}
      locale={locale}
      resources={resources}
    >
      <Navigation />
      <div className="container">
        <div className="mt-5">
          <h1>{t("about.About title")}</h1>
          <p>{t("about.About description")}</p>
        </div>
      </div>
    </TranslationsProvider>
  );
}

export default About;

Create a TranslationsProvider for Translating Client Components

To make our translations available to all the Client Components (Navigation Component in our case) on the page, we’re going to use a provider. I’m going to create a new file with a component called TranslationsProvider in /components/TranslationsProvider.ts:

"use client";
import { I18nextProvider } from "react-i18next";
import { ReactNode } from "react";
import initializeTranslations from "@/app/i18n";
import { Resource, createInstance } from "i18next";

export default function TranslationsProvider({
  children,
  locale,
  namespaces,
  resources,
}: {
  children: ReactNode;
  locale: string;
  namespaces: string[];
  resources: Resource;
}) {
  const i18n = createInstance();

  initializeTranslations(locale, namespaces, i18n, resources);

  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}

This provider is a Client Component that creates an i18next instance on the client and uses the I18nextProvider to provide the instance to all descendent Client Components.

We only need to use the provider once per page. Let’s add it to our home page /app/[locale]/page.tsx file.

import TranslationsProvider from "@/components/TranslationsProvider";
import Navigation from "@/components/Navigation";
import initializeTranslations from "../i18n";

const i18nNamespaces = ["common"];

async function Home({ params: { locale } }: { params: { locale: string } }) {
  const { t, resources } = await initializeTranslations(locale, i18nNamespaces);

  return (
    <TranslationsProvider
      namespaces={i18nNamespaces}
      locale={locale}
      resources={resources}
    >
      <Navigation />
      <div className="container">
        <div className="mt-5">
          <h1>{t("home.Home title")}</h1>
          <p>{t("home.Home description")}</p>
        </div>
      </div>
    </TranslationsProvider>
  );
}

export default Home;

Add Translation for Navigation Component (Client Component)

Now that our page has this provider wrapped around it, we can use react-i18next in our Client Components the same way we would use it in any React app.

If you haven’t used react-i18next it before, the way that we render translations is by using a hook named useTranslation.

Let’s use it in our Navigation Component.

"use client";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { usePathname } from "next/navigation";

export default function Navigation() {
  const pathname = usePathname();

  const { t } = useTranslation("common");

  return (
    <nav className="navbar navbar-expand-lg bg-light">
      <div className="container-fluid">
        <Link href="/" className="navbar-brand">
          Next.js 14 Multi-Language
        </Link>
        <div className="navbar-collapse" id="navbarText">
          <ul className="navbar-nav me-auto mb-2 mb-lg-0">
            <li className="nav-item">
              <Link
                href="/"
                className={`nav-link ${pathname === "/" ? "active" : ""}`}
              >
                {t("navigation.Home")}
              </Link>
            </li>
            <li className="nav-item">
              <Link
                href="/about"
                className={`nav-link ${pathname === "/about" ? "active" : ""}`}
              >
                {t("navigation.About")}
              </Link>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  );
}

Just like with our initializeTranslations function, we call the t function with our string’s key.

Create a Component for Changing Languages

The next-i18n-router does a good job of detecting a visitor’s preferred language, but oftentimes we want to allow our visitors to change the language themselves.

To do this, we will create a dropdown for our user to select their new language. We will take their selected locale and set it as a cookie named "NEXT_LOCALE" that next-i18n-router uses to override the automatic locale detection.

For this, we are creating a new component called /components/LocaleSwitcher.tsx file.

"use client";
import { useRouter } from "next/navigation";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import i18nConfig from "@/i18nConfig";
import { ChangeEvent } from "react";

export default function LocaleSwitcher() {
  const { i18n } = useTranslation();
  const currentLocale = i18n.language;
  const router = useRouter();
  const currentPathname = usePathname();

  const handleChangeLocale = (e: ChangeEvent<HTMLSelectElement>) => {
    const newLocale = e.target.value;

    // set cookie for next-i18n-router
    const days = 30;
    const date = new Date();
    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
    document.cookie = `NEXT_LOCALE=${newLocale};expires=${date.toUTCString()};path=/`;

    // redirect to the new locale path
    if (
      currentLocale === i18nConfig.defaultLocale &&
      !i18nConfig.prefixDefault
    ) {
      router.push("/" + newLocale + currentPathname);
    } else {
      router.push(
        currentPathname.replace(`/${currentLocale}`, `/${newLocale}`)
      );
    }

    router.refresh();
  };

  return (
    <select onChange={handleChangeLocale} value={currentLocale}>
      <option value="en">English</option>
      <option value="ar">Arabic</option>
    </select>
  );
}

Let’s also add the LocaleSwitcher component in our Navigation component and try it out.

"use client";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import LocaleSwitcher from "./LocaleSwitcher";
import { usePathname } from "next/navigation";

export default function Navigation() {
  const pathname = usePathname();

  const { t } = useTranslation("common");

  return (
    <nav className="navbar navbar-expand-lg bg-light">
      <div className="container-fluid">
        <Link href="/" className="navbar-brand">
          Next.js 14 Multi-Language
        </Link>
        <div className="navbar-collapse" id="navbarText">
          <ul className="navbar-nav me-auto mb-2 mb-lg-0">
            <li className="nav-item">
              <Link
                href="/"
                className={`nav-link ${pathname === "/" ? "active" : ""}`}
              >
                {t("navigation.Home")}
              </Link>
            </li>
            <li className="nav-item">
              <Link
                href="/about"
                className={`nav-link ${pathname === "/about" ? "active" : ""}`}
              >
                {t("navigation.About")}
              </Link>
            </li>
          </ul>
          <LocaleSwitcher />
        </div>
      </div>
    </nav>
  );
}

Generate Static HTML Files for All Languages

Lastly, let’s update our layout.ts. We’ll use generateStaticProps so that Next.js statically generates pages for each of our languages.

We’ll also make sure to add the current locale to the <html> tag of our app. This enables the RTL support for the supported languages.

import i18nConfig from "@/i18nConfig";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { ReactNode } from "react";
import { dir } from "i18next";

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

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

export function generateStaticParams() {
  return i18nConfig.locales.map((locale) => ({ locale }));
}

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

GitHub

You can clone this project from the GitHub repository to access the code and work on it.

https://github.com/techomoro/nextjs-14-i18n-multi-language-demo

Summary

Configuring the react-i18next with the Next.js App Router is distinct from setting it up on a regular React app using Client Side Rendering or the Next.js Pages Router. However, once we have completed the initial setup, we will find that the development process is not too dissimilar.

After considering multiple strategies for setting up react-i18next with the App Router, we found this approach to be the most efficient.

Be the first to reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.