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 theaccept-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 intoinitTranslations
. - 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/codeariv/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