Fix next-intl Redirects Breaking Locale Routing: Quick Guide

Step-by-step fixes for locale-aware redirect failures in Next.js + next-intl; prevent misleading root-layout errors

·Updated on:·Matija Žiberna·
Fix next-intl Redirects Breaking Locale Routing: Quick Guide

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

No spam. Unsubscribe anytime.

Why Your next-intl Redirects Are Breaking Locale Routing: A Deep Dive Into the "not-found.tsx Doesn't Have a Root Layout" Error

I was building a multi-language e-commerce checkout flow with Next.js 15 and next-intl when my empty cart redirect started throwing a cryptic error: "not-found.tsx doesn't have a root layout." The error message made no sense—my layouts were properly structured, and the not-found.tsx file was in the correct location. After hours of debugging, I realized the real culprit wasn't missing layouts at all. It was hardcoded redirect paths that were breaking the entire locale-aware routing middleware chain. Once I understood the cascade failure, the fix became obvious. Here's exactly what was happening and how to prevent it.

Understanding the Locale Routing Cascade

Before we dig into the problem, you need to understand how next-intl's middleware and locale-aware routing works. It's elegant but fragile when you don't account for how it processes requests.

When you use next-intl with the App Router, the middleware intercepts every request matching your configured pattern. For a multi-language site with locales like sl (Slovenian), en (English), and ru (Russian), the middleware does something like this: it checks the request pathname, determines the locale, validates that the locale exists in your routing configuration, and then ensures the locale is properly set in the request context. This happens before your page components even render.

Here's the critical part: this routing validation is global and affects all subsequent routing operations, including redirects and layout resolution. If a redirect uses a path that doesn't match the locale-aware routing system's expectations, the entire chain breaks.

The Root Cause: Hardcoded Paths in Server Components

Let me show you the exact mistake I made in the checkout page:

// File: src/app/(frontend)/[locale]/narocilo/page.tsx (WRONG)
export default async function CheckoutPage({ params }: Props) {
  const { locale } = await params;
  const cart = await readCartWithMetadata();

  // This is the problem
  if (!cart.items || cart.items.length === 0) {
    redirect("/trznica"); // Hardcoded Slovenian path!
  }

  // Rest of the component...
}

When a user visits /en/checkout (English) with an empty cart, here's what happens:

  1. The middleware validates the request and sets the locale context to en
  2. The CheckoutPage component renders with locale = "en"
  3. The cart is empty, so it calls redirect('/trznica')
  4. The hardcoded /trznica path is Slovenian—it doesn't include the en locale prefix
  5. The middleware receives this redirect request for /trznica, which it doesn't recognize as a valid localized path
  6. The routing system can't match this to any locale-aware route, so it triggers the 404 fallback
  7. Next.js attempts to render not-found.tsx, but because the routing context is now broken, it can't properly resolve the layout hierarchy
  8. Result: "not-found.tsx doesn't have a root layout" error

The error message is misleading. The real problem isn't the layout file—it's that the redirect broke the locale routing context before the layout resolution could happen.

The Fix: Making Redirects Locale-Aware

The solution is straightforward but easy to miss. You need to include the locale in your redirect path and use the routing system's locale-aware pathnames.

Here's the corrected version:

// File: src/app/(frontend)/[locale]/narocilo/page.tsx (CORRECT)
export default async function CheckoutPage({ params }: Props) {
  const { locale } = await params;
  const cart = await readCartWithMetadata();

  if (!cart.items || cart.items.length === 0) {
    // Build locale-aware marketplace path
    const marketplacePath =
      locale === "en" ? "marketplace" : locale === "ru" ? "rynok" : "trznica";
    // Always include the locale in the redirect
    redirect(`/${locale}/${marketplacePath}`);
  }

  // Rest of the component...
}

Now when a user with an empty cart visits /en/checkout:

  1. The middleware validates and sets locale = "en"
  2. The component redirects to /${locale}/marketplace which resolves to /en/marketplace
  3. The middleware recognizes /en/marketplace as a valid locale-aware path
  4. The routing context remains intact, and the marketplace page loads correctly
  5. No 404, no broken layouts

The key insight is that you're not fighting against next-intl's routing system—you're working with it by respecting the locale prefix and using the correct localized pathway for each locale.

Client Components: A Better Pattern with useRouter

If you're doing navigation from client components (like handling form submissions or button clicks), you should use next-intl's useRouter hook instead of redirect(). The useRouter from @/i18n/routing is automatically locale-aware:

// File: src/app/(frontend)/[locale]/narocilo/AuthenticatedCheckout.tsx
"use client";

import { useRouter } from "@/i18n/routing";
import { useTranslations } from "next-intl";

export function AuthenticatedCheckout(
  {
    /* props */
  },
) {
  const router = useRouter();
  const t = useTranslations("checkout.authenticated");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      const result = await submitOrder({
        /* data */
      });

      if (result.success && result.orderNumber) {
        // This automatically handles locale routing
        router.push(`/narocilo/potrditev/${result.orderNumber}`);
      }
    } catch (err) {
      console.error("Error:", err);
    }
  };

  // Rest of component...
}

Notice that you're using the canonical internal pathname (/narocilo/potrditev/...), not the English translation (/checkout/confirmation/...). The useRouter hook from next-intl automatically translates these based on the current locale. This is much cleaner than building conditional paths manually.

Similarly, for links, use the Link component from @/i18n/routing:

// File: src/app/(frontend)/[locale]/narocilo/UnauthenticatedCheckout.tsx
'use client';

import { Link } from '@/i18n/routing';

export function UnauthenticatedCheckout({ /* props */ }) {
  return (
    <div>
      <Button asChild>
        <Link href="/prijava?redirect=/narocilo">
          Sign In
        </Link>
      </Button>
    </div>
  );
}

You're using the Slovenian paths (/prijava, /narocilo), but the Link component automatically translates them to the user's current locale:

  • English user: /login?redirect=/checkout
  • Slovenian user: /prijava?redirect=/narocilo
  • Russian user: /vkhod?redirect=/zakaz

This is configured in your routing file where you define the pathname mappings.

Server Components and Payload Queries: Don't Forget the Locale Parameter

While you're fixing redirects, make sure you're also passing the locale to any data-fetching operations. In my case, I was fetching products from Payload CMS without specifying the locale:

// File: src/app/(frontend)/[locale]/narocilo/page.tsx (WRONG)
const product = await payload.findByID({
  collection: "products",
  id: item.productId,
});

This should include the locale:

// File: src/app/(frontend)/[locale]/narocilo/page.tsx (CORRECT)
const product = await payload.findByID({
  collection: "products",
  id: item.productId,
  locale: locale as "sl" | "en" | "ru",
});

This ensures that product titles, descriptions, and other localized content come back in the correct language, without needing manual translation logic in your component.

Key Takeaways

The "not-found.tsx doesn't have a root layout" error in next-intl projects is almost always caused by redirects or routing operations that don't respect the locale-aware middleware. Here's what to watch for:

Use hardcoded paths that include the locale parameter when calling redirect() in server components. Always include the locale in the path: redirect('/${locale}/path') rather than redirect('/path').

Leverage next-intl's useRouter and Link components in client code. They automatically handle locale translation, so you can use canonical pathnames and let the routing system handle the rest.

When fetching data from a CMS or database that supports localization, pass the locale parameter to ensure content comes back in the correct language.

Test your redirects across different locales, not just the default. The error only manifests when you hit a locale-aware path with a hardcoded redirect to a different locale.

The underlying principle is simple: next-intl's routing middleware is powerful and automatic, but it requires every routing operation—server-side redirects, client-side navigation, and data fetching—to participate in the locale-aware system. Once you understand this, the error messages start to make sense, and the fixes become obvious.

Let me know in the comments if you've hit this issue yourself or if you have questions about implementing locale-aware routing. And subscribe for more practical development guides that solve real-world problems you'll actually encounter.

Thanks, Matija

0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.