Skip to content
Docs
Routing
Middleware

Next.js 13 middleware for i18n routing

The middleware handles redirects and rewrites based on the detected user locale.

middleware.ts
import createMiddleware from 'next-intl/middleware';
 
export default createMiddleware({
  // A list of all locales that are supported
  locales: ['en', 'de'],
 
  // If this locale is matched, pathnames work without a prefix (e.g. `/about`)
  defaultLocale: 'en'
});
 
export const config = {
  // Skip all paths that should not be internationalized. This example skips
  // certain folders and all pathnames with a dot (e.g. favicon.ico)
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
};

In addition to handling i18n routing, the middleware sets the link header (opens in a new tab) to inform search engines that your content is available in different languages.

Strategies

There are two strategies for detecting the locale:

  1. Prefix-based routing (default)
  2. Domain-based routing

Once a locale is detected, it will be saved in a cookie.

Strategy 1: Prefix-based routing (default)

Since your pages are nested within a [locale] folder, all routes are prefixed with one of your supported locales (e.g. /de/about). To keep the URL short, requests for the default locale are rewritten internally to work without a locale prefix.

Request examples:

  • //en
  • /about/en/about
  • /de/about/de/about

Locale detection

The locale is detected based on these priorities:

  1. A locale prefix is present in the pathname (e.g. /de/about)
  2. A cookie is present that contains a previously detected locale
  3. The accept-language header (opens in a new tab) is matched against the available locales
  4. 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.

Example workflow:

  1. A user requests / and based on the accept-language header, the de locale is matched.
  2. The de locale is saved in a cookie and the user is redirected to /de.
  3. The app renders <Link locale="en" href="/">Switch to English</Link> to allow the user to change the locale to en.
  4. When the user clicks on the link, a request to /en is initiated.
  5. The middleware will update the cookie value to en and subsequently redirects the user to /.

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.

Example:

  • us.example.com (default: en)
  • ca.example.com (default: en)
  • ca.example.com/fr (fr)
middleware.ts
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 managed by this domain. If this
      // domain receives requests for another locale (e.g. us.example.com/fr),
      // then the middleware will redirect to a domain that supports it.
      locales: ['en']
    },
    {
      domain: 'ca.example.com',
      defaultLocale: 'en'
      // If there are no `locales` specified on a domain,
      // all global locales will be supported here.
    }
  ]
});

The middleware rewrites the requests internally, so that requests for the defaultLocale on a given domain work without a locale prefix (e.g. us.example.com/about/en/about). If you want to include a prefix for the default locale as well, you can add localePrefix: 'always'.

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:

  1. A locale prefix is present in the pathname and the domain supports it (e.g. ca.example.com/fr)
  2. If the host of the request is configured in domains, the defaultLocale of the domain is used
  3. As a fallback, the locale detection of prefix-based routing applies
💡

Since unknown domains will be handled with prefix-based routing, this strategy can be used for local development where the host is localhost.

Since the middleware is aware of all your domains, the domain will automatically be switched when the user requests to change the locale.

Example workflow:

  1. The user requests us.example.com and based on the defaultLocale of this domain, the en locale is matched.
  2. The app renders <Link locale="fr" href="/">Switch to French</Link> to allow the user to change the locale to fr.
  3. When the link is clicked, a request to us.example.com/fr is initiated.
  4. The middleware recognizes that the user wants to switch to another domain and responds with a redirect to ca.example.com/fr.

Further configuration

Always use a locale prefix

If you want to include a prefix for the default locale as well, you can configure the middleware accordingly.

middleware.ts
import createMiddleware from 'next-intl/middleware';
 
export default createMiddleware({
  // ... other config
 
  localePrefix: 'always'
});

In this case, requests without a prefix will be redirected accordingly (e.g. /about to /en/about).

Note that this will affect both prefix-based as well as domain-based routing.

Never use a locale prefix

For applications behind an authentication layer, where there is no need for SEO, it is possible to have the locale never show up in the URL.

middleware.ts
import createMiddleware from 'next-intl/middleware';
 
export default createMiddleware({
  // ... other config
 
  localePrefix: 'never'
});

In this case all requests for all locales will be rewritten to have the locale 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 alternate links are disabled in this mode since there are no distinct URLs per language.

Disable automatic 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.

middleware.ts
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.

Disable 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, you can opt-out of this behavior.

middleware.ts
import createMiddleware from 'next-intl/middleware';
 
export default createMiddleware({
  // ... other config
 
  alternateLinks: false // Defaults to `true`
});

Localizing pathnames

⚠️

This API is only available in the Server Components beta.

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.

middleware.ts
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]'
    },
 
    // 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.

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:

middleware.ts
export const config = {
  // Skip all paths that should not be internationalized. This example skips
  // certain folders and all pathnames with a dot (e.g. favicon.ico)
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
};

However, this can lead to false negatives if you have pages that contain the character . (e.g. /users/jane.doe). To make sure these are processed by the middleware, you can add corresponding entries to the matcher config:

middleware.ts
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 without `.`
    '/((?!api|_next|_vercel|.*\\..*).*)',
    // Match all pathnames within `/users`, optionally with a locale prefix
    '/(.+)?/users/(.+)'
  ]
};

Additionally, 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.

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.

middleware.ts
import createIntlMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
 
export default async function middleware(request: NextRequest) {
  // Step 1: Use the incoming request
  const defaultLocale = request.headers.get('x-default-locale') || 'en';
 
  // Step 2: Create and call the next-intl middleware
  const handleI18nRouting = createIntlMiddleware({
    locales: ['en', 'de'],
    defaultLocale
  });
  const response = handleI18nRouting(request);
 
  // Step 3: Alter the response
  response.headers.set('x-default-locale', defaultLocale);
 
  return response;
}
 
export const config = {
  matcher: ['/((?!_next|.*\\..*).*)']
};

Example: Integrating with Clerk

@clerk/nextjs (opens in a new tab) provides a middleware that can be integrated with next-intl by using the beforeAuth hook (opens in a new tab). By doing this, the middleware from next-intl will run first, potentially redirect or rewrite incoming requests, followed by the middleware from @clerk/next acting on the response.

middleware.ts
import {authMiddleware} from '@clerk/nextjs';
import createMiddleware from 'next-intl/middleware';
 
const intlMiddleware = createMiddleware({
  locales: ['en', 'de'],
  defaultLocale: 'en'
});
 
export default authMiddleware({
  beforeAuth(request) {
    return intlMiddleware(request);
  },
 
  // Ensure that locale-specific sign in pages are public
  publicRoutes: ['/', '/:locale/sign-in']
});
 
export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)']
};

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.

middleware.ts
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,
  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.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|.*\\..*).*)']
};

Many thanks to narakhan (opens in a new tab) for sharing his middleware implementation (opens in a new tab)!

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:

  1. There's no default locale that can be used without a prefix (same as localePrefix: 'always')
  2. The locale can't be negotiated at runtime (same as localeDetection: false)
  3. You can't use pathname localization
  4. You need to add a redirect for the root of the app
  5. Currently this is limited to the usage of next-intl in Client Components (Server Components are not supported yet).
app/page.tsx
import {redirect} from 'next/navigation';
 
// Redirect the user to the default locale when the app root is requested
export default function RootPage() {
  redirect('/en');
}

You can explore a working demo by building the Next.js 13 example after enabling the static export:

next.config.js
module.exports = {
  output: 'export'
};