Middleware
The middleware is only needed when you're using i18n routing.
The middleware handles redirects and rewrites based on the detected user locale.
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// A list of all locales that are supported
locales: ['en', 'de'],
// Used when no locale matches
defaultLocale: 'en'
});
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(de|en)/:path*']
};
In addition to handling i18n routing, the middleware sets the link
header to inform search engines that your content is available in different languages (see alternate links).
Strategies
There are two strategies for detecting the locale:
Once a locale is detected, it will be saved in the NEXT_LOCALE
cookie.
Strategy 1: Prefix-based routing (default)
Since your pages are nested within a [locale]
folder, all routes are by default prefixed with one of your supported locales (e.g. /en/about
).
Locale detection
The locale is detected based on these priorities:
- A locale prefix is present in the pathname (e.g.
/en/about
) - A cookie is present that contains a previously detected locale
- A locale can be matched based on the
accept-language
header (opens in a new tab) - As a last resort, the
defaultLocale
is used
To change the locale, users can visit a prefixed route. This will take precedence over a previously matched locale that is saved in a cookie or the accept-language
header and will update the previous cookie value.
Example workflow:
- A user requests
/
and based on theaccept-language
header, theen
locale is matched. - The
en
locale is saved in a cookie and the user is redirected to/en
. - The app renders
<Link locale="de" href="/">Switch to German</Link>
to allow the user to change the locale tode
. - When the user clicks on the link, a request to
/de
is initiated. - The middleware will update the cookie value to
de
.
You can optionally remove the locale prefix in pathnames by changing the
localePrefix
setting.
Which algorithm is used to match the accept-language header against the available locales?
To determine the best-matching locale based on the available options from your app, the middleware uses the "best fit" algorithm of @formatjs/intl-localematcher
(opens in a new tab). This algorithm is expected to provide better results than the more conservative "lookup" algorithm that is specified in RFC 4647 (opens in a new tab).
To illustrate this with an example, let's consider your app supports these locales:
en-US
de-DE
The "lookup" algorithm works by progressively removing subtags from the user's accept-language
header until a match is found. This means that if the user's browser sends the accept-language
header en-GB
, the "lookup" algorithm will not find a match, resulting in the default locale being used.
In contrast, the "best fit" algorithm compares the distance between the user's accept-language
header and the available locales, while taking into consideration regional information. Due to this, the "best fit" algorithm is able to match en-US
as the best-matching locale in this case.
Strategy 2: Domain-based routing
If you want to serve your localized content based on different domains, you can provide a list of mappings between domains and locales to the middleware.
Examples:
us.example.com/en
ca.example.com/en
ca.example.com/fr
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// All locales across all domains
locales: ['en', 'fr'],
// Used when no domain matches (e.g. on localhost)
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
}
]
});
You can optionally remove the locale prefix in pathnames by changing the
localePrefix
setting.
Locale detection
To match the request against the available domains, the host is read from the x-forwarded-host
header, with a fallback to host
.
The locale is detected based on these priorities:
- A locale prefix is present in the pathname (e.g.
ca.example.com/fr
) - A locale is stored in a cookie and is supported on the domain
- A locale that the domain supports is matched based on the
accept-language
header (opens in a new tab) - As a fallback, the
defaultLocale
of the domain is used
Since the middleware is aware of all your domains, if a domain receives a request for a locale that is not supported (e.g. en.example.com/fr
), it will redirect to an alternative domain that does support the locale.
Example workflow:
- The user requests
us.example.com
and based on thedefaultLocale
of this domain, theen
locale is matched. - The app renders
<Link locale="fr" href="/">Switch to French</Link>
to allow the user to change the locale tofr
. - When the link is clicked, a request to
us.example.com/fr
is initiated. - The middleware recognizes that the user wants to switch to another domain and responds with a redirect to
ca.example.com/fr
.
How is the best matching domain for a given locale detected?
The bestmatching domain is detected based on these priorities:
- Stay on the current domain if the locale is supported here
- Use an alternative domain where the locale is configured as the
defaultLocale
- Use an alternative domain where the available
locales
are restricted and the locale is supported - Use an alternative domain that supports all locales
Further configuration
Locale prefix
By default, the pathnames of your app will be available under a prefix that matches your directory structure (e.g. src/app/[locale]/about/page.tsx
→ /en/about
). You can however customize the routing to optionally remove the prefix.
Note that if you're using the navigation APIs from next-intl
, you want to make sure your localePrefix
setting matches your middleware configuration.
Always use a locale prefix (default)
By default, pathnames always start with the locale (e.g. /en/about
).
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// ... other config
localePrefix: 'always' // This is the default
});
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 don't want to include a locale prefix for the default locale, but only for non-default locales, you can configure the middleware accordingly:
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// ... other config
localePrefix: 'as-needed'
});
In this case, requests where the locale prefix matches the default locale will be redirected (e.g. /en/about
to /about
). This will affect both prefix-based as well as domain-based routing.
Note that:
- If you use this strategy, you should make sure that your matcher detects unprefixed pathnames.
- If you use the
Link
component, the initial render will point to the prefixed version but will be patched immediately on the client once the component detects that the default locale has rendered. The prefixed version is still valid, but SEO tools might report a hint that the link points to a redirect.
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 middleware in the first place.
In case you're using the middleware, but you don't want your pathnames to be prefixed with a locale, you can configure the middleware to never show a locale prefix in the URL. This can be useful e.g. if you're using domain-based routing and you support only a single locale per domain.
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// ... other config
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 make sure that your matcher detects unprefixed pathnames.
- If you don't use domain-based routing, the cookie is now the source of truth for determining the locale in the middleware. 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 (opens in a new tab) for cacheable requests). - Alternate links are disabled in this mode since there might not be distinct URLs per locale.
Locale detection
If you want to rely entirely on the URL to resolve the locale, you can disable locale detection based on the accept-language
header and a potentially existing cookie value from a previous visit.
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// ... other config
localeDetection: false
});
In this case, only the locale prefix and a potentially matching domain are used to determine the locale.
Note that by setting this option, the middleware will no longer return a set-cookie
response header, which can be beneficial for CDN caching (see e.g. the Cloudflare Cache rules for set-cookie
(opens in a new tab)).
Alternate links
The middleware automatically sets the link
header (opens in a new tab) 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.
If you prefer to include these links yourself, e.g. because you're using a CMS to manage localized slugs of your pages, you can opt-out of this behavior.
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// ... other config
alternateLinks: false // Defaults to `true`
});
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
(opens in a new tab) 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 createIntlMiddleware from 'next-intl/middleware';
import LinkHeader from 'http-link-header';
import {NextRequest} from 'next/server';
const handleI18nRouting = createIntlMiddleware(/* ... */);
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;
}
Localizing pathnames
Many apps choose to localize pathnames, especially when search engine optimization is relevant, e.g.:
/en/about
/de/ueber-uns
Since you want to define these routes only once internally, you can use the next-intl
middleware to rewrite (opens in a new tab) such incoming requests to shared pathnames.
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
defaultLocale: 'en',
locales: ['en', 'de'],
// 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]'
}
}
});
If you have pathname localization set up in the middleware, you likely want to use the localized navigation APIs in your components.
How can I revalidate localized pathnames?
Depending on if a route is generated statically (at build time) or dynamically (at runtime), revalidatePath
(opens in a new tab) needs to be called either for the localized or the internal pathname.
Consider this example:
app
└── [locale]
└── news
└── [slug]
… with this middleware configuration:
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
defaultLocale: 'en',
locales: ['en', 'fr'],
pathnames: {
'/news/[slug]': {
en: '/news/[slug]',
fr: '/infos/[slug]'
}
}
});
Depending on whether some-article
was included in generateStaticParams
(opens in a new tab) or not, you can revalidate the route like this:
// Statically generated at build time
revalidatePath('/fr/news/some-article');
// Dynamically generated at runtime:
revalidatePath('/fr/infos/some-article');
When in doubt, you can revalidate both paths to be on the safe side.
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 (opens in a new tab), 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.
Matcher config
The middleware is intended to only run on pages, not on arbitrary files that you serve independently of the user locale (e.g. /favicon.ico
).
Because of this, the following config is generally recommended:
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(de|en)/:path*']
};
This enables:
- A redirect at
/
to a suitable locale - Internationalization of all pathnames starting with a locale (e.g.
/en/about
)
Can I avoid hardcoding the locales in the matcher
config?
A Next.js matcher
(opens in a new tab) needs to be statically analyzable, therefore you can't use variables to generate this value dynamically. However, in case you're self-hosting Next.js via a Node.js server, you can implement the matcher dynamically instead:
import {NextRequest} from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
// Can be imported from a shared module
const locales = ['en', 'de'];
const intlMiddleware = createIntlMiddleware({
locales
// ...
});
export default function middleware(request: NextRequest) {
const {pathname} = request.nextUrl;
const shouldHandle =
pathname === '/' ||
new RegExp(`^/(${locales.join('|')})(/.*)?$`).test(
request.nextUrl.pathname
);
if (!shouldHandle) return;
return intlMiddleware(request);
}
Pathnames without a locale prefix
There are two use cases where you might want to match pathnames without a locale prefix:
- You're using a config for
localePrefix
other thanalways
- You want to implement redirects that add a locale for unprefixed pathnames (e.g.
/about
→/en/about
)
For these cases, the middleware should run on requests for pathnames without a locale prefix as well.
A popular strategy is to match all routes that don't start with certain segments (e.g. /_next
) and also none that include a dot (.
) since these typically indicate static files. However, if you have some routes where a dot is expected (e.g. /users/jane.doe
), you should explicitly provide a matcher for these.
export const config = {
// Matcher entries are linked with a logical "or", therefore
// if one of them matches, the middleware will be invoked.
matcher: [
// Match all pathnames except for
// - … if they start with `/api`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
'/((?!api|_next|_vercel|.*\\..*).*)',
// However, match all pathnames within `/users`, optionally with a locale prefix
'/([\\w-]+)?/users/(.+)'
]
};
Note that some third-party providers like Vercel Analytics (opens in a new tab) and umami (opens in a new tab) typically use internal endpoints that are then rewritten to an external URL (e.g. /_vercel/insights/view
). Make sure to exclude such requests from your middleware matcher so they aren't accidentally rewritten.
Base path
The next-intl
middleware as well as the navigation APIs will automatically pick up a basePath
(opens in a new tab) 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
]
};
Composing other middlewares
By calling createMiddleware
, you'll receive a function of the following type:
middleware(request: NextRequest): NextResponse
If you need to incorporate additional behavior, you can either modify the request before the next-intl
middleware receives it, or modify the response that is returned.
import createIntlMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
export default async function middleware(request: NextRequest) {
// Step 1: Use the incoming request (example)
const defaultLocale = request.headers.get('x-your-custom-locale') || 'en';
// Step 2: Create and call the next-intl middleware (example)
const handleI18nRouting = createIntlMiddleware({
locales: ['en', 'de'],
defaultLocale
});
const response = handleI18nRouting(request);
// Step 3: Alter the response (example)
response.headers.set('x-your-custom-locale', defaultLocale);
return response;
}
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(de|en)/:path*']
};
Example: Additional rewrites
If you need to handle rewrites apart from the ones provided by next-intl
, you can adjust the pathname
of the request
before invoking the next-intl
middleware (based on "A/B Testing with Cookies" by Vercel (opens in a new tab)).
This example rewrites requests for /[locale]/profile
to /[locale]/profile/new
if a special cookie is set.
import createIntlMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
export default async function middleware(request: NextRequest) {
const [, locale, ...segments] = request.nextUrl.pathname.split('/');
if (locale != null && segments.join('/') === 'profile') {
const usesNewProfile =
(request.cookies.get('NEW_PROFILE')?.value || 'false') === 'true';
if (usesNewProfile) {
request.nextUrl.pathname = `/${locale}/profile/new`;
}
}
const handleI18nRouting = createIntlMiddleware({
locales: ['en', 'de'],
defaultLocale: 'en'
});
const response = handleI18nRouting(request);
return response;
}
export const config = {
matcher: ['/', '/(de|en)/:path*']
};
Note that if you use a localePrefix
other than always
, you need to adapt the handling appropriately to handle unprefixed pathnames too.
Example: Integrating with Clerk
@clerk/nextjs
(opens in a new tab) provides a middleware that can be combined (opens in a new tab) with other middlewares like the one provided by next-intl
. By combining them, the middleware from @clerk/next
will first ensure protected routes are handled appropriately. Subsequently, the middleware from next-intl
will run, potentially redirecting or rewriting incoming requests.
import {clerkMiddleware, createRouteMatcher} from '@clerk/nextjs/server';
import createMiddleware from 'next-intl/middleware';
const intlMiddleware = createMiddleware({
locales: ['en', 'de'],
defaultLocale: 'en'
});
const isProtectedRoute = createRouteMatcher(['/:locale/dashboard(.*)']);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect();
return intlMiddleware(req);
});
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(de|en)/:path*']
};
Example: Integrating with Supabase Authentication
In order to use Supabase Authentication with next-intl
, you need to combine the Supabase middleware with the one from next-intl
.
You can do so by following the setup guide from Supabase (opens in a new tab) and adapting the middleware as follows:
import {type NextRequest} from 'next/server';
import {createServerClient, type CookieOptions} from '@supabase/ssr';
import createIntlMiddleware from 'next-intl/middleware';
const handleI18nRouting = createIntlMiddleware({
locales: ['en', 'de'],
defaultLocale: 'en'
});
export async function middleware(request: NextRequest) {
const response = handleI18nRouting(request);
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({name, value, ...options});
response.cookies.set({name, value, ...options});
},
remove(name: string, options: CookieOptions) {
request.cookies.set({name, value: '', ...options});
response.cookies.set({name, value: '', ...options});
}
}
}
);
await supabase.auth.getUser();
return response;
}
export const config = {
matcher: ['/', '/(de|en)/:path*']
};
Note that cookies need to be set simultaneously for the request and the response.
Example: Integrating with Auth.js (aka NextAuth.js)
The Next.js middleware of Auth.js (opens in a new tab) requires an integration with their control flow to be compatible with other middlewares. The success callback (opens in a new tab) can be used to run the next-intl
middleware on authorized pages. However, public pages need to be treated separately.
For pathnames specified in the pages
object (opens in a new tab) (e.g. signIn
), Auth.js will skip the entire middleware and not run the success callback. Therefore, we have to detect these pages before running the Auth.js middleware and only run the next-intl
middleware in this case.
import {withAuth} from 'next-auth/middleware';
import createIntlMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
const locales = ['en', 'de'];
const publicPages = ['/', '/login'];
const intlMiddleware = createIntlMiddleware({
locales,
localePrefix: 'as-needed',
defaultLocale: 'en'
});
const authMiddleware = withAuth(
// Note that this callback is only invoked if
// the `authorized` callback has returned `true`
// and not for pages listed in `pages`.
function onSuccess(req) {
return intlMiddleware(req);
},
{
callbacks: {
authorized: ({token}) => token != null
},
pages: {
signIn: '/login'
}
}
);
export default function middleware(req: NextRequest) {
const publicPathnameRegex = RegExp(
`^(/(${locales.join('|')}))?(${publicPages
.flatMap((p) => (p === '/' ? ['', '/'] : p))
.join('|')})/?$`,
'i'
);
const isPublicPage = publicPathnameRegex.test(req.nextUrl.pathname);
if (isPublicPage) {
return intlMiddleware(req);
} else {
return (authMiddleware as any)(req);
}
}
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)']
};
There's a working example that combines next-intl
with Auth.js on GitHub.
Usage without middleware (static export)
If you're using the static export feature from Next.js (opens in a new tab) (output: 'export'
), the middleware will not run. You can use prefix-based routing nontheless to internationalize your app, but a few tradeoffs apply.
Static export limitations:
- There's no default locale that can be used without a prefix (same as
localePrefix: 'always'
) - The locale can't be negotiated at runtime (same as
localeDetection: false
) - You can't use pathname localization
- This requires static rendering
- You need to add a redirect for the root of the app
import {redirect} from 'next/navigation';
// Redirect the user to the default locale when `/` is requested
export default function RootPage() {
redirect('/en');
}
Note that other limitations as documented by Next.js (opens in a new tab) will apply too.
Troubleshooting
"Unable to find next-intl
locale because the middleware didn't run on this request."
This can happen either because:
- You're using a setup with i18n routing but the middleware is not set up.
- You're using a setup without i18n routing but are reading the
locale
param passed to the function withingetRequestConfig
. - The middleware is set up in the wrong file (e.g. you're using the
src
folder, butmiddleware.ts
was added in the root folder). - The middleware matcher didn't match a request, but you're using APIs from
next-intl
in a component. - You're attempting to implement static rendering via
force-static
(opens in a new tab).
To recover from this error, please make sure that:
- You're consistently using a setup with or without i18n routing (i.e. with or without the routing APIs).
- If you're not using i18n routing:
- You don't read the
locale
param ingetRequestConfig
but instead return it.
- You don't read the
- If you're using i18n routing:
- You're using APIs from
next-intl
(including the navigation APIs) exclusively within the[locale]
segment. - Your middleware matcher matches all routes of your application, including dynamic segments with potentially unexpected characters like dots (e.g.
/users/jane.doe
). - If you're using
localePrefix: 'as-needed'
, thelocale
segment effectively acts like a catch-all for all unknown routes. You should make sure that thelocale
is validated before it's used by any APIs fromnext-intl
. - To implement static rendering, make sure to provide a static locale to
next-intl
instead of usingforce-static
.
- You're using APIs from
Note that next-intl
will invoke the notFound()
function to abort the render if the locale can't be found. You should consider adding a not-found
page due to this.