ユーザーの言語選択を保持する

選択を記憶するためにクッキーを使用する

問題

ユーザーがサイト上で手動で「フランス語」を選択した場合、ブラウザを閉じて後でサイトのメインアドレス(例:example.com)を入力すると、アプリケーションはデフォルト(例:英語)または自動検出された言語に戻ってしまいます。この選択を記憶できないため、ユーザーは新しいセッションを開始するたびに言語切り替えを見つけて言語を再選択する必要があります。

解決策

ユーザーが言語を選択した際、その選択をクッキーに保存します。ミドルウェアでは、ユーザーがルートパス(/)にアクセスした時、Accept-Languageヘッダーをチェックする前にこのクッキーを確認します。有効なクッキーが見つかった場合、ブラウザのデフォルト設定を上書きして、ユーザーが選択した言語のルート(例:/fr)にリダイレクトします。

手順

1. 言語設定を定義する

サポートする言語、デフォルト言語、およびユーザーの設定を保存するクッキーの名前を格納する中央設定ファイルを作成します。

// i18n.config.ts

export const locales = ['en', 'es', 'fr'];
export const defaultLocale = 'en';
export const localeCookieName = 'NEXT_LOCALE';

2. 言語パーサーをインストールする

クッキーが設定されていない場合のフォールバックとして使用されるAccept-Languageヘッダーのパーサーが必要です。

npm install accept-language-parser

3. ミドルウェアを作成する

プロジェクトのルートにmiddleware.tsファイルを作成します。このミドルウェアはまずNEXT_LOCALEクッキーを確認します。見つからない場合は、Accept-Languageヘッダーの確認にフォールバックします。このロジックはルートパス(/)へのリクエストにのみ適用されます。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import parser from 'accept-language-parser';
import {
  locales,
  defaultLocale,
  localeCookieName,
} from './i18n.config';

function getPreferredLocale(request: NextRequest) {
  // 1. クッキーを確認
  const cookie = request.cookies.get(localeCookieName);
  if (cookie) {
    const locale = cookie.value;
    if (locales.includes(locale)) {
      return locale;
    }
  }

  // 2. Accept-Languageヘッダーを確認
  const acceptLang = request.headers.get('Accept-Language');
  if (acceptLang) {
    const bestMatch = parser.pick(locales, acceptLang, {
      loose: true,
    });
    if (bestMatch) {
      return bestMatch;
    }
  }

  // 3. デフォルトを返す
  return defaultLocale;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. リクエストがルートパスかどうかを確認
  if (pathname === '/') {
    // ユーザーの優先言語を取得(クッキーまたはヘッダーから)
    const bestLocale = getPreferredLocale(request);

    // 最適な言語パスにリダイレクト
    request.nextUrl.pathname = `/${bestLocale}`;
    return NextResponse.redirect(request.nextUrl);
  }

  // 2. その他のパスについては通常通り続行
  return NextResponse.next();
}

export const config = {
  matcher: [
    // 現時点ではルートパスでのみ実行
    '/',
    // 非ルートパスも一致させて通過させる必要がある
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

このミドルウェアのロジックは、ユーザーがサイトのルートにアクセスした際に、ユーザーの明示的な選択(クッキー)を暗黙的な設定(ブラウザヘッダー)よりも優先するようになりました。