Handling unsupported language codes

Problem

An application uses the URL path (e.g., /en/, /fr/) to determine language, but users can manually type any value, like /xx/about. When this value doesn't match a supported language, the application may crash, show a generic error, or display un-translated content, failing to guide the user back to a valid experience.

Solution

Use a middleware to intercept all incoming requests. This middleware will validate the language code from the URL against a definitive list of supported languages. If the code is not supported, the request will be rewritten to a "Not Found" page before it reaches the application logic.

Steps

1. Define your supported languages

Create a central configuration file to store your list of valid language codes (locales). This makes the list reusable for your middleware and other parts of your app.

// i18n.config.ts
export const locales = ['en', 'es', 'fr'];

2. Create the middleware file

Create a new file named middleware.ts at the root of your project (or in your src/ directory). Next.js will automatically detect this file and run it on requests.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { locales } from './i18n.config';

export function middleware(request: NextRequest) {
  // Logic will go here in the next step
}

3. Add the validation logic

Inside the middleware function, get the pathname from the request. We need to check the first segment of the path (e.g., en in /en/about) and see if it's a valid, supported language.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { locales } from './i18n.config';

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

  // 1. Handle the root path separately
  // (This will be handled by the next recipe: "Detecting a user's
  // preferred language"). For now, we just let it pass.
  if (pathname === '/') {
    return NextResponse.next();
  }

  // 2. Extract the language code from the path
  const langCode = pathname.split('/')[1];

  // 3. Check if the language code is in our list
  if (locales.includes(langCode)) {
    // Language is valid, continue to the requested page
    return NextResponse.next();
  }

  // 4. If the language is not valid, rewrite to a 404 page
  // This keeps the invalid URL in the browser bar
  const url = request.nextUrl.clone();
  url.pathname = `/404`; // Assumes you have an app/404.tsx file
  return NextResponse.rewrite(url);
}

This logic checks every request. If the URL is /fr/about, langCode is fr, it's found in locales, and the request continues. If the URL is /xx/about, langCode is xx, it's not found, and the user is shown the 404 page without the app trying to process the invalid request.

4. Configure the middleware matcher

To make your middleware more efficient, you should tell it which paths to run on. We want it to run on page requests but skip static files and API routes.

Add a config object to the bottom of your middleware.ts file.

// middleware.ts

// ... (the middleware function from above)

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 regular expression tells the middleware to run on all paths except for the ones that are typically for static assets or API calls. This prevents unnecessary validation on every image, font, or data request.