Skip to main content

How to Set Up Internationalization (i18n) on Next.js with the App Router

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

  1. Create a [lang] Directory: To handle dynamic routing by language, create a new directory named [lang] within your app directory.
  2. Move Components to [lang]: Move all components and pages in your app into app/[lang]. This change enables dynamic language routing, where lang can represent language codes like en, ko, or jp.

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! ๐ŸŒ

    Popular posts from this blog

    Xcode and iOS Version Mismatch: Troubleshooting "Incompatible Build Number" Errors

    Have you ever encountered a frustrating error while trying to run your iOS app in Xcode, leaving you scratching your head? A common issue arises when your device's iOS version is too new for the Xcode version you're using. This often manifests as an "incompatible build number" error, and looks like this: DVTDeviceOperation: Encountered a build number "" that is incompatible with DVTBuildVersion. This usually happens when you are testing with beta versions of either iOS or Xcode, and can prevent Xcode from properly compiling your storyboards. Let's explore why this occurs and what you can do to resolve it. Why This Error Occurs The core problem lies in the mismatch between the iOS version on your test device and the Software Development Kit (SDK) supported by your Xcode installation. Xcode uses the SDK to understand how to build and run apps for specific iOS versions. When your device runs a newer iOS version than Xcode anticipates, Xcode mi...

    How to Fix the “Invariant Violation: TurboModuleRegistry.getEnforcing(…): ‘RNCWebView’ Could Not Be Found” Error in React Native

    When working with React Native, especially when integrating additional libraries like react-native-signature-canvas , encountering errors can be frustrating. One such error is: Invariant Violation: TurboModuleRegistry. getEnforcing (...): 'RNCWebView' could not be found This error often occurs when the necessary dependencies for a module are not properly linked or when the environment you’re using doesn’t support the required native modules. Here’s a breakdown of how I encountered and resolved this issue. The Problem I was working on a React Native project where I needed to add the react-native-signature-canvas library to capture user signatures. The installation process seemed straightforward: Installed the package: npm install react-native-signature- canvas 2. Since react-native-signature-canvas depends on react-native-webview , I also installed the WebView package: npm install react- native -webview 3. I navigated to the iOS directory and ran: cd ios pod install Everythi...

    Fixing FirebaseMessagingError: Requested entity was not found.

    If you’re working with Firebase Cloud Messaging (FCM) and encounter the error: FirebaseMessagingError: Requested entity was not found. with the error code: messaging/registration-token-not-registered this means that the FCM registration token is invalid, expired, or unregistered . This issue can prevent push notifications from being delivered to users. ๐Ÿ” Possible Causes & Solutions 1️⃣ Invalid or Expired FCM Token FCM tokens are not permanent and may expire over time. If you’re storing tokens in your database, some might be outdated. ✅ Solution: Remove invalid tokens from your database when sending push notifications. Refresh and store the latest FCM token when the app starts. Example: Automatically Refresh Token firebase. messaging (). onTokenRefresh ( ( newToken ) => { // Send newToken to your backend and update the stored token }); 2️⃣ Token Unregistered on Client Device A token might become unregistered if: The app is uninstalled on the user’s device. ...