troubleshooting2025-03-15Β·12 minΒ·149/348

Resolving next-intl Multilingual Routing Conflicts in Next.js

How to fix routing conflicts when using next-intl for internationalization with App Router in Next.js

Resolving next-intl Multilingual Routing Conflicts in Next.js

Introduction

Building multilingual applications with Next.js requires careful coordination between the routing system and the i18n library. The next-intl library is one of the most popular choices for internationalization in the Next.js ecosystem, but when combined with the App Router, it introduces routing complexities that can be difficult to debug.

After implementing Korean, English, and Portuguese translations for a client project, I encountered a series of routing conflicts where locale prefixes overlapped with dynamic route segments, middleware redirects created infinite loops, and static generation failed for non-default locales.

Environment

  • OS: Windows 11
  • Node.js: v20.10.0
  • Next.js: 14.0.4
  • next-intl: 3.3.1
  • Locales: en, ko, pt

Problem

The application exhibited multiple routing issues:

Error: Could not find the locale "" in the routing config
Warning: Detected possible locale routing conflict.
Route "/" matches both "/" and "/[locale]/"
Error: Maximum call stack size exceeded
(infinite redirect loop in middleware)

When navigating to http://localhost:3000/ko/about, the page rendered in English instead of Korean. The locale prefix was being stripped but the content was not being served from the correct locale directory.

Analysis

The root causes were:

1. Directory structure conflict. The app/[locale] directory structure created conflicts with static routes defined at the root level.

2. Missing middleware configuration. Without proper middleware, the locale prefix was not being correctly parsed and applied to content resolution.

3. Incomplete static path generation. generateStaticParams was not returning all locale combinations, causing static generation to fail for non-default locales.

4. Cookie and header conflicts. The browser's Accept-Language header was overriding the URL-based locale detection, creating inconsistent behavior.

Solution

Fix 1: Proper directory structure

app/
β”œβ”€β”€ [locale]/
β”‚   β”œβ”€β”€ layout.tsx
β”‚   β”œβ”€β”€ page.tsx
β”‚   β”œβ”€β”€ about/
β”‚   β”‚   └── page.tsx
β”‚   └── contact/
β”‚       └── page.tsx
β”œβ”€β”€ layout.tsx (root layout - minimal)
└── not-found.tsx

Fix 2: Configure next-intl plugin

// next.config.js
const createNextIntlPlugin = require('next-intl/plugin')

const withNextIntl = createNextIntlPlugin('./i18n.ts')

/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = withNextIntl(nextConfig)
// i18n.ts
import { getRequestConfig } from 'next-intl/server'

export default getRequestConfig(async ({ locale }) => {
  return {
    messages: (await import(`./messages/${locale}.json`)).default,
  }
})

Fix 3: Configure middleware for locale detection

// middleware.ts
import createMiddleware from 'next-intl/middleware'
import { locales, defaultLocale } from './i18n-config'

export default createMiddleware({
  locales,
  defaultLocale,
  localeDetection: false, // Disable automatic detection to avoid conflicts
})

export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(ko|en|pt)/:path*'],
}
// i18n-config.ts
export const locales = ['en', 'ko', 'pt'] as const
export type Locale = (typeof locales)[number]
export const defaultLocale: Locale = 'en'

Fix 4: Generate static params for all locales

// app/[locale]/about/page.tsx
import { getMessages } from 'next-intl/server'
import { useTranslations } from 'next-intl'
import { locales } from '@/i18n-config'

export function generateStaticParams() {
  return locales.map((locale) => ({ locale }))
}

export default async function AboutPage({
  params: { locale },
}: {
  params: { locale: string }
}) {
  const messages = await getMessages()
  const t = useTranslations('about', messages)

  return (
    

{t('title')}

{t('description')}

) }

Fix 5: Use the NextIntlClientProvider for client components

// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'

export default async function LocaleLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode
  params: { locale: string }
}) {
  const messages = await getMessages()

  return (
    
      
        
          {children}
        
      
    
  )
}
// components/LanguageSwitcher.tsx
'use client'

import { usePathname, useRouter } from 'next/navigation'
import { useLocale } from 'next-intl'

const locales = ['en', 'ko', 'pt']

export function LanguageSwitcher() {
  const locale = useLocale()
  const router = useRouter()
  const pathname = usePathname()

  const switchLocale = (newLocale: string) => {
    // Remove the current locale prefix
    const segments = pathname.split('/')
    segments[1] = newLocale
    router.push(segments.join('/'))
  }

  return (
    
  )
}

Lessons Learned

  1. Disable automatic locale detection when using URL-based locale routing. Accept-Language header detection conflicts with explicit locale prefixes in the URL.

  2. Always generate static params for all locales. Missing a locale in generateStaticParams will cause that locale to 404 during static export.

  3. Use the middleware matcher carefully. Match only the paths that need locale handling to avoid intercepting API routes and static files.

  4. Test locale switching from every page. Edge cases often appear when switching locales from nested routes or parameterized pages.

  5. Keep translation files organized by namespace:

messages/
β”œβ”€β”€ en/
β”‚   β”œβ”€β”€ common.json
β”‚   β”œβ”€β”€ about.json
β”‚   └── contact.json
β”œβ”€β”€ ko/
β”‚   β”œβ”€β”€ common.json
β”‚   β”œβ”€β”€ about.json
β”‚   └── contact.json
└── pt/
    β”œβ”€β”€ common.json
    β”œβ”€β”€ about.json
    └── contact.json

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.