Server Actions, Metadata & Route Handlers
There are a few places in Next.js apps where you can apply internationalization outside of React components:
next-intl/server
provides a set of awaitable functions that can be used in these cases.
Metadata API
To internationalize metadata like the page title, you can use functionality from next-intl
in the generateMetadata
function that can be exported from pages and layouts.
import {getTranslations} from 'next-intl/server';
export async function generateMetadata({params: {locale}}) {
const t = await getTranslations({locale, namespace: 'Metadata'});
return {
title: t('title')
};
}
By passing an explicit locale
to the awaitable functions from next-intl
,
you can make the metadata handler eligible for static
rendering
if you’re using i18n routing.
Server Actions
Server Actions provide a mechanism to execute server-side code that is invoked by the client. In case you’re returning user-facing messages, you can use next-intl
to localize them based on the user’s locale.
import {getTranslations} from 'next-intl/server';
async function loginAction(data: FormData) {
'use server';
const t = await getTranslations('LoginForm');
const areCredentialsValid = /* ... */;
if (!areCredentialsValid) {
return {error: t('invalidCredentials')};
}
}
Note that when you’re displaying messages generated in Server Actions to the user, you should consider the case if the user can switch the locale while the message is displayed to ensure that the UI is localized consistently. If you’re using a [locale]
segment as part of your routing strategy then this is handled automatically. If you’re not, you might want to clear the message manually, e.g. by resetting the state of the respective component via key={locale}
.
When using Zod for validation, how can I localize error messages?
Zod allows you to provide contextual error maps that can be used to customize error messages per parse invocation. Since the locale is specific to a particular request, this mechanism comes in handy to turn structured errors from zod
into localized messages:
import {getTranslations} from 'next-intl/server';
import {loginUser} from '@/services/session';
import {isEqual} from 'lodash';
import {z} from 'zod';
const loginFormSchema = z.object({
email: z.string().email(),
password: z.string().min(1)
});
// ...
async function loginAction(data: FormData) {
'use server';
const t = await getTranslations('LoginForm');
const values = Object.fromEntries(data);
const result = await loginFormSchema
.refine(async (credentials) => loginUser(credentials), {
message: t('invalidCredentials')
})
.safeParseAsync(values, {
errorMap(issue, ctx) {
let message;
if (isEqual(issue.path, ['email'])) {
message = t('invalidEmail');
} else if (isEqual(issue.path, ['password'])) {
message = t('invalidPassword');
}
return {message: message || ctx.defaultError};
}
});
// ...
}
See the App Router without i18n routing example for a working implementation.
Open Graph images
If you’re programmatically generating Open Graph images, you can apply internationalization by calling functions from next-intl
in the exported function:
import {ImageResponse} from 'next/og';
import {getTranslations} from 'next-intl/server';
export default async function OpenGraphImage({params: {locale}}) {
const t = await getTranslations({locale, namespace: 'OpenGraphImage'});
return new ImageResponse(<div style={{fontSize: 128}}>{t('title')}</div>);
}
Manifest
Since the manifest file needs to be placed in the root of the app
folder (outside the [locale]
dynamic segment), you need to provide a locale explicitly since next-intl
can’t infer it from the pathname:
import {MetadataRoute} from 'next';
import {getTranslations} from 'next-intl/server';
export default async function manifest(): Promise<MetadataRoute.Manifest> {
// Pick a locale that is representative of the app
const locale = 'en';
const t = await getTranslations({
namespace: 'Manifest',
locale
});
return {
name: t('name'),
start_url: '/',
theme_color: '#101E33'
};
}
Sitemap
If you’re using a sitemap to inform search engines about all pages of your site, you can attach locale-specific alternate entries to every URL in the sitemap to indicate that a particular page is available in multiple languages or regions.
Note that by default, next-intl
returns the link
response header to instruct search engines that a page is available in multiple languages. While this sufficiently links localized pages for search engines, you may choose to provide this information in a sitemap in case you have more specific requirements.
Next.js supports providing alternate URLs per language via the alternates
entry as of version 14.2. You can use your default locale for the main URL and provide alternate URLs based on all locales that your app supports. Keep in mind that also the default locale should be included in the alternates
object.
import {MetadataRoute} from 'next';
import {routing, getPathname} from '@/i18n/routing';
// Adapt this as necessary
const host = 'https://acme.com';
export default function sitemap(): MetadataRoute.Sitemap {
// Adapt this as necessary
return [getEntry('/'), getEntry('/users')];
}
type Href = Parameters<typeof getPathname>[0]['href'];
function getEntry(href: Href) {
return {
url: getUrl(href, routing.defaultLocale),
alternates: {
languages: Object.fromEntries(
routing.locales.map((locale) => [locale, getUrl(href, locale)])
)
}
};
}
function getUrl(href: Href, locale: (typeof routing.locales)[number]) {
const pathname = getPathname({locale, href});
return host + pathname;
}
Depending on if you’re using the pathnames
setting, dynamic params can either be passed as:
// 1. A final string (when not using `pathnames`)
getEntry('/users/1');
// 2. An object (when using `pathnames`)
getEntry({
pathname: '/users/[id]',
params: {id: '1'}
});
Route Handlers
You can use next-intl
in Route Handlers too. The locale
can either be received from a search param, a layout segment or by parsing the accept-language
header of the request.
import {NextResponse} from 'next/server';
import {getTranslations} from 'next-intl/server';
export async function GET(request) {
// Example: Receive the `locale` via a search param
const {searchParams} = new URL(request.url);
const locale = searchParams.get('locale');
const t = await getTranslations({locale, namespace: 'Hello'});
return NextResponse.json({title: t('title')});
}