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:
- Locale-aware paths:
usePathname()returns the pure path without the locale prefix - locale option: You can pass
{ locale: 'en' }option torouter.replace()orrouter.push() - 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:
- Create navigation helpers with
createNavigation(routing) - Import
useRouterandusePathnamefromnext-intl/navigation - Specify locale with
router.replace(path, { locale }) - Preserve query parameters with
useSearchParams