Skip to main content
The Vercel adapter provides comprehensive support for Next.js internationalization (i18n), including automatic locale routing, domain-based locales, and locale detection.

i18n configuration

The adapter reads i18n configuration from your Next.js config:
const i18nConfig = config.i18n;
const vercelConfig: VercelConfig = {
  version: 3,
  overrides: {},
  wildcard: i18nConfig?.domains
    ? i18nConfig.domains.map((item) => {
        return {
          domain: item.domain,
          value:
            item.defaultLocale === i18nConfig.defaultLocale
              ? ''
              : `/${item.defaultLocale}`,
        };
      })
    : undefined,
  images: getImagesConfig(config),
};
Location: index.ts:50-66

Wildcard domain configuration

Domain-based locales use the wildcard configuration:
wildcard: i18nConfig?.domains
  ? i18nConfig.domains.map((item) => {
      return {
        domain: item.domain,
        value:
          item.defaultLocale === i18nConfig.defaultLocale
            ? ''
            : `/${item.defaultLocale}`,
      };
    })
  : undefined
Location: index.ts:54-64 and types.ts:31-36
The wildcard value is empty for the default locale matching the global default, or contains the locale path for other locales.

Automatic locale prefix

The adapter automatically adds the default locale to paths:
1

Root path handling

{
  src: `^${path.posix.join(
    '/',
    config.basePath,
    '/'
  )}(?!(?:_next/.*|${config.i18n.locales
    .map((locale) => escapeStringRegexp(locale))
    .join('|')})(?:/.*|$))$`,
  dest: `${config.basePath && config.basePath !== '/'
    ? path.posix.join('/', config.basePath)
    : ''
  }$wildcard${config.trailingSlash ? '/' : ''}`,
  continue: true,
}
Location: index.ts:287-303
2

Path with segments

{
  src: `^${path.posix.join(
    '/',
    config.basePath,
    '/'
  )}(?!(?:_next/.*|${config.i18n.locales
    .map((locale) => escapeStringRegexp(locale))
    .join('|')})(?:/.*|$))(.*)$`,
  dest: `${config.basePath && config.basePath !== '/'
    ? path.posix.join('/', config.basePath)
    : ''
  }$wildcard/$1`,
  continue: true,
}
Location: index.ts:304-320

Domain-based locale redirects

When using domain-based locales with detection enabled:
...(config.i18n.domains &&
config.i18n.domains.length > 0 &&
config.i18n.localeDetection !== false
  ? [
      {
        src: `^${path.posix.join(
          '/',
          config.basePath
        )}/?(?:${config.i18n.locales
          .map((locale) => escapeStringRegexp(locale))
          .join('|')})?/?$`,
        locale: {
          redirect: config.i18n.domains.reduce(
            (prev: Record<string, string>, item) => {
              prev[item.defaultLocale] = `http${
                item.http ? '' : 's'
              }://${item.domain}/`;

              if (item.locales) {
                item.locales.map((locale) => {
                  prev[locale] = `http${item.http ? '' : 's'}://${
                    item.domain
                  }/${locale}`;
                });
              }
              return prev;
            },
            {}
          ),
          cookie: 'NEXT_LOCALE',
        },
        continue: true,
      },
    ]
  : [])
Location: index.ts:323-357
Domain redirects use the NEXT_LOCALE cookie to detect the user’s preferred locale.

Path-based locale redirects

For path-based locales with detection enabled:
...(config.i18n.localeDetection !== false
  ? [
      {
        src: '/',
        locale: {
          redirect: config.i18n.locales.reduce(
            (prev: Record<string, string>, locale) => {
              prev[locale] =
                locale === config.i18n?.defaultLocale
                  ? `/`
                  : `/${locale}`;
              return prev;
            },
            {}
          ),
          cookie: 'NEXT_LOCALE',
        },
        continue: true,
      },
    ]
  : [])
Location: index.ts:360-384

Default locale prefix handling

The adapter adds the default locale prefix before user redirects:
{
  src: `^${path.posix.join('/', config.basePath)}$`,
  dest: `${path.posix.join(
    '/',
    config.basePath,
    config.i18n.defaultLocale
  )}`,
  continue: true,
},
{
  src: `^${path.posix.join(
    '/',
    config.basePath,
    '/'
  )}(?!(?:_next/.*|${config.i18n.locales
    .map((locale) => escapeStringRegexp(locale))
    .join('|')})(?:/.*|$))(.*)$`,
  dest: `${path.posix.join(
    '/',
    config.basePath,
    config.i18n.defaultLocale
  )}/$1`,
  continue: true,
}
Location: index.ts:390-418

404 and 500 locale handling

Error pages respect locale routing:
...(config.i18n
  ? [
      {
        src: `${path.posix.join(
          '/',
          config.basePath,
          '/'
        )}(?:${config.i18n.locales
          .map((locale) => locale.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
          .join('|')})?[/]?404/?`,
        status: 404,
        continue: true,
        missing: [
          {
            type: 'header' as const,
            key: 'x-prerender-revalidate',
          },
        ],
      },
    ]
  : [...])
Location: index.ts:433-489 and index.ts:893-935

Locale prefix removal

Locale prefixes are removed when checking public files:
{
  src: path.posix.join(
    '/',
    config.basePath,
    escapeStringRegexp(config.i18n.defaultLocale)
  ),
  dest: '/',
  check: true,
},
{
  src: `^${path.posix.join(
    '/',
    config.basePath
  )}/?(?:${config.i18n.locales
    .map((locale) => escapeStringRegexp(locale))
    .join('|')})/(.*)`,
  dest: `${path.posix.join('/', config.basePath, '/')}$1`,
  check: true,
}
Location: index.ts:732-751

Locale detection control

When locale detection is disabled, rewrites are added as fallbacks:
...(config.i18n?.localeDetection === false
  ? [
      {
        src: `^${path.posix.join('/', config.basePath)}$`,
        dest: `${path.posix.join(
          '/',
          config.basePath,
          config.i18n.defaultLocale
        )}`,
        check: true,
      },
      {
        src: `^${path.posix.join(
          '/',
          config.basePath,
          '/'
        )}(?!(?:_next/.*|${config.i18n.locales
          .map((locale) => escapeStringRegexp(locale))
          .join('|')})(?:/.*|$))(.*)$`,
        dest: `${path.posix.join(
          '/',
          config.basePath,
          config.i18n.defaultLocale
        )}/$1`,
        check: true,
      },
    ]
  : [])
Location: index.ts:697-730
With locale detection disabled, the default locale prefix is added during the “check” phase instead of before redirects.

Locale normalization in handlers

The Node.js handler normalizes locale paths:
function normalizeLocalePath(
  pathname: string,
  locales?: readonly string[]
): {
  pathname: string;
  locale?: string;
} {
  if (!locales) return { pathname };

  const lowercasedLocales = locales.map((locale) =>
    locale.toLowerCase()
  );

  const segments = pathname.split('/', 2);
  if (!segments[1]) return { pathname };

  const segment = segments[1].toLowerCase();

  const index = lowercasedLocales.indexOf(segment);
  if (index < 0) return { pathname };

  const detectedLocale = locales[index];
  pathname = pathname.slice(detectedLocale.length + 1) || '/';

  return { pathname, locale: detectedLocale };
}
Location: node-handler.ts:134-168

Locale in function context

The detected locale is passed to page handlers:
const {
  matchedPathname: page,
  locale,
  matches,
} = matchUrlToPage(urlPathname);

await mod.handler(req, res, {
  waitUntil: getRequestContext().waitUntil,
  requestMeta: {
    ...internalMetadata,
    minimalMode: true,
    relativeProjectDir: '.',
    locale,
    initURL,
  },
});
Location: node-handler.ts:387-431

Build docs developers (and LLMs) love