Setting up i18n (Internationalization) in a Next.js application using the App Router (as opposed to the Pages Router) allows you to offer a multi-language experience to your users. Here’s a step-by-step guide on how to configure i18n in your Next.js app.
Step 1: Install Necessary Packages
To get started, install the following packages:
npm i @formatjs/intl-localematcher negotiator @types/negotiator
These packages help with locale negotiation, allowing the app to detect and use the user’s preferred language settings.
Step 2: Set Up Dynamic Route for Language
- Create a
[lang]
Directory: To handle dynamic routing by language, create a new directory named[lang]
within yourapp
directory. - Move Components to
[lang]
: Move all components and pages in your app intoapp/[lang]
. This change enables dynamic language routing, wherelang
can represent language codes likeen
,ko
, orjp
.
Step 3: Create Middleware for Language Detection
Within the app
directory, create a middleware.ts
file. This file will detect the user's language preference and redirect them to the appropriate language version of your app.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
const LOCALES = ['en', 'ko', 'jp'];
const DEFAULT_LOCALE = 'jp';
function getLocale() {
const headers = { 'accept-language': 'en-US,en;q=0.5' };
const languages = new Negotiator({ headers }).languages();
return match(languages, LOCALES, DEFAULT_LOCALE);
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const pathnameHasLocale = LOCALES.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return;
const locale = getLocale();
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
- Explanation: This middleware checks if the current URL contains a locale prefix (like
/en
,/ko
, or/jp
). If not, it redirects to a URL with the appropriate language prefix based on the user's language preference. - Note:
matcher
specifies routes where the middleware should apply, excluding routes like API and static files.
Step 4: Set Up Dictionaries for Each Language
Next, create a dictionaries.ts
file in a directory like app/lib/utils
to load the translation files dynamically.
// dictionaries.ts
import 'server-only';
type Locale = 'en' | 'ko' | 'jp';
const dictionaries = {
en: () => import('../../components/dictionaries/en.json').then((module) => module.default),
ko: () => import('../../components/dictionaries/ko.json').then((module) => module.default),
jp: () => import('../../components/dictionaries/jp.json').then((module) => module.default),
};
export const getDictionary = async (locale: Locale) => dictionaries[locale]();
- Explanation: This file defines the available dictionaries for each locale and provides a
getDictionary
function that loads the appropriate dictionary based on the language code.
Step 5: Create JSON Translation Files
Create JSON files for each language. These files should be in the components/dictionaries
folder, as referenced in dictionaries.ts
.
Example of en.json
(English):
{
"home": {
"title": "Welcome"
}
}
Example of ko.json
(Korean):
{
"home": {
"title": "์๋
ํ์ธ์"
}
}
- Note: Structure the JSON according to your app’s needs. Each language should have its own file with corresponding translations.
Step 6: Add Metadata for Language-Specific Pages
In root layout.tsx
, add metadata to define language settings based on the route parameters.
// root layout.tsx
export async function generateStaticParams() {
return [{ lang: 'en' }, { lang: 'ko' }, { lang: 'jp' }];
}
export default function Root({ children, params }) {
return (
<html lang={params.lang}>
<body>{children}</body>
</html>
);
}
This setup allows each page to be statically generated for each language.
Step 7: Access Translations in Your Pages
To use translations in a page, retrieve the dictionary using the getDictionary
function.
// [lang]/page.tsx
import { getDictionary } from './dictionaries';
export default async function Page({ params: { lang } }) {
const dict = await getDictionary(lang);
return <h1>{dict.home.title}</h1>; // Renders "Welcome" or "์๋
ํ์ธ์" based on the language
}
In this example, the title
for home
is displayed based on the language specified in lang
.
Step 8: Add a Language Selector Component
To allow users to switch languages, create a LanguageMenu
component.
// components/LanguageMenu.tsx
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
export const LanguageMenu = () => {
const pathname = usePathname();
const cleanedPathname = pathname.replace(/^\/(en|ko|jp)/, '');
return (
<div className="space-x-2">
<Link href={`/jp${cleanedPathname}`} locale="jp">
Japanese
</Link>
<span>|</span>
<Link href={`/ko${cleanedPathname}`} locale="ko">
Korean
</Link>
<span>|</span>
<Link href={`/en${cleanedPathname}`} locale="en">
English
</Link>
</div>
);
};
Place the LanguageMenu
component in your layout or navigation bar so users can switch between languages easily.
Wrapping Up
By following these steps, you can implement multilingual support in your Next.js app using the App Router. This setup leverages dynamic routing and middleware to detect user language preferences and redirects them accordingly. Additionally, with JSON-based dictionaries and an intuitive language selector, you can easily expand support for more languages in the future.
This solution ensures a seamless user experience for international audiences and allows your application to grow with ease. Happy coding! ๐