- Published on
Implement i18n Configuration for Next.js Project
What is I18n
Internationalization (i18n) is the process of designing and developing a software application so that it can be adapted to various languages and regions without requiring engineering changes to the source code. In the context of web development, i18n allows applications to support multiple languages, making them accessible to a broader audience.
Setting up Next.js for I18n
Prerequisites
- Next.js => React framework for production.
- Next-intl => support for internationalization in Next.js applications.
- I18n Ally (lokalise.i18n-ally)
Install Next-intl
pnpm install next-intl
src/
├── app/
│ └── [locale]/
│ ├── layout.tsx
│ └── page.tsx
├── i18n/
│ ├── routing.ts
│ ├── request.ts
│ └── navigation.ts
├── messages/
│ ├── en.json
│ └── vi.json
├── components/
│ └── LanguageSwitcher.tsx
└── middleware.ts
Create Translation Files
messages/en.json
{
"common": {
"welcome": "Welcome",
"description": "This is a multilingual Next.js application",
"language": "Language",
"switchLanguage": "Switch Language"
},
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"home": {
"title": "Home Page",
"greeting": "Hello, {name}!"
}
}
messages/vi.json
{
"common": {
"welcome": "Chào mừng",
"description": "Đây là ứng dụng Next.js đa ngôn ngữ",
"language": "Ngôn ngữ",
"switchLanguage": "Chuyển ngôn ngữ"
},
"nav": {
"home": "Trang chủ",
"about": "Giới thiệu",
"contact": "Liên hệ"
},
"home": {
"title": "Trang Chủ",
"greeting": "Xin chào, {name}!"
}
}
Configure routing
src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing'
export const routing = defineRouting({
locales: ['en', 'vi'],
defaultLocale: 'en', // English as default
localePrefix: 'as-needed', // Only show prefix for non-default locales
})
export type Locale = (typeof routing.locales)[number]
src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation'
import { routing } from './routing'
export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing)
src/i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
import { routing } from './routing'
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale
// Validate that the incoming locale is valid
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
}
})
Setup middleware
src/middleware.ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'
export default createMiddleware(routing)
export const config = {
// Match all pathnames except for
// - API routes
// - Static files
// - Internal Next.js paths
matcher: [
'/((?!api|_next|_vercel|.*\\..*).*)',
// Also match root
'/',
],
}
Update Next.js Config
next.config.ts
import type { NextConfig } from 'next'
import createNextIntlPlugin from 'next-intl/plugin'
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
const nextConfig: NextConfig = {
// Your other config options
}
export default withNextIntl(nextConfig)
Setup Default Route to English
The configuration above already sets English as default with localePrefix: 'as-needed':
/ → English (no prefix needed) /vi → Vietnamese /about → English about page /vi/about → Vietnamese about page
App Layout with Locale src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
type Props = {
children: React.ReactNode;
params: Promise<{ locale: string }>;
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }: Props) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'common' });
return {
title: t('welcome'),
description: t('description')
};
}
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params;
// Validate locale
if (!routing.locales.includes(locale as any)) {
notFound();
}
// Get messages for client components
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
src/app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import LanguageSwitcher from '@/components/LanguageSwitcher';
export default function HomePage() {
const t = useTranslations('home');
const tCommon = useTranslations('common');
return (
<main className="min-h-screen p-8">
<div className="max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">{t('title')}</h1>
<LanguageSwitcher />
</div>
<p className="text-lg mb-4">{tCommon('welcome')}</p>
<p className="text-gray-600">{t('greeting', { name: 'zzz' })}</p>
</div>
</main>
);
}
Language Switcher Component
src/components/LanguageSwitcher.tsx
'use client';
import { useLocale, useTranslations } from 'next-intl';
import { useRouter, usePathname } from '@/i18n/navigation';
import { routing, type Locale } from '@/i18n/routing';
import { useTransition } from 'react';
const languageNames: Record<Locale, string> = {
en: 'English',
vi: 'Tiếng Việt',
};
const languageFlags: Record<Locale, string> = {
en: '🇺🇸',
vi: '🇻🇳',
};
export default function LanguageSwitcher() {
const t = useTranslations('common');
const locale = useLocale() as Locale;
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();
const handleLocaleChange = (newLocale: Locale) => {
startTransition(() => {
router.replace(pathname, { locale: newLocale });
});
};
return (
<div className="relative inline-block">
<label className="sr-only">{t('switchLanguage')}</label>
<select
value={locale}
onChange={(e) => handleLocaleChange(e.target.value as Locale)}
disabled={isPending}
className="appearance-none bg-white border border-gray-300 rounded-lg px-4 py-2 pr-8 cursor-pointer hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
>
{routing.locales.map((loc) => (
<option key={loc} value={loc}>
{languageFlags[loc]} {languageNames[loc]}
</option>
))}
</select>
{isPending && (
<span className="absolute right-2 top-1/2 -translate-y-1/2">
<svg
className="animate-spin h-4 w-4 text-gray-500"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
</span>
)}
</div>
);
}
Hook custom
src/hooks/useLanguage.ts
'use client'
import { useLocale } from 'next-intl'
import { useRouter, usePathname } from '@/i18n/navigation'
import { routing, type Locale } from '@/i18n/routing'
import { useTransition, useCallback } from 'react'
export function useLanguage() {
const locale = useLocale() as Locale
const router = useRouter()
const pathname = usePathname()
const [isPending, startTransition] = useTransition()
const setLocale = useCallback(
(newLocale: Locale) => {
if (newLocale === locale) return
if (!routing.locales.includes(newLocale)) {
console.warn(`Invalid locale: ${newLocale}`)
return
}
startTransition(() => {
router.replace(pathname, { locale: newLocale })
})
},
[locale, pathname, router]
)
const toggleLocale = useCallback(() => {
const currentIndex = routing.locales.indexOf(locale)
const nextIndex = (currentIndex + 1) % routing.locales.length
setLocale(routing.locales[nextIndex])
}, [locale, setLocale])
return {
locale,
locales: routing.locales,
defaultLocale: routing.defaultLocale,
setLocale,
toggleLocale,
isPending,
}
}
Conclusion
By following this guide, you have successfully implemented internationalization (i18n) in your Next.js project using the next-intl library. Your application can now support multiple languages, allowing users to switch between them seamlessly. This setup enhances the user experience and broadens the reach of your application to a global audience.