Adding multi-language support to a Next.js application is straightforward with next-intl. It provides a complete solution for both server and client components, with built-in support for ICU message formatting, pluralization, and date/number formatting.
Setting Up Routing
The first step is defining your supported locales and the default locale. next-intl provides a defineRouting helper that centralizes this configuration:
// lib/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'fr', 'de', 'es'],
defaultLocale: 'en',
});Middleware Configuration
The middleware handles locale detection from the URL, browser preferences, or cookies, and redirects users accordingly:
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './lib/i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/', '/(en|fr|de|es)/:path*'],
};Translation Files
Translations are stored as JSON files organized by locale. next-intl supports ICU message syntax, which enables powerful formatting features like pluralization:
// locales/en/common.json
{
"greeting": "Hello, {name}!",
"items_count": "You have {count, plural, =0 {no items} one {# item} other {# items}}"
}
// In a component:
import { useTranslations } from 'next-intl';
export default function Dashboard() {
const t = useTranslations();
return (
<div>
<h1>{t('greeting', { name: 'Alex' })}</h1>
<p>{t('items_count', { count: 5 })}</p>
</div>
);
}Server vs Client Components
next-intl works seamlessly with both server and client components. Client components use the useTranslations hook, while server components use getTranslations.
Server Component Usage
In server components, you use the async getTranslations function, which is perfect for generating metadata:
// Server component — access translations without hooks
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ params }) {
const { locale } = await params;
const t = await getTranslations({ locale });
return { title: t('page_title') };
}Best Practices
Keep your translation keys organized by feature or page. Use ICU message syntax for complex strings with variables, plurals, or gender. Always add translations for all supported locales at the same time to avoid missing translations in production.