Next.js internationalized routing
Routing APIs are only needed when you’re using i18n routing.
next-intl
integrates with the routing system of Next.js in two places:
- Middleware: Negotiates the locale and handles redirects & rewrites (e.g.
/
→/en
) - Navigation APIs: Lightweight wrappers around Next.js’ navigation APIs like
<Link />
This enables you to express your app in terms of APIs like <Link href="/about">
, while aspects like the locale and user-facing pathnames are automatically handled behind the scenes (e.g. /de/ueber-uns
).
Define routing
The routing configuration that is shared between the middleware and the navigation APIs can be defined with the defineRouting
function.
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// A list of all locales that are supported
locales: ['en', 'de'],
// Used when no locale matches
defaultLocale: 'en'
});
Depending on your routing needs, you may wish to consider further settings—see below.
What if the locales aren’t known at build time?
In case you’re building an app where locales can be added and removed at runtime, you can provide the routing configuration for the middleware dynamically per request.
To create the corresponding navigation APIs, you can omit the locales
argument from createNavigation
in this case.
Still, in case you’re defining other routing config, make sure to keep them in sync between the middleware and the navigation APIs.
Locale prefix
By default, the pathnames of your app will be available under a prefix that matches your directory structure (e.g. /en/about
→ app/[locale]/about/page.tsx
). You can however adapt the routing to optionally remove the prefix or customize it per locale by configuring the localePrefix
setting.
Always use a locale prefix (default)
By default, pathnames always start with the locale (e.g. /en/about
).
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localePrefix: 'always'
});
How can I redirect unprefixed pathnames?
If you want to redirect unprefixed pathnames like /about
to a prefixed alternative like /en/about
, you can adjust your middleware matcher to match unprefixed pathnames too.
Don’t use a locale prefix for the default locale
If you want to use no prefix for the default locale (e.g. /about
), you can configure your routing accordingly:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localePrefix: 'as-needed'
});
Important: For this routing strategy to work as expected, you should additionally adapt your middleware matcher to detect unprefixed pathnames.
Note that if a superfluous locale prefix like /en/about
is requested, the middleware will automatically redirect to the unprefixed version /about
. This can be helpful in case you’re redirecting from another locale and you want to update a potential cookie value first (e.g. <Link />
relies on this mechanism).
Never use a locale prefix
If you’d like to provide a locale to next-intl
, e.g. based on user settings, you can consider setting up next-intl
without i18n routing. This way, you don’t need to use the routing integration in the first place.
However, you can also configure the middleware to never show a locale prefix in the URL, which can be helpful in the following cases:
- You want to use domain-based routing and have only one locale per domain
- You want to use a cookie to determine the locale while enabling static rendering
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localePrefix: 'never'
});
In this case, requests for all locales will be rewritten to have the locale only prefixed internally. You still need to place all your pages inside a [locale]
folder for the routes to be able to receive the locale
param.
Note that:
- If you use this strategy, you should adapt your matcher to detect unprefixed pathnames.
- If you don’t use domain-based routing, the cookie is now the source of truth for determining the locale. Make sure that your hosting solution reliably returns the
set-cookie
header from the middleware (e.g. Vercel and Cloudflare are known to potentially strip this header for cacheable requests). - Alternate links are disabled in this mode since URLs might not be unique per locale. Due to this, consider including these yourself, or set up a sitemap that links localized pages via
alternates
.
Custom prefixes
If you’d like to customize the user-facing prefix, you can provide a locale-based mapping:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en-US', 'de-AT', 'zh'],
defaultLocale: 'en-US',
localePrefix: {
mode: 'always',
prefixes: {
'en-US': '/us',
'de-AT': '/eu/at'
// (/zh will be used as-is)
}
}
});
Note that:
- You should adapt your middleware matcher to match the custom prefixes.
- Custom prefixes are only visible to the user and rewritten internally to the corresponding locale. Therefore, the
[locale]
segment corresponds to the locale, not the prefix.
Can I read the matched prefix in my app?
Since the custom prefix is rewritten to the locale internally, you can’t access the prefix directly. However, you can extract details like the region from the locale:
import {useLocale} from 'next-intl';
function Component() {
// Assuming the locale is 'en-US'
const locale = useLocale();
// Extracts the "US" region
const {region} = new Intl.Locale(locale);
}
The region must be a valid ISO 3166-1 alpha-2 country code or a UN M49 region code. When passed to Intl.Locale
, the region code is treated as case-insensitive and normalized to uppercase. You can also combine languages with regions where the language is not natively spoken (e.g. en-AT
describes English as used in Austria).
Apart from the region, a locale can encode further properties, like the numbering system.
If you’d like to encode custom information in the locale, you can use arbitrary private extensions, denoted by the -x-
prefix (e.g. en-US-x-usd
). The Intl.Locale
constructor ignores private extensions, but you can extract them from the locale string manually.
Localized pathnames
Many apps choose to localize pathnames, especially when search engine optimization is relevant, e.g.:
/en/about
/de/ueber-uns
Since you typically want to define these routes only once internally, you can use the next-intl
middleware to rewrite such incoming requests to shared pathnames.
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'de'],
defaultLocale: 'en',
// The `pathnames` object holds pairs of internal and
// external paths. Based on the locale, the external
// paths are rewritten to the shared, internal ones.
pathnames: {
// If all locales use the same pathname, a single
// external path can be used for all locales
'/': '/',
'/blog': '/blog',
// If locales use different paths, you can
// specify each external path per locale
'/about': {
en: '/about',
de: '/ueber-uns'
},
// Dynamic params are supported via square brackets
'/news/[articleSlug]-[articleId]': {
en: '/news/[articleSlug]-[articleId]',
de: '/neuigkeiten/[articleSlug]-[articleId]'
},
// Static pathnames that overlap with dynamic segments
// will be prioritized over the dynamic segment
'/news/just-in': {
en: '/news/just-in',
de: '/neuigkeiten/aktuell'
},
// Also (optional) catch-all segments are supported
'/categories/[...slug]': {
en: '/categories/[...slug]',
de: '/kategorien/[...slug]'
}
}
});
Localized pathnames map to a single internal pathname that is created via the file-system based routing in Next.js. In the example above, /de/ueber-uns
will be handled by the page at /[locale]/about/page.tsx
.
How can I revalidate localized pathnames?
Depending on if a route is generated statically (at build time) or dynamically (at runtime), revalidatePath
needs to be called either for the localized or the internal pathname.
Consider this example:
app
└── [locale]
└── news
└── [slug]
… with this routing configuration:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'de'],
defaultLocale: 'en',
pathnames: {
'/news/[slug]': {
en: '/news/[slug]',
de: '/neuigkeiten/[slug]'
}
}
});
Depending on whether some-article
was included in generateStaticParams
or not, you can revalidate the route like this:
// Statically generated at build time
revalidatePath('/de/news/some-article');
// Dynamically generated at runtime:
revalidatePath('/de/neuigkeiten/some-article');
When in doubt, you can revalidate both paths to be on the safe side.
See also: vercel/next.js#59825
How can I localize dynamic segments?
If you have a route like /news/[articleSlug]-[articleId]
, you may want to localize the articleSlug
part in the pathname like this:
/en/news/launch-of-new-product-94812
/de/neuigkeiten/produktneuheit-94812
In this case, the localized slug can either be provided by the backend or generated in the frontend by slugifying the localized article title.
A good practice is to include the ID in the URL, allowing you to retrieve the article based on this information from the backend. The ID can be further used to implement self-healing URLs, where a redirect is added if the articleSlug
doesn’t match.
If you localize the values for dynamic segments, you might want to turn off alternate links and provide your own implementation that considers localized values for dynamic segments.
How do I integrate with an external system like a CMS that provides localized pathnames?
In case you’re using a system like a CMS to configure localized pathnames, you’ll typically implement this with a dynamic segment that catches all localized pathnames instead of using the pathnames
configuration from next-intl
.
Examples:
- All pathnames are handled by your CMS:
[locale]/[[...slug]]/page.tsx
- Some pathnames are handled by your CMS:
[locale]/blog/[...slug]/page.tsx
import {notFound} from 'next';
import {fetchContent} from './cms';
type Props = {
params: {
locale: string;
slug: Array<string>;
};
};
export default async function CatchAllPage({params}: Props) {
const content = await fetchContent(params.locale, params.slug);
if (!content) notFound();
// ...
}
In this case, you’ll likely want to disable alternate links and provide your own implementation instead.
Furthermore, in case you provide a locale switcher, it might require special care to be able to switch between localized pathnames of the same page. A simplified implementation might always redirect to the home page instead.
Domains
If you want to serve your localized content based on different domains, you can provide a list of mappings between domains and locales via the domains
setting.
Examples:
us.example.com/en
ca.example.com/en
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',
// Optionally restrict the locales available on this domain
locales: ['en']
},
{
domain: 'ca.example.com',
defaultLocale: 'en'
// If there are no `locales` specified on a domain,
// all available locales will be supported here
}
]
});
Note that:
- You can optionally remove the locale prefix in pathnames by changing the
localePrefix
setting. E.g.localePrefix: 'never'
can be helpful in case you have unique domains per locale. - If no domain matches, the middleware will fall back to the
defaultLocale
(e.g. onlocalhost
).
How can I locally test if my setup is working?
To test your domain setup locally, you can conditionally adapt the domains to refer to hosts that are available locally:
import {defineRouting} from 'next-intl/routing';
const isDev = process.env.NODE_ENV === 'development';
export const routing = defineConfig({
// ...
domains: [
{
domain: isDev ? 'localhost:3000' : 'us.example.com'
// ...
},
{
domain: isDev ? 'localhost:3001' : 'ca.example.com'
// ...
}
]
});
Now, you can run your development server on one of the configured ports and test the routing for different use cases:
# Like `us.example.com`
PORT=3000 npm run dev
# Like `ca.example.com`
PORT=3001 npm run dev
Can I use a different localePrefix
setting per domain?
Since such a configuration would require reading the domain at runtime, this would prevent the ability to render pages statically. Due to this, next-intl
doesn’t support this configuration out of the box.
However, you can still achieve this by building the app for each domain separately, while injecting diverging routing configuration via an environment variable.
Example:
import {defineRouting} from 'next-intl/routing';
const isUsDomain =
process.env.VERCEL_PROJECT_PRODUCTION_URL === 'us.example.com';
export const routing = defineRouting({
locales: isUsDomain ? ['en'] : ['en', 'fr'],
defaultLocale: 'en',
localePrefix: isUsDomain ? 'never' : 'always'
});
Special case: Using domains
with localePrefix: 'as-needed'
Since domains can have different default locales, this combination requires some tradeoffs that apply to the navigation APIs in order for next-intl
to avoid reading the current host on the server side (which would prevent the usage of static rendering).
<Link />
: This component will always render a locale prefix on the server side, even for the default locale of a given domain. However, during hydration on the client side, the prefix is potentially removed, if the default locale of the current domain is used. Note that the temporarily prefixed pathname will always be valid, however the middleware will potentially clean up a superfluous prefix via a redirect if the user clicks on a link before hydration.redirect
: When calling this function, a locale prefix is always added, regardless of the provided locale. However, similar to the handling with<Link />
, the middleware will potentially clean up a superfluous prefix.getPathname
: This function requires that adomain
is passed as part of the arguments in order to avoid ambiguity. This can either be provided statically (e.g. when used in a sitemap), or read from a header likex-forwarded-host
.
import {getPathname} from '@/i18n/routing';
import {headers} from 'next/headers';
// Case 1: Statically known domain
const domain = 'ca.example.com';
// Case 2: Read at runtime (dynamic rendering)
const domain = headers().get('x-forwarded-host');
// Assuming the current domain is `ca.example.com`,
// the returned pathname will be `/about`
const pathname = getPathname({
href: '/about',
locale: 'en',
domain
});
A domain
can optionally also be passed to redirect
in the same manner to ensure that a prefix is only added when necessary. Alternatively, you can also consider redirecting in the middleware or via useRouter
on the client side.
If you need to avoid these tradeoffs, you can consider building the same app for each domain separately, while injecting diverging routing configuration via an environment variable.
Turning off locale detection
The middleware will detect a matching locale based on your routing configuration and the incoming request.
If you want to rely entirely on the URL to resolve the locale, you can set the localeDetection
property to false
. This will disable locale detection based on the accept-language
header and a potentially existing cookie value from a previous visit.
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localeDetection: false
});
In this case, only the locale prefix and a potentially matching domain are used to determine the locale.
Locale cookie
By default, the middleware will set a cookie called NEXT_LOCALE
that contains the most recently detected locale. This is used to remember the user’s locale preference for future requests.
By default, the cookie will be configured with the following attributes:
maxAge
: This value is set to 1 year so that the preference of the user is kept as long as possible.sameSite
: This value is set tolax
so that the cookie can be set when coming from an external site.path
: This value is not set by default, but will use the value of yourbasePath
if configured.
If you have more specific requirements, you can adjust these settings accordingly:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
// Will be merged with the defaults
localeCookie: {
// Custom cookie name
name: 'USER_LOCALE',
// Expire in one day
maxAge: 60 * 60 * 24
}
});
… or turn the cookie off entirely:
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
localeCookie: false
});
Note that the cookie is only set when the user switches the locale and is not updated on every request.
Alternate links
The middleware automatically sets the link
header to inform search engines that your content is available in different languages. Note that this automatically integrates with your routing strategy and will generate the correct links based on your configuration.
However, there are cases where you may want to provide these links yourself:
- You have pages that are only available for certain locales
- You’re using an external system like a CMS to manage localized slugs of your pages
In this case, you can opt-out of this behavior by setting alternateLinks
to false
.
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
// ...
alternateLinks: false
});
If you decide to manage alternate links yourself, a good option can be to include them in a sitemap.
Which alternate links are included?
Using the middleware defaults, the link
header of a response for /
will look like this:
link: <https://example.com/en>; rel="alternate"; hreflang="en",
<https://example.com/de>; rel="alternate"; hreflang="de",
<https://example.com/>; rel="alternate"; hreflang="x-default"
The x-default
entry is included to point to a variant that can be used if no other language matches the user’s browser setting. This special entry is reserved for language selection & detection, in our case issuing a 307 redirect to the best matching locale.
Note that middleware configuration is automatically incorporated with the following special cases:
localePrefix: 'always'
(default): Thex-default
entry is only included for/
, not for nested pathnames like/about
. The reason is that the default matcher doesn’t handle unprefixed pathnames apart from/
, therefore these URLs could be 404s. Note that this only applies to the optionalx-default
entry, locale-specific URLs are always included.localePrefix: 'never'
: Alternate links are entirely turned off since there might not be unique URLs per locale.
Other configuration options like domains
, pathnames
and basePath
are automatically considered.
Can I customize the alternate links?
If you need to customize the alternate links, you can either turn them off and provide your own implementation, or if you only need to make minor adaptions, you can compose the middleware and add your custom logic after the middleware has run:
import createMiddleware from 'next-intl/middleware';
import LinkHeader from 'http-link-header';
import {NextRequest} from 'next/server';
import {routing} from './i18n/routing';
const handleI18nRouting = createMiddleware(routing);
export default async function middleware(request: NextRequest) {
const response = handleI18nRouting(request);
// Example: Remove the `x-default` entry
const link = LinkHeader.parse(response.headers.get('link'));
link.refs = link.refs.filter((entry) => entry.hreflang !== 'x-default');
response.headers.set('link', link.toString());
return response;
}
Base path
The next-intl
middleware as well as the navigation APIs will automatically pick up a basePath
that you might have configured in your next.config.js
.
Note however that you should make sure that your middleware matcher
handles the root of your base path:
export const config = {
// The `matcher` is relative to the `basePath`
matcher: [
// This entry handles the root of the base
// path and should always be included
'/'
// ... other matcher config
]
};
Trailing slash
If you have trailingSlash
set to true
in your Next.js config, this setting will be taken into account by the middleware and the navigation APIs.
Note that if you’re using localized pathnames, your internal and external pathnames can be defined either with or without a trailing slash as they will be normalized internally.