Skip to content
DocsRoutingMiddleware

Middleware

💡

The middleware is only needed when you’re using i18n routing.

The middleware receives a routing configuration and takes care of:

  1. Locale negotiation
  2. Applying relevant redirects & rewrites
  3. Providing alternate links for search engines

Example:

middleware.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
 
export default createMiddleware(routing);
 
export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(de|en)/:path*']
};

Locale detection

The locale is negotiated based on your localePrefix and domains setting. Once a locale is detected, it will be remembered for future requests by being stored in the NEXT_LOCALE cookie.

Prefix-based routing (default)

By default, prefix-based routing is used to determine the locale of a request.

In this case, the locale is detected based on these priorities:

  1. A locale prefix is present in the pathname (e.g. /en/about)
  2. A cookie is present that contains a previously detected locale
  3. A locale can be matched based on the accept-language header
  4. 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:

  1. A user requests / and based on the accept-language header, the en locale is matched.
  2. The en locale is saved in a cookie and the user is redirected to /en.
  3. The app renders <Link locale="de" href="/">Switch to German</Link> to allow the user to change the locale to de.
  4. When the user clicks on the link, a request to /de is initiated.
  5. The middleware will update the cookie value to de.
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. This algorithm is expected to provide better results than the more conservative “lookup” algorithm that is specified in RFC 4647.

To illustrate this with an example, let’s consider your app supports these locales:

  1. en-US
  2. 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 a 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.

Domain-based routing

If you’re using domain-based routing, the middleware will match the request against the available domains to determine the best-matching locale. To retrieve the domain, the host is read from the x-forwarded-host header, with a fallback to host (hosting platforms typically provide these headers out-of-the-box).

The locale is detected based on these priorities:

  1. A locale prefix is present in the pathname (e.g. ca.example.com/fr)
  2. A locale is stored in a cookie and is supported on the domain
  3. A locale that the domain supports is matched based on the accept-language header
  4. 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:

  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.
How is the best matching domain for a given locale detected?

The bestmatching domain is detected based on these priorities:

  1. Stay on the current domain if the locale is supported here
  2. Use an alternative domain where the locale is configured as the defaultLocale
  3. Use an alternative domain where the available locales are restricted and the locale is supported
  4. Use an alternative domain that supports all locales

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 = {
  // Match only internationalized pathnames
  matcher: ['/', '/(de|en)/:path*']
};

This enables:

  1. A redirect at / to a suitable locale
  2. 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 needs to be statically analyzable, therefore you can’t use variables to generate this value. However, you can alternatively implement a programmatic condition in the middleware:

middleware.ts
import {NextRequest} from 'next/server';
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
 
const handleI18nRouting = createMiddleware(routing);
 
export default function middleware(request: NextRequest) {
  const {pathname} = request.nextUrl;
 
  // Matches '/', as well as all paths that start with a locale like '/en'
  const shouldHandle =
    pathname === '/' ||
    new RegExp(`^/(${locales.join('|')})(/.*)?$`).test(
      request.nextUrl.pathname
    );
  if (!shouldHandle) return;
 
  return handleI18nRouting(request);
}

Pathnames without a locale prefix

There are two use cases where you might want to match pathnames without a locale prefix:

  1. You’re using a config for localePrefix other than always
  2. You want to enable 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.

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 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 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 rewritten by accident.

Composing other middlewares

By calling createMiddleware, you’ll receive a function of the following type:

function middleware(request: NextRequest): NextResponse;

If you need to incorporate additional behavior, you can either modify the request before the next-intl middleware receives it, modify the response or even create the middleware based on dynamic configuration.

middleware.ts
import createMiddleware 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 = createMiddleware({
    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).

This example rewrites requests for /[locale]/profile to /[locale]/profile/new if a special cookie is set.

middleware.ts
import createMiddleware 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 = createMiddleware({
    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. Also, make sure to only rewrite pathnames that will not lead to a redirect, as otherwise rewritten pathnames will be redirected to.

Example: Integrating with Clerk

@clerk/nextjs provides a middleware that can be combined 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.

middleware.ts
import {clerkMiddleware, createRouteMatcher} from '@clerk/nextjs/server';
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
 
const handleI18nRouting = createMiddleware(routing);
 
const isProtectedRoute = createRouteMatcher(['/:locale/dashboard(.*)']);
 
export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) auth().protect();
 
  return handleI18nRouting(req);
});
 
export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(de|en)/:path*']
};

(based on @clerk/nextjs@^5.0.0)

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 and adapting the middleware utils to accept a response object that’s been created by the next-intl middleware instead of creating a new one:

utils/supabase/middleware.ts
import {createServerClient} from '@supabase/ssr';
import {NextResponse, type NextRequest} from 'next/server';
 
export async function updateSession(
  request: NextRequest,
  response: NextResponse
) {
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({name, value}) =>
            request.cookies.set(name, value)
          );
          cookiesToSet.forEach(({name, value, options}) =>
            response.cookies.set(name, value, options)
          );
        }
      }
    }
  );
 
  const {
    data: {user}
  } = await supabase.auth.getUser();
 
  return response;
}

Now, we can integrate the Supabase middleware with the one from next-intl:

middleware.ts
import createMiddleware from 'next-intl/middleware';
import {type NextRequest} from 'next/server';
import {routing} from './i18n/routing';
import {updateSession} from './utils/supabase/middleware';
 
const handleI18nRouting = createMiddleware(routing);
 
export async function middleware(request: NextRequest) {
  const response = handleI18nRouting(request);
 
  // A `response` can now be passed here
  return await updateSession(request, response);
}
 
export const config = {
  matcher: ['/', '/(de|en)/:path*']
};

(based on @supabase/ssr@^0.5.0)

Example: Integrating with Auth.js (aka NextAuth.js)

The Next.js middleware of Auth.js requires an integration with their control flow to be compatible with other middlewares. The success callback 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 (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 createMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
import {routing} from './i18n/routing';
 
const publicPages = ['/', '/login'];
 
const handleI18nRouting = createMiddleware(routing);
 
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 handleI18nRouting(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 handleI18nRouting(req);
  } else {
    return (authMiddleware as any)(req);
  }
}
 
export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)']
};

(based on next-auth@^4.0.0)

💡

Have a look at the next-intl with NextAuth.js example to explore a working setup.

Usage without middleware (static export)

If you’re using the static export feature from Next.js (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. Using a locale prefix is required (same as localePrefix: 'always')
  2. The locale can’t be negotiated on the server (same as localeDetection: false)
  3. You can’t use pathname localization, as these require server-side rewrites
  4. Static rendering is required

Additionally, other limitations as documented by Next.js will apply too.

If you choose this approach, you might want to enable a redirect at the root of your app:

app/page.tsx
import {redirect} from 'next/navigation';
 
// Redirect the user to the default locale when `/` is requested
export default function RootPage() {
  redirect('/en');
}

Additionally, Next.js will ask for a root layout for app/page.tsx, even if it’s just passing children through:

app/layout.tsx
export default function RootLayout({children}) {
  return children;
}

Troubleshooting

”The middleware doesn’t run for a particular page.”

To resolve this, make sure that:

  1. The middleware is set up in the correct file (e.g. src/middleware.ts).
  2. Your middleware matcher correctly matches all routes of your application, including dynamic segments with potentially unexpected characters like dots (e.g. /users/jane.doe).
  3. In case you’re composing other middlewares, ensure that the middleware is called correctly.
  4. In case you require static rendering, make sure to follow the static rendering guide instead of relying on hacks like force-static.

”My page content isn’t localized despite the pathname containing a locale prefix.”

This is very likely the result of your middleware not running on the request. As a result, a potential fallback from i18n/request.ts might be applied.

”Unable to find next-intl locale because the middleware didn’t run on this request and no locale was returned in getRequestConfig.”

If the middleware is not expected to run on this request (e.g. because you’re using a setup without i18n routing), you should explicitly return a locale from getRequestConfig to recover from this error.

If the middleware is expected to run, verify that your middleware is set up correctly.

Note that next-intl will invoke the notFound() function to abort the render if no locale is available after getRequestConfig has run. You should consider adding a not-found page due to this.

Docs

 · 

Examples

 · 

Blog