Skip to content
Docs
Metadata & Route Handlers

Internationalization of Metadata & Route Handlers with the Next.js App Router

There are a few places in Next.js apps where you can apply internationalization outside of React components:

  1. Metadata API (opens in a new tab)
  2. Open Graph images (opens in a new tab)
  3. Manifest (opens in a new tab)
  4. Sitemap (opens in a new tab)
  5. Route Handlers (opens in a new tab)

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 (opens in a new tab) function that can be exported from pages and layouts.

layout.tsx
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.

Open Graph images

If you're programmatically generating Open Graph images (opens in a new tab), you can apply internationalization by calling functions from next-intl in the exported function:

opengraph-image.tsx
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 (opens in a new tab) 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:

app/manifest.ts
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 (opens in a new tab) 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 (opens in a new tab) 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.

If you're using shared pathnames, you can iterate over an array of pathnames that your app supports and generate a sitemap entry for each pathname.

Example:

import {MetadataRoute} from 'next';
 
// Can be imported from shared config
const defaultLocale = 'en' as const;
const locales = ['en', 'de'] as const;
 
// Adapt this as necessary
const pathnames = ['/', '/about'];
const host = 'https://acme.com';
 
export default function sitemap(): MetadataRoute.Sitemap {
  function getUrl(pathname: string, locale: string) {
    return `${host}/${locale}${pathname === '/' ? '' : pathname}`;
  }
 
  return pathnames.map((pathname) => ({
    url: getUrl(pathname, defaultLocale),
    lastModified: new Date(),
    alternates: {
      languages: Object.fromEntries(
        locales.map((locale) => [locale, getUrl(pathname, locale)])
      )
    }
  }));
}
💡

Note that your implementation may vary depending on your routing configuration (e.g. if you're using a localePrefix other than always or locale-specific domains).

Route Handlers

You can use next-intl in Route Handlers (opens in a new tab) too. The locale can either be received from a search param, a layout segment or by parsing the accept-language header of the request.

app/api/hello/route.tsx
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')});
}