Next.js App Router と next-intl を使用した多言語対応アプリケーションで、リロードなしの言語切り替えを実装する方法をまとめます。

環境

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

設定概要

localePrefix: ‘as-needed’ とは

next-intllocalePrefix: '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 から useRouterusePathname をインポートする理由:

  1. ロケール対応のパス : usePathname() はロケールプレフィックスを除いた純粋なパスを返す
  2. locale オプション : router.replace()router.push(){ locale: 'en' } オプションを渡せる
  3. リロードなし遷移 : クライアントサイドナビゲーションでスムーズに言語切り替え

クエリパラメータの維持

言語切り替え時に ?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 で言語切り替えを実装する際は、以下のポイントを押さえましょう:

  1. createNavigation(routing) でナビゲーションヘルパーを作成
  2. useRouter, usePathnamenext-intl/navigation からインポート
  3. router.replace(path, { locale }) でロケールを指定
  4. useSearchParams でクエリパラメータを維持