Next.js App Router と next-intl を使用した多言語対応アプリケーションで、リロードなしの言語切り替えを実装する方法をまとめます。
環境
- Next.js 16 (App Router)
- next-intl
- TypeScript
設定概要
localePrefix: ‘as-needed’ とは
next-intl の localePrefix: 'as-needed' 設定を使用すると、デフォルト言語ではURLにプレフィックスが付かず、その他の言語のみプレフィックスが付きます。
例(デフォルト言語が日本語の場合):
- 日本語:
/,/gallery,/viewer - 英語:
/en,/en/gallery,/en/viewer
実装手順
1. Middleware設定(重要)
next-intl は middleware を使用してロケールのルーティングを処理します。静的ファイルが middleware によってリダイレクトされないよう、matcher の設定が重要です。
// 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|.*\\..*).*)']
}
注意 : .*\\..* は「ドットを含むパス」を除外します。これにより /og-image.png などの静的ファイルがmiddlewareをスキップし、正常にアクセスできるようになります。
2. ルーティング設定
// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing'
export const routing = defineRouting({
locales: ['ja', 'en'],
defaultLocale: 'ja',
localePrefix: 'as-needed'
})
3. ナビゲーションヘルパーの作成
next-intl が提供する createNavigation を使用して、ロケール対応のナビゲーションヘルパーを作成します。
// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation'
import { routing } from './routing'
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing)
4. 言語切り替えコンポーネント
// 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) => {
// クエリパラメータを維持
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>
)
}
ポイント解説
next-intl のナビゲーションを使う理由
next/navigation ではなく next-intl/navigation から useRouter と usePathname をインポートする理由:
- ロケール対応のパス :
usePathname()はロケールプレフィックスを除いた純粋なパスを返す - locale オプション :
router.replace()やrouter.push()に{ locale: 'en' }オプションを渡せる - リロードなし遷移 : クライアントサイドナビゲーションでスムーズに言語切り替え
クエリパラメータの維持
言語切り替え時に ?videoId=xxx&gpxUrl=yyy などのクエリパラメータを維持するために、useSearchParams を使用します。
const queryString = searchParams.toString()
const fullPath = queryString ? `${pathname}?${queryString}` : pathname
router.replace vs router.push
router.replace: ブラウザ履歴に新しいエントリを追加しない(推奨)router.push: ブラウザ履歴に新しいエントリを追加
言語切り替えでは replace を使うことで、戻るボタンで前の言語に戻ることを防ぎます。
失敗パターンと解決策
パターン1: window.location.href を使用
// NG: ページがリロードされる
window.location.href = newPath
動作はするが、ページ全体がリロードされるためUXが悪い。
パターン2: next/navigation の useRouter を使用
// NG: localeオプションが使えない
import { useRouter } from 'next/navigation'
router.push('/en/gallery')
手動でパスを構築する必要があり、localePrefix: 'as-needed' との相性が悪い。
パターン3: パスの手動構築
// NG: 複雑でバグが起きやすい
const newPath = newLocale === 'ja' ? pathWithoutLocale : `/${newLocale}${pathWithoutLocale}`
localePrefix の設定によって挙動が変わるため、保守が難しい。
まとめ
next-intl で言語切り替えを実装する際は、以下のポイントを押さえましょう:
createNavigation(routing)でナビゲーションヘルパーを作成useRouter,usePathnameはnext-intl/navigationからインポートrouter.replace(path, { locale })でロケールを指定useSearchParamsでクエリパラメータを維持