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
Terminalnpm 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:
JSON1{ 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:
TypeScript1export 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:
TypeScript1import { 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:
TSX1'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:
TSX1import { 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:
TSX1import { 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:
TSX1'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:
TypeScript1import { 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
- Server vs Client Components - Use Server Components for static content, Client Components for interactivity
- TypeScript - Add type definitions for translation keys
- SEO - Use
generateMetadatawith translations for SEO
Scaling with IntlPull
Terminal1npx @intlpullhq/cli init 2npx @intlpullhq/cli extract 3npx @intlpullhq/cli translate --all 4npx @intlpullhq/cli download --output ./app/locales
