Detecting a user's preferred language

Redirecting new visitors to their most likely language

Problem

When a user first visits an application's root (e.g., /), it displays a default language, such as English. This creates immediate friction for users who speak other languages, forcing them to find a language switcher manually, even though their browser already communicates their preference.

Solution

Use middleware to intercept requests for the root path (/). Check the user's Accept-Language HTTP header to find their preferred language. If that language is supported by the application, redirect the user to that language's root (e.g., /fr). If not, redirect them to a default language (e.g., /en).

Steps

1. Install a language parser

The Accept-Language header can be complex (e.g., fr-CH, fr;q=0.9, en;q=0.8). A small library helps to parse this header and find the best match from our list of supported languages.

Run this command in your terminal:

npm install accept-language-parser

2. Define your languages and default

Create a central configuration file to store your list of supported languages and define a default. This default will be used if a user's browser preferences don't match any language you support.

// i18n.config.ts

export const locales = ['en', 'es', 'fr'];
export const defaultLocale = 'en';

3. Create the middleware

Create a middleware.ts file at the root of your project. This file will run on incoming requests, allowing you to check the path and headers.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import parser from 'accept-language-parser';
import { locales, defaultLocale } from './i18n.config';

// Helper function to find the best language match
function getBestLocale(acceptLangHeader: string | null) {
  if (!acceptLangHeader) {
    return defaultLocale;
  }

  // Use the parser to find the best supported language
  const bestMatch = parser.pick(locales, acceptLangHeader, {
    loose: true,
  });

  return bestMatch || defaultLocale;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. Check if the request is for the root path
  if (pathname === '/') {
    // Get the user's preferred language
    const acceptLang = request.headers.get('Accept-Language');
    const bestLocale = getBestLocale(acceptLang);

    // Redirect to the best-matched language path
    request.nextUrl.pathname = `/${bestLocale}`;
    return NextResponse.redirect(request.nextUrl);
  }

  // 2. For all other paths, continue as normal
  return NextResponse.next();
}

export const config = {
  matcher: [
    // Skip all paths that start with:
    // - api (API routes)
    // - _next/static (static files)
    // - _next/image (image optimization files)
    // - favicon.ico (favicon file)
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

This code only runs logic for the root path (/). If a user visits /, it checks their Accept-Language header, finds the best match (e.g., es), and redirects them to /es. All other requests, like /en/about, are ignored by this logic and pass through.