Internationalization in Next.js with next-intl
Internationalization (i18n) is essential for providing a multi-language experience for global applications. next-intl
integrates well with Next.jsβ App Router, handling i18n routing, locale detection, and dynamic configuration. This guide will walk you through setting up i18n in Next.js using next-intl
for URL-based routing, user-specific settings, and domain-based locale routing.
Getting Started
First, create a Next.js app with the App Router and install next-intl
:
npm install next-intl
Next, configure next-intl
in the next.config.ts
file to provide a request-specific i18n configuration for Server Components:
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {
/* config options here */
};
export default withNextIntl(nextConfig);
Without i18n Routing
Setting up an app without i18n routing integration can be advantageous in scenarios where you want to provide a locale to next-intl
based on user-specific settings or when your app supports only a single language. This approach offers the simplest way to begin using next-intl
, as it requires no changes to your appβs structure, making it an ideal choice for straightforward implementations.
βββ translations
β βββ en.json
β βββ es.json
βββ next.config.ts
βββ src
βββ i18n
β βββ request.ts
βββ app
βββ layout.tsx
βββ page.tsx
Hereβs a quick explanation of each file's role:
translations/
: Stores different translations per language (e.g.,en.json
for English,es.json
for Spanish). Organize this as needed, e.g.,translations/en/common.json
.request.ts
: Manages locale-based configuration scoped to each request.
Setup request.ts for Request-Specific Configuration
Since we will be using features from next-intl
in Server Components, we need to add the following configuration in i18n/request.ts
:
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async () => {
const locale = 'en';
return {
locale,
messages: (await import(`../../translations/${locale}.json`)).default,
};
});
Here, we define a static locale and use that to determine which translation file to import. The imported JSON data is stored in the message variable, and is returned together with the locale so that we can access them from various components in the application.
Using Translation in RootLayout
Inside RootLayout
, we use getLocale()
to retrieve the static locale and set the document language for SEO and pass translations to NextIntlClientProvider
:
import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const locale = await getLocale();
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
</body>
</html>
);
}
Note that NextIntlClientProvider
automatically inherits configuration from i18n/request.ts
here, but messages must be explicitly passed.
Now you can use translations and other functionality from next-intl
in your components:
import { useTranslations } from 'next-intl';
import { Link } from '@/i18n/routing';
export default function HomePage() {
const t = useTranslations('HomePage');
return (
<div>
<h1>{t('title')}</h1>
<Link href="/about">{t('about')}</Link>
</div>
);
}
In case of async components, you can use the awaitable getTranslations
function instead:
import { getTranslations } from 'next-intl/server';
import { Link } from '@/i18n/routing';
export default async function HomePage() {
const t = await getTranslations('HomePage');
return (
<div>
<h1>{t('title')}</h1>
<Link href="/about">{t('about')}</Link>
</div>
);
}
And with that, you have i18n configured and working on your application!
Now, letβs take it a step further by introducing routing. \
With i18n Routing
To set up i18n routing, we need a file structure that separates each language configuration and translation file. Below is the recommended structure:
βββ translations
β βββ en.json
β βββ es.json
βββ next.config.ts
βββ src
βββ i18n
β βββ routing.ts
β βββ request.ts
βββ middleware.ts
βββ app
βββ [locale]
βββ layout.tsx
βββ page.tsx
We updated the earlier structure to include some files that we require for routing:
routing.ts
: Sets up locales, default language, and routing, shared between middleware and navigation.middleware.ts
: Handles URL rewrites and locale negotiation.app/[locale]/
: Creates dynamic routes for each locale like/en/about
and/es/about
.
Define Routing Configuration in i18n/routing.ts
The routing.ts
file configures supported locales and the default locale, which is referenced by middleware.ts
and other navigation functions:
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';
export const routing = defineRouting({
locales: ['en', 'es'], // Supported locales
defaultLocale: 'en', // Fallback locale if none matches
});
// Provides wrappers for Next.js navigation APIs to handle locale routing
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
This configuration lets Next.js handle URL paths like /about
, with locale management managed by next-intl
.
Update request.ts for Request-Specific Configuration
We need to update the getRequestConfig
function from the above implementation in i18n/request.ts
.
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../translations/${locale}.json`)).default,
};
});
Here, request.ts
ensures that each request loads the correct translation files based on the userβs locale or falls back to the default.
Setup Middleware for Locale Matching
The middleware.ts
file matches the locale
based on the request:
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/', '/(es|en)/:path*'], // Matches i18n paths only
};
Middleware handles locale matches and redirects to localized paths like /en
or /es
.
Updating the RootLayout
file
Inside RootLayout
, we use the locale from params (matched by middleware) instead of calling getLocale()
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import '../globals.css';
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const { locale } = params;
if (!routing.locales.includes(locale as any)) {
notFound();
}
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
</body>
</html>
);
}
The locale
we get from the params was matched in the middleware.ts
file and we use that here to set the document language for SEO purposes. Additionally, we used this file to pass configuration from i18n/request.ts
to Client Components through NextIntlClientProvider
.
Note: When using the above setup with i18n routing, next-intl
will currently opt into dynamic rendering when APIs like useTranslations
are used in Server Components. next-intl
provides a temporary API that can be used to enable static rendering.
Static Rendering for i18n Routes
For apps with dynamic routes, use generateStaticParams
to pass all possible locale values, allowing Next.js to render at build time:
import { routing } from '@/i18n/routing';
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
next-intl
provides an API setRequestLocale
that can be used to distribute the locale that is received via params in layouts and pages for usage in all Server Components that are rendered as part of the request. You need to call this function in every layout/page that you intend to enable static rendering for since Next.js can render layouts and pages independently.
import { setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
export default async function RootLayout({ children, params: { locale } }) {
if (!routing.locales.includes(locale as any)) {
notFound();
}
setRequestLocale(locale);
return (
// ...
);
}
Note: Call setRequestLocale
before invoking useTranslations
or getMessages
or any next-intl
functions.
Domain Routing
For domain-specific locale support, use the domains
setting to map domains to locales, such as us.example.com/en
or ca.example.com/fr
.
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'fr'],
defaultLocale: 'en',
domains: [
{ domain: 'us.example.com', defaultLocale: 'en', locales: ['en'] },
{ domain: 'ca.example.com', defaultLocale: 'en' },
],
});
This setup allows you to serve localized content based on domains. Read more on domain routing here.
Conclusion
Setting up internationalization in Next.js with next-intl
provides a modular way to handle URL-based routing, user-defined locales, and domain-specific configurations. Whether you need URL-based routing or a straightforward single-locale setup, next-intl
adapts to fit diverse i18n needs.
With these tools, your app will be ready to deliver a seamless multi-language experience to users worldwide.