How to validate locale parameters in URLs in React Router v7

Handle unsupported locale codes gracefully

Problem

When locale identifiers become part of URL structure, they transform into user input that can contain any arbitrary value. A user might manually type /xx/about, /gibberish/contact, or any other invalid locale code into the address bar. Without validation, the application must decide how to handle these invalid inputs. Allowing invalid locales to proceed can lead to missing translations, broken formatting, or runtime errors when i18n libraries attempt to load non-existent locale data. Silently falling back to a default locale without informing the user creates confusion about which language they are viewing. Displaying error boundaries or blank pages leaves users stranded with no clear path forward.

The challenge is compounded by the fact that locale validation must happen early in the request lifecycle, before components render and before translation data is loaded. If validation occurs too late, the application may have already attempted to fetch locale-specific resources or initialized i18n providers with invalid configuration, wasting resources and potentially causing cascading failures.

Solution

Validate the locale parameter in a route loader before the page renders. Loaders in React Router run before components mount, making them the ideal place to check whether the requested locale exists in your application's list of supported languages. If the locale is valid, allow the request to proceed normally. If the locale is invalid, immediately redirect the user to a safe fallback—either the same path with a valid default locale, or a dedicated not-found page that explains the issue.

This approach prevents invalid locales from reaching your components and i18n providers. By returning a redirect response from the loader, you leverage React Router's built-in navigation system to handle the error gracefully. The redirect happens on the server during SSR or on the client during navigation, ensuring consistent behavior across rendering strategies. Users receive immediate feedback through the URL change, and your application avoids attempting to load resources for non-existent locales.

Steps

1. Define your supported locales

Create a list of valid locale codes that your application supports. This list serves as the source of truth for validation.

export const SUPPORTED_LOCALES = ["en", "es", "fr", "de", "ja"] as const;

export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];

export function isValidLocale(locale: string): locale is SupportedLocale {
  return SUPPORTED_LOCALES.includes(locale as SupportedLocale);
}

This helper function provides type-safe validation and can be reused across loaders and other parts of your application.

2. Add validation to your locale-prefixed route loader

In the route module for your locale-prefixed pages, export a loader that checks the locale parameter and redirects if it's invalid.

import type { Route } from "./+types/page";
import { redirect } from "react-router";
import { isValidLocale } from "~/i18n/locales";

export async function loader({ params }: Route.LoaderArgs) {
  const { locale } = params;

  if (!locale || !isValidLocale(locale)) {
    return redirect("/en/not-found");
  }

  return { locale };
}

export default function Page({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <h1>Content in {loaderData.locale}</h1>
    </div>
  );
}

The loader extracts the locale from the URL, validates it, and redirects to a safe fallback if validation fails. If the locale is valid, it returns data that components can use.

3. Configure your routes with locale parameters

In your routes.ts file, define routes that include the locale as a dynamic segment.

import { type RouteConfig, route } from "@react-router/dev/routes";

export default [
  route(":locale/about", "./routes/about.tsx"),
  route(":locale/contact", "./routes/contact.tsx"),
  route(":locale/not-found", "./routes/not-found.tsx"),
] satisfies RouteConfig;

Each route with a :locale parameter will invoke its loader, where validation occurs before the component renders.

4. Create a not-found page for invalid locales

Build a dedicated page that explains the locale was not found and offers navigation options.

import { Link } from "react-router";
import { SUPPORTED_LOCALES } from "~/i18n/locales";

export default function NotFound() {
  return (
    <div>
      <h1>Language Not Found</h1>
      <p>The requested language is not supported.</p>
      <nav>
        <p>Choose a language:</p>
        <ul>
          {SUPPORTED_LOCALES.map((locale) => (
            <li key={locale}>
              <Link to={`/${locale}`}>{locale.toUpperCase()}</Link>
            </li>
          ))}
        </ul>
      </nav>
    </div>
  );
}

This page provides clear feedback and actionable next steps, helping users recover from the error without leaving your application.

5. Add a catch-all route for completely invalid paths

For URLs that don't match any defined route pattern, add a splat route at the end of your route configuration.

import { type RouteConfig, route } from "@react-router/dev/routes";

export default [
  route(":locale/about", "./routes/about.tsx"),
  route(":locale/contact", "./routes/contact.tsx"),
  route(":locale/not-found", "./routes/not-found.tsx"),
  route("*", "./routes/catch-all.tsx"),
] satisfies RouteConfig;

The splat route matches any path that doesn't match earlier routes, allowing you to handle completely malformed URLs separately from invalid locale codes.

6. Optionally redirect to a default locale instead of a not-found page

If you prefer to silently correct invalid locales rather than showing an error, redirect to the same path with a default locale.

import type { Route } from "./+types/page";
import { redirect } from "react-router";
import { isValidLocale } from "~/i18n/locales";

const DEFAULT_LOCALE = "en";

export async function loader({ params, request }: Route.LoaderArgs) {
  const { locale } = params;

  if (!locale || !isValidLocale(locale)) {
    const url = new URL(request.url);
    const newPath = url.pathname.replace(/^\/[^/]+/, `/${DEFAULT_LOCALE}`);
    return redirect(newPath);
  }

  return { locale };
}

This approach preserves the rest of the URL path while replacing only the invalid locale segment, providing a smoother user experience when the locale is the only issue.