R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 17 of 42Article12 min

Routing: Internationalization

A simple i18n pattern puts the language first: /en/courses, /hi/courses, /es/courses. The locale folder is a dynamic segment [locale] (or fixed folders en, hi).

Locale as a URL segment

A simple i18n pattern puts the language first: /en/courses, /hi/courses, /es/courses. The locale folder is a dynamic segment [locale] (or fixed folders en, hi).

Root redirects can send / to /en or detect Accept-Language in middleware/proxy and rewrite or redirect.

Real-life example: Locale routing is airport gates labeled EN, HI, ES — same flight (page), different language channel.

Folder structure with [locale]
app/
  [locale]/
    layout.tsx       # lang attribute, locale nav
    page.tsx         # /en  /hi
    courses/
      page.tsx       # /en/courses  /hi/courses

Basic i18n routing pattern

Validate the locale. If someone visits /xx/courses with an unknown code, redirect to a default language. Pass locale into dictionaries or next-intl / similar libraries later — start with folders and a simple messages map.

Real-life example: Validating locale is checking the passport language stamp before handing out the correct guidebook.

Tip: For production apps, consider a library (next-intl, next-i18next patterns adapted to App Router). The folder + middleware pattern above is the foundation those libraries build on.
app/[locale]/layout.tsx
import { notFound } from "next/navigation";
import type { ReactNode } from "react";

const locales = ["en", "hi", "es"] as const;

type Props = {
  children: ReactNode;
  params: Promise<{ locale: string }>;
};

export default async function LocaleLayout({ children, params }: Props) {
  const { locale } = await params;

  if (!locales.includes(locale as (typeof locales)[number])) {
    notFound();
  }

  return (
    <div lang={locale}>
      <header>Rishtaara · {locale.toUpperCase()}</header>
      {children}
    </div>
  );
}
Middleware — prefix default locale
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const locales = ["en", "hi", "es"];
const defaultLocale = "en";

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const hasLocale = locales.some(
    (l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`)
  );

  if (hasLocale) return NextResponse.next();

  // /courses → /en/courses
  const url = request.nextUrl.clone();
  url.pathname = `/${defaultLocale}${pathname}`;
  return NextResponse.redirect(url);
}

export const config = {
  matcher: ["/((?!_next|api|favicon.ico).*)"],
};
Tiny dictionary helper
const messages = {
  en: { hello: "Hello learner" },
  hi: { hello: "नमस्ते learner" },
  es: { hello: "Hola learner" },
} as const;

export function t(locale: keyof typeof messages, key: "hello") {
  return messages[locale][key];
}