TanStack Start v1でユーザー言語設定を検出する方法

ブラウザ設定に基づく自動リダイレクト

問題

ウェブブラウザは、ユーザーの優先言語を優先順位に従って示すAccept-Languageヘッダーを全てのHTTPリクエストと共に送信します。このヘッダーはユーザーが最も快適に使用できる言語に関する貴重な情報を提供しますが、多くのアプリケーションはこれを完全に無視しています。代わりに、ユーザーの実際の好みに関係なく、すべての訪問者にデフォルト言語(通常は英語)を表示します。ユーザーは手動で言語切り替え機能を探さなければならず、アプリケーションがより良い初期選択をするために必要な情報をすでに持っているにもかかわらず、不必要な摩擦を生み出しています。

このパーソナライゼーションの機会の損失は、ブラウザの言語設定を明示的に構成した非英語話者にとって特に不満の原因となります。その結果、ユーザーが好みの言語でコンテンツを利用する前に、最初の印象が悪化し、追加の手順が必要になります。

解決策

ルートパスへのリクエストを傍受し、Accept-Languageヘッダーを調べてユーザーの優先言語を判断します。ヘッダーを解析して、アプリケーションがサポートする最優先言語を抽出します。一致するものが見つかった場合、ユーザーを適切にローカライズされたルートにリダイレクトします。ヘッダーにサポートされている言語が見つからない場合は、デフォルト言語のフォールバックにリダイレクトします。

このアプローチは、各言語が独自のパスプレフィックスを持つ明確なURL構造を維持しながら、ユーザーの好みを自動的に尊重します。リダイレクトはページがレンダリングされる前にサーバーサイドで行われるため、ユーザーは間違った言語が一瞬表示されることなく、好みの言語のコンテンツに直接アクセスできます。

手順

1. Accept-Languageヘッダーを解析するヘルパーを作成する

Accept-Languageヘッダーには、優先順位を示すオプションの品質値を持つ言語コードが含まれています。これらの言語を抽出し、優先順位でソートし、サポートされている言語リストから最初の一致を見つけるパーサーを構築します。

export function parseAcceptLanguage(
  header: string | null,
  supportedLocales: string[],
): string | null {
  if (!header) {
    return null;
  }

  const languages = header
    .split(",")
    .map((lang) => {
      const [code, qValue] = lang.trim().split(";q=");
      const quality = qValue ? parseFloat(qValue) : 1.0;
      return { code: code.toLowerCase(), quality };
    })
    .sort((a, b) => b.quality - a.quality);

  for (const { code } of languages) {
    const exactMatch = supportedLocales.find(
      (locale) => locale.toLowerCase() === code,
    );
    if (exactMatch) {
      return exactMatch;
    }

    const baseCode = code.split("-")[0];
    const baseMatch = supportedLocales.find((locale) =>
      locale.toLowerCase().startsWith(baseCode),
    );
    if (baseMatch) {
      return baseMatch;
    }
  }

  return null;
}

この関数はヘッダーをカンマで分割し、品質値を抽出し、優先順位でソートし、サポートされているロケールに対して完全一致とベース言語の一致の両方をチェックします。

2. サポートするロケールを定義する

アプリケーションがサポートするすべての言語をリストアップし、デフォルトのフォールバックとして使用する言語を指定する設定を作成します。

export const SUPPORTED_LOCALES = ["en", "fr", "de", "es", "ja"];
export const DEFAULT_LOCALE = "en";

これらの定数は言語設定を一元化し、アプリケーションの成長に合わせてサポートする言語の追加や削除を簡単にします。

3. ルートパス用のサーバールートハンドラを作成する

インデックスルートにサーバーサイドのGETハンドラを追加して、Accept-Languageヘッダーを読み取り、最適なロケールを決定し、ユーザーを適切な言語固有のパスにリダイレクトします。

import { createFileRoute, redirect } from "@tanstack/react-router";
import { getRequestHeaders } from "@tanstack/react-start/server";

export const Route = createFileRoute("/")({
  server: {
    handlers: {
      GET: async () => {
        const headers = getRequestHeaders();
        const acceptLanguage = headers.get("accept-language");

        const preferredLocale = parseAcceptLanguage(
          acceptLanguage,
          SUPPORTED_LOCALES,
        );

        const targetLocale = preferredLocale || DEFAULT_LOCALE;

        throw redirect({
          to: `/${targetLocale}`,
          statusCode: 302,
        });
      },
    },
  },
});

このハンドラは、ユーザーがルートパスにアクセスした際にサーバー上で実行され、ユーザーの言語設定を調べ、クライアントサイドのコードが実行される前に最適なロケールルートに即座にリダイレクトします。

4. ヘルパー関数をインポートする

ロケール設定と一緒に、パース用ヘルパーをルートファイルの先頭でインポートして利用できるようにします。

import { createFileRoute, redirect } from "@tanstack/react-router";
import { getRequestHeaders } from "@tanstack/react-start/server";
import {
  parseAcceptLanguage,
  SUPPORTED_LOCALES,
  DEFAULT_LOCALE,
} from "../lib/locale";

parseAcceptLanguage関数とロケール定数を共有モジュールに配置して、必要に応じてアプリケーション全体で再利用できるようにします。