This article summarizes how to implement language switching without page reload in a multilingual application using Next.js App Router and next-intl.

Environment

  • Next.js 16 (App Router)
  • next-intl
  • TypeScript

Configuration Overview

localePrefix: ‘as-needed’

Using the localePrefix: 'as-needed' setting in next-intl, the default language does not have a URL prefix, while other languages do.

Example (when the default language is Japanese):

  • Japanese: /, /gallery, /viewer
  • English: /en, /en/gallery, /en/viewer

Implementation Steps

1. Middleware Configuration (Important)

next-intl uses middleware to handle locale routing. It is important to configure the matcher so that static files are not redirected by the middleware.

// middleware.ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './src/i18n/routing'

export default createMiddleware(routing)

export const config = {
  // Skip middleware for static files and API routes
  matcher: ['/((?!api|_next|.*\\..*).*)']
}

Note: .*\\..* excludes “paths containing dots.” This allows static files like /og-image.png to skip middleware and be accessed normally.

2. Routing Configuration

// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing'

export const routing = defineRouting({
  locales: ['ja', 'en'],
  defaultLocale: 'ja',
  localePrefix: 'as-needed'
})

3. Creating Navigation Helpers

Use createNavigation provided by next-intl to create locale-aware navigation helpers.

// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation'
import { routing } from './routing'

export const { Link, redirect, usePathname, useRouter } = createNavigation(routing)

4. Language Switching Component

// src/components/LanguageSwitcher.tsx
'use client'

import { useLocale } from 'next-intl'
import { useRouter, usePathname } from '@/src/i18n/navigation'
import { useSearchParams } from 'next/navigation'
import { routing } from '@/src/i18n/routing'

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

  const handleChange = (newLocale: string) => {
    // Preserve query parameters
    const queryString = searchParams.toString()
    const fullPath = queryString ? `${pathname}?${queryString}` : pathname

    router.replace(fullPath, { locale: newLocale })
  }

  return (
    <select
      value={locale}
      onChange={(e) => handleChange(e.target.value)}
      className="px-2 py-1 rounded text-sm cursor-pointer"
    >
      {routing.locales.map((loc) => (
        <option key={loc} value={loc}>
          {loc === 'ja' ? '日本語' : 'English'}
        </option>
      ))}
    </select>
  )
}

Key Points

Why Use next-intl Navigation

The reasons for importing useRouter and usePathname from next-intl/navigation instead of next/navigation:

  1. Locale-aware paths: usePathname() returns the pure path without the locale prefix
  2. locale option: You can pass { locale: 'en' } option to router.replace() or router.push()
  3. No-reload transitions: Smooth language switching via client-side navigation

Preserving Query Parameters

To preserve query parameters like ?videoId=xxx&gpxUrl=yyy during language switching, use useSearchParams.

const queryString = searchParams.toString()
const fullPath = queryString ? `${pathname}?${queryString}` : pathname

router.replace vs router.push

  • router.replace: Does not add a new entry to browser history (recommended)
  • router.push: Adds a new entry to browser history

Using replace for language switching prevents going back to the previous language with the back button.

Failure Patterns and Solutions

Pattern 1: Using window.location.href

// NG: Page reloads
window.location.href = newPath

This works but the entire page reloads, resulting in poor UX.

Pattern 2: Using useRouter from next/navigation

// NG: locale option not available
import { useRouter } from 'next/navigation'
router.push('/en/gallery')

You need to manually build the path, which doesn’t work well with localePrefix: 'as-needed'.

Pattern 3: Manual Path Construction

// NG: Complex and error-prone
const newPath = newLocale === 'ja' ? pathWithoutLocale : `/${newLocale}${pathWithoutLocale}`

Behavior changes depending on localePrefix settings, making it difficult to maintain.

Summary

When implementing language switching with next-intl, keep the following points in mind:

  1. Create navigation helpers with createNavigation(routing)
  2. Import useRouter and usePathname from next-intl/navigation
  3. Specify locale with router.replace(path, { locale })
  4. Preserve query parameters with useSearchParams