Azhar ZamanAzhar Zaman
Download CV
Implementing i18n in Next.js 15 (App) using i18next

Implementing i18n in Next.js 15 (App) using i18next

May 22, 2025

•

3 min read

Azhar Zaman

Azhar Zaman

Full-stack Developer

If you’ve made it to this article, you’re probably working on a multilingual site in Next.js 15+ using the new App Router. Good news: you’re in the right place. We're going to integrate internationalization (i18n) using i18next, react-i18next, next-i18n-router, and i18next-resources-to-backend.

Let’s build a setup that works on both server and client, doesn’t download translations twice, and won’t drive you (or your future self) crazy.

Initial Setup & Middleware Configuration

Step 1: Create a new Next.js App

Use the latest Next.js setup tool by running:

1npx create-next-app@latest my-next-i18n-app

This command will set up a fresh Next.js 15+ project with App Router.

Installing Dependencies

Before diving into the implementation, install the required packages:

1npm install i18next react-i18next i18next-resources-to-backend next-i18n-router

Or if you're using Yarn:

1yarn add i18next react-i18next i18next-resources-to-backend next-i18n-router

Step 2: Add a Dynamic Locale Route

Next, structure your application to support multiple languages dynamically. In the app directory, create a dynamic route folder named [locale]. Your directory structure will look like this:

This setup allows the app to handle different locales based on URL segments (/en, /ar, etc.).

Step 3: Create an i18n Configuration File

To manage locales, create a configuration file with your supported locales, default locale. For this guides purpose, we will assume you are using src directory.

Create src/lib/i18n/index.ts with this content.

1import { AllowedLocales, Locale, I18NConfig } from "@/lib/types";
2
3export const defaultLocale = "en";
4
5export const allowedLocales: AllowedLocales = [defaultLocale, "ar"];
6
7const i18nConfig: I18NConfig = {
8  locales: allowedLocales,
9  defaultLocale,
10  prefixDefault: false, // avoids prefixing the default locale in URLs
11};
12
13export { i18nConfig };

Now it's time to define types. Create another file src/lib/types/index.ts

1export type Locale = "en" | "ar";
2
3export type AllowedLocales = Locale[];
4
5export type I18NConfig = {
6  locales: string[];
7  defaultLocale: string;
8  prefixDefault: boolean;
9};

Step 4: Middleware Setup for Locale Route Handling

To automatically handle locale-specific routing, create a middleware file at the root level src/middleware.ts:

1import { i18nRouter } from "next-i18n-router";
2import { i18nConfig } from "./lib/i18n/index";
3
4export function middleware(request: any) {
5  return i18nRouter(request, i18nConfig);
6}
7
8// Apply middleware only to relevant routes (excluding API, static files, and internal Next.js paths)
9export const config = {
10  matcher: "/((?!api|static|.*\\..*|_next).*)",
11};

Explanation:

  • This middleware intercepts incoming requests, checks for locale information, and automatically routes the request to the appropriate locale-based path.
  • The matcher ensures the middleware doesn't interfere with API endpoints or static assets.

Step 5: Set lang and dir inside Root Layout

Next we need to set lang and dir attribute on <html> tag inside root layout. For that we will write a utility function that gives us direction for active locale.
Inside src/lib/utils/index.ts

1import type { Locale } from "./types";
2
3export function getDirection(locale: Locale) {
4  if (locale === "ar") {
5    return "rtl";
6  }
7  return "ltr";
8}

Let's use this utility function inside src/app/[locale]/layout.tsx which is also root layout of the Next.js app.

1import { getDirection } from "@/lib/utils";
2import { Locale } from "@/lib/types";
3
4type RootLayoutProps = {
5  children: React.ReactNode;
6  params: Promise<{ locale: Locale }>;
7};
8
9export default async function RootLayout({
10  children,
11  params,
12}: RootLayoutProps) {
13  const { locale } = await params;
14  return (
15    <html lang={locale} dir={getDirection(locale)}>
16      <body
17        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
18      >
19          {children}
20      </body>
21    </html>
22  );
23}

Step 6: Add translations

Next we will add translations as JSON files inside src/lib/i18n/transalations that we will read in next steps. Create a structured directory to organize your translation JSON files:

/translations/en/home.json

1{
2  "welcome": "Welcome",
3  "description": "This is an English description."
4}

/translations/ar/home.json

1{
2  "welcome": "مرحبا",
3  "description": "هذا وصف باللغة العربية."
4}

Server side Internationalization (i18n)

In Next.js App Router, translations are loaded by default on the server side. Let's see how you can efficiently manage and consume translations.

Step 1: Create Server-side Translation Utility

Next, implement a utility that initializes and fetches translations server-side. We will export serverSideTranslation from src/lib/i18n/index.ts which helps us load translations anywhere on server (components, functions like generateMetadata).

src/lib/i18n/index.ts

1import { createInstance, TFunction } from "i18next";
2import { initReactI18next } from "react-i18next/initReactI18next";
3import resourcesToBackend from "i18next-resources-to-backend";
4import { i18nConfig } from "@/lib/i18n/index";
5import { Locale } from "../types/i18n";
6
7async function initTranslations(
8  locale: string,
9  namespaces: string[],
10  options?: { keyPrefix?: string; i18nInstance?: any; resources?: any }
11) {
12  const i18nInstance = options?.i18nInstance || createInstance();
13
14  i18nInstance.use(initReactI18next);
15
16  if (!options?.resources) {
17    i18nInstance.use(
18      resourcesToBackend(
19        (language: string, namespace: string) =>
20          import(`@/lib/i18n/translations/${language}/${namespace}.json`)
21      )
22    );
23  }
24
25  await i18nInstance.init({
26    lng: locale,
27    resources: options?.resources,
28    fallbackLng: i18nConfig.defaultLocale,
29    supportedLngs: i18nConfig.locales,
30    defaultNS: namespaces[0],
31    fallbackNS: namespaces[0],
32    ns: namespaces,
33    preload: options?.resources ? [] : i18nConfig.locales,
34  });
35
36  return {
37    i18n: i18nInstance,
38    resources: i18nInstance.services.resourceStore.data,
39    t: i18nInstance.getFixedT(
40      locale,
41      namespaces[0],
42      options?.keyPrefix
43    ) as TFunction<Locale, undefined>,
44  };
45}
46
47// Exported function to fetch translations on server components
48export async function serverSideTranslation(
49  locale: string,
50  namespaces: string[],
51  options?: { keyPrefix?: string; i18nInstance?: any; resources?: any }
52) {
53  return await initTranslations(locale, namespaces, options);
54}

Step 2: Consuming Translations in Server Components

To use these translations in your server components, for example inside home page (src/app/[locale]/page.tsx)

1import { serverSideTranslation } from "@/lib/i18n";
2
3type Props = {
4  params: Promise<{ locale: string }>
5}
6
7export default async function Page({ params }: Props) {
8  const {locale} = await params
9  const ns = ["home"]; // namespace used for this page
10  const { t } = await serverSideTranslation(locale, ns);
11
12  return (
13    <div>
14      <h1>{t("welcome")}</h1>
15      <p>{t("description")}</p>
16    </div>
17  );
18}

Explanation:

  • locale: Comes directly from the dynamic route params ([locale]).
  • ns: The namespace corresponds to the JSON translation file (home.json).
  • t: Translation function returned from serverSideTranslation.

Client-side Internationalization (i18n)

Why Send Translations to the Client?

In Next.js App Router, translations are loaded server-side by default. However, your client-side components (interactive components, buttons, inputs, etc.) also need access to translations. To achieve this, you'll pass translations from the server to the client via a wrapper provider.

Step 1: Create the Client-side Translation Provider

Create a wrapper component (I18NProvider) that passes translations from server-side to client-side components:

Create new file src/components/i18n/i18n-provider.tsx

1"use client";
2
3import { I18nextProvider } from "react-i18next";
4import { createInstance } from "i18next";
5import { serverSideTranslation } from "@/lib/i18n";
6
7type Props = {
8  children: React.ReactNode;
9  locale: string;
10  namespaces: string[];
11  resources: any;
12};
13
14export default function I18NProvider({
15  children,
16  locale,
17  namespaces,
18  resources,
19}: Props) {
20  const i18n = createInstance();
21
22  // Initialize client-side i18n instance with resources provided by the server
23  serverSideTranslation(locale, namespaces, { i18nInstance: i18n, resources });
24
25  return (
26    <I18nextProvider i18n={i18n}>
27      {children}
28    </I18nextProvider>
29  );
30}
31

Explanation:

  • Uses createInstance() from i18next to initialize a new client-side i18n instance.
  • Reuses server-loaded translations by passing resources directly to the client, avoiding extra client-side fetches.

Step 2: Wrap Your Client Components

In your server components (or page, layouts), wrap your client-side tree using the I18NProvider created above:

1import { serverSideTranslation } from "@/lib/i18n";
2import ClientComponent from "./client-component"
3
4type Props = {
5  params: Promise<{ locale: string }>
6}
7
8export default async function Page({ params }: Props) {
9  const {locale} = await params
10  const ns = ["home"]; // namespace used for this page
11  const { t } = await serverSideTranslation(locale, ns);
12
13  return (
14    <div>
15      <h1>{t("welcome")}</h1>
16      <p>{t("description")}</p>
17      
18      <I18NProvider locale={locale} namespaces={ns} resources={resources}>
19        <ClientComponent />
20      </I18NProvider>
21    </div>
22  );
23}

Explanation:

  • Page fetches translations server-side via serverSideTranslation.
  • Passes resources and locale to the client-side via the I18NProvider.

Step 3: Consuming Translations in Client Components

Once wrapped, any client-side component can easily access translations using the familiar useTranslation hook:

But, first add some translation strings to;

translations/en/home.json

1{
2  "welcome": "Welcome",
3  "description": "This is an English description.",
4  "client-component": {
5    "heading": "This is client component"
6  }
7}

translations/ar/home.json

1{
2  "welcome": "مرحبا",
3  "description": "هذا وصف باللغة العربية.",
4  "client-component": {
5    "heading": "هذا هو مكون العميل"
6  }
7}

Now let's use translations inside client component (src/app/[locale]/client-component.tsx).

1"use client";
2
3import { useTranslation } from "react-i18next";
4
5export default function ClientComponent() {
6  const { t } = useTranslation("home", {
7    keyPrefix: "content.client-component"
8  });
9
10  return (
11    <div>
12      <h2>{t("heading")}</h2>
13    </div>
14  );
15}

Explanation:

  • Simply imports useTranslation hook.
  • Uses the same keys defined in your translation JSON (home.json).

Locale Switcher

To allow users to switch between languages on the client side, you'll implement a locale switcher component. This component updates the language preference, persists it in a cookie, and redirects the user to the correct localized route.

src/components/i18n/locale-switch.tsx

1"use client";
2
3import React from "react";
4import { usePathname, useRouter } from "next/navigation";
5import { useTranslation } from "react-i18next";
6import { i18nConfig } from "@/lib/i18n";
7
8export default function LocaleSwitch() {
9  const { i18n } = useTranslation();
10  const activeLocale = i18n.language;
11
12  const router = useRouter();
13  const currentPathname = usePathname();
14
15  const onLanguageChange = () => {
16    const newLocale = activeLocale === "en" ? "ar" : "en";
17
18    // Set a cookie for next-i18n-router to read the new locale
19    const days = 30;
20    const date = new Date();
21    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
22    const expires = date.toUTCString();
23    document.cookie = `NEXT_LOCALE=${newLocale};expires=${expires};path=/`;
24
25    // Compute new path with updated locale
26    if (activeLocale === i18nConfig.defaultLocale && !i18nConfig.prefixDefault) {
27      router.push("/" + newLocale + currentPathname);
28    } else {
29      router.push(currentPathname.replace(`/${activeLocale}`, `/${newLocale}`));
30    }
31
32    router.refresh();
33  };
34
35  return (
36    <div className="widget language-switch-widget">
37      <button
38        className="w-auto cursor-pointer bg-transparent"
39        onClick={onLanguageChange}
40      >
41          {activeLocale === "en" ? "عربي" : "English"}
42      </Button>
43    </div>
44  );
45}

Now you can simply render this anywhere inside your app. Don't forget to wrap it inside I18NProvider - for example if you are rendering it inside root layout (src/app/[locale]/layout.tsx).

1import { getDirection } from "@/lib/utils";
2import { Locale } from "@/lib/types";
3import LocaleSwitch from "@/components/i18n/locale-switch";
4import I18NProvider from "@/components/i18n/translations-provider";
5
6type RootLayoutProps = {
7  children: React.ReactNode;
8  params: Promise<{ locale: Locale }>;
9};
10
11export default async function RootLayout({
12  children,
13  params,
14}: RootLayoutProps) {
15  const { locale } = await params;
16  return (
17    <html lang={locale} dir={getDirection(locale)}>
18      <body
19        className={`${geistSans.variable} ${geistMono.variable} antialiased relative`}
20      >
21        {/* pass locale to client-side i18n provider */}
22        <I18NProvider locale={locale} namespaces={[]} resources={[]}>
23            {children}
24            <LocaleSwitch />
25        </I18NProvider>
26      </body>
27    </html>
28  );
29}

Conclusion

You're now fully equipped to build a multilingual Next.js 15+ App Router application with i18next!

See you in next one.