IntlPull
Tutorial
12 min read

Next.js App Router with react-i18next: Complete i18n Tutorial 2026

Step-by-step tutorial for setting up react-i18next in Next.js 14, 15, and 16 App Router. Learn Server Components, Client Components, and language switching.

IntlPull Team
IntlPull Team
03 Feb 2026, 11:44 AM [PST]
On this page
Summary

Step-by-step tutorial for setting up react-i18next in Next.js 14, 15, and 16 App Router. Learn Server Components, Client Components, and language switching.

Quick Answer

To use react-i18next with Next.js App Router: Install react-i18next i18next, create an i18n configuration file, set up a [locale] dynamic segment in your app directory, create a translation provider for Client Components, and use useTranslation hook in your components. For Server Components, use the createInstance pattern.


Prerequisites

  • Next.js 14, 15, or 16 with App Router
  • Basic understanding of React Server Components
  • Node.js 18+

Project Setup

Step 1: Install Dependencies

Terminal
npm install react-i18next i18next i18next-resources-to-backend accept-language

Directory Structure

app/
├── [locale]/
│   ├── layout.tsx
│   ├── page.tsx
├── i18n/
│   ├── settings.ts
│   ├── client.tsx
│   └── server.ts
└── locales/
    ├── en/
    │   └── common.json
    ├── es/
    └── fr/

Step 2: Create Translation Files

locales/en/common.json:

JSON
1{
2  "title": "Welcome to our app",
3  "description": "This is a multilingual Next.js application",
4  "greeting": "Hello, {{name}}!",
5  "items_one": "{{count}} item",
6  "items_other": "{{count}} items"
7}

Step 3: Configure i18n Settings

app/i18n/settings.ts:

TypeScript
1export const fallbackLng = 'en';
2export const languages = ['en', 'es', 'fr'];
3export const defaultNS = 'common';
4
5export function getOptions(lng = fallbackLng, ns = defaultNS) {
6  return {
7    supportedLngs: languages,
8    fallbackLng,
9    lng,
10    defaultNS,
11    ns,
12  };
13}

Step 4: Server-Side i18n

app/i18n/server.ts:

TypeScript
1import { createInstance } from 'i18next';
2import resourcesToBackend from 'i18next-resources-to-backend';
3import { initReactI18next } from 'react-i18next/initReactI18next';
4import { getOptions } from './settings';
5
6const initI18next = async (lng: string, ns: string) => {
7  const i18nInstance = createInstance();
8  await i18nInstance
9    .use(initReactI18next)
10    .use(
11      resourcesToBackend(
12        (language: string, namespace: string) =>
13          import(`../locales/\${language}/\${namespace}.json`)
14      )
15    )
16    .init(getOptions(lng, ns));
17  return i18nInstance;
18};
19
20export async function getTranslation(lng: string, ns: string = 'common') {
21  const i18nextInstance = await initI18next(lng, ns);
22  return {
23    t: i18nextInstance.getFixedT(lng, ns),
24    i18n: i18nextInstance,
25  };
26}

Step 5: Client-Side Provider

app/i18n/client.tsx:

TSX
1'use client';
2
3import { useEffect, useState } from 'react';
4import i18next from 'i18next';
5import { initReactI18next, useTranslation as useTranslationOrg } from 'react-i18next';
6import resourcesToBackend from 'i18next-resources-to-backend';
7import { getOptions, languages } from './settings';
8
9const runsOnServerSide = typeof window === 'undefined';
10
11i18next
12  .use(initReactI18next)
13  .use(
14    resourcesToBackend(
15      (language: string, namespace: string) =>
16        import(`../locales/\${language}/\${namespace}.json`)
17    )
18  )
19  .init({
20    ...getOptions(),
21    lng: undefined,
22    preload: runsOnServerSide ? languages : [],
23  });
24
25export function useTranslation(lng: string, ns?: string) {
26  const ret = useTranslationOrg(ns);
27  const { i18n } = ret;
28
29  useEffect(() => {
30    if (lng && i18n.resolvedLanguage !== lng) {
31      i18n.changeLanguage(lng);
32    }
33  }, [lng, i18n]);
34
35  return ret;
36}

Step 6: Locale Layout

app/[locale]/layout.tsx:

TSX
1import { dir } from 'i18next';
2import { languages } from '../i18n/settings';
3
4export async function generateStaticParams() {
5  return languages.map((locale) => ({ locale }));
6}
7
8export default function RootLayout({
9  children,
10  params: { locale },
11}: {
12  children: React.ReactNode;
13  params: { locale: string };
14}) {
15  return (
16    <html lang={locale} dir={dir(locale)}>
17      <body>{children}</body>
18    </html>
19  );
20}

Step 7: Server Component Page

app/[locale]/page.tsx:

TSX
1import { getTranslation } from '../i18n/server';
2import LanguageSwitcher from './components/LanguageSwitcher';
3
4export default async function Home({ params: { locale } }) {
5  const { t } = await getTranslation(locale);
6
7  return (
8    <main>
9      <h1>{t('title')}</h1>
10      <p>{t('description')}</p>
11      <p>{t('greeting', { name: 'World' })}</p>
12      <LanguageSwitcher locale={locale} />
13    </main>
14  );
15}

Step 8: Language Switcher

app/[locale]/components/LanguageSwitcher.tsx:

TSX
1'use client';
2
3import Link from 'next/link';
4import { usePathname } from 'next/navigation';
5import { languages } from '../../i18n/settings';
6
7export default function LanguageSwitcher({ locale }: { locale: string }) {
8  const pathname = usePathname();
9  
10  const redirectedPathname = (newLocale: string) => {
11    const segments = pathname.split('/');
12    segments[1] = newLocale;
13    return segments.join('/');
14  };
15
16  return (
17    <nav>
18      {languages.map((lng) => (
19        <Link
20          key={lng}
21          href={redirectedPathname(lng)}
22          style={{ fontWeight: locale === lng ? 'bold' : 'normal' }}
23        >
24          {lng}
25        </Link>
26      ))}
27    </nav>
28  );
29}

Step 9: Middleware

middleware.ts:

TypeScript
1import { NextRequest, NextResponse } from 'next/server';
2import acceptLanguage from 'accept-language';
3import { fallbackLng, languages } from './app/i18n/settings';
4
5acceptLanguage.languages(languages);
6
7export function middleware(req: NextRequest) {
8  const pathname = req.nextUrl.pathname;
9  
10  const pathnameHasLocale = languages.some(
11    (locale) => pathname.startsWith(`/\${locale}/`) || pathname === `/\${locale}`
12  );
13
14  if (!pathnameHasLocale) {
15    const lng = acceptLanguage.get(req.headers.get('Accept-Language')) || fallbackLng;
16    return NextResponse.redirect(new URL(`/\${lng}\${pathname}`, req.url));
17  }
18
19  return NextResponse.next();
20}
21
22export const config = {
23  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
24};

Best Practices

  1. Server vs Client Components - Use Server Components for static content, Client Components for interactivity
  2. TypeScript - Add type definitions for translation keys
  3. SEO - Use generateMetadata with translations for SEO

Scaling with IntlPull

Terminal
1npx @intlpullhq/cli init
2npx @intlpullhq/cli extract
3npx @intlpullhq/cli translate --all
4npx @intlpullhq/cli download --output ./app/locales

Tags
next.js
react-i18next
app-router
i18n
tutorial
server-components
2026
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.