【通信プロトコル】Phoenix アプリはおそらく Accept-Language を正しく処理していない

Phoenix アプリはおそらく Accept-Language を正しく処理していない

多くのPhoenix開発者は、アプリケーションの国際化(i18n)をGettextに任せて安心しきっています。しかし、Webアプリケーションの多言語対応において、「クライアントが何を求めているか」を正しく解釈することは、単にGettextの関数を呼び出す以上の複雑さを伴います。本稿では、PhoenixアプリケーションにおけるAccept-Languageヘッダーの取り扱いにおける落とし穴と、堅牢な多言語対応を実現するためのアーキテクチャを解説します。

Accept-Languageの仕様と現実の乖離

RFC 7231で定義されているAccept-Languageヘッダーは、クライアントが好む言語の優先順位を表現するためのものです。例えば、「Accept-Language: ja, en-US;q=0.9, en;q=0.8」というヘッダーは、ユーザーが日本語を最も好み、次点で米英語、その次に一般的な英語を許容することを示しています。

多くのPhoenix開発者が犯す間違いは、このヘッダーを単なる文字列として扱い、単純なマッチングで済ませてしまうことです。「Phoenix.Controller.get_session」や「Plug.Conn」から得られるヘッダーをそのままGettextのロケール設定に放り込むだけでは、以下のような問題が発生します。

1. クライアントが送信する言語タグが、サーバー側でサポートしている言語と一致しない場合、アプリケーションがデフォルト言語にフォールバックできず、エラーや予期せぬ挙動を引き起こす。
2. ユーザーのプロファイル設定(データベース上の設定)とブラウザのヘッダーが競合した際、どちらを優先すべきかのロジックが欠如している。
3. クローラーやAPIクライアントが送信する不正なフォーマットのヘッダーに対して、アプリケーションが脆弱である。

Plugによる標準化されたロケール解決の実装

Phoenixにおいて、リクエストのライフサイクルの中でロケールを決定する最も適切な場所は、Plugのパイプライン内です。Controllerの中で毎回ロケールを解決するのはDRY原則に反します。以下に、堅牢なロケール解決を行うためのPlug実装例を示します。

defmodule MyAppWeb.Plugs.SetLocale do
  import Plug.Conn
  import Gettext

  @supported_locales ["ja", "en"]
  @default_locale "ja"

  def init(opts), do: opts

  def call(conn, _opts) do
    locale =
      get_locale_from_user(conn) ||
      get_locale_from_header(conn) ||
      @default_locale

    Gettext.put_locale(MyAppWeb.Gettext, locale)
    assign(conn, :locale, locale)
  end

  defp get_locale_from_header(conn) do
    case get_req_header(conn, "accept-language") do
      [header | _] ->
        header
        |> String.split(",")
        |> Enum.map(fn lang ->
          lang
          |> String.split(";")
          |> hd()
          |> String.split("-")
          |> hd()
          |> String.trim()
        end)
        |> Enum.find(&(&1 in @supported_locales))
      _ ->
        nil
    end
  end

  defp get_locale_from_user(conn) do
    # セッションやトークンからユーザー設定を取得するロジック
    # conn.assigns[:current_user] などを使用
    nil
  end
end

この実装では、ブラウザの優先順位を尊重しつつ、サーバー側でサポートしている言語のみを許可するホワイトリスト方式を採用しています。これにより、未知の言語が送られてきた場合でも安全にデフォルト言語へフォールバックされます。

実務における注意点とアーキテクチャの最適化

実務においては、単にロケールを設定するだけでは不十分なケースが多々あります。特に以下の点に注意してください。

1. キャッシュの汚染(Varyヘッダー)
CDNやNginxなどのリバースプロキシを利用している場合、Accept-Languageに基づいてコンテンツを出し分けるならば、必ず「Vary: Accept-Language」ヘッダーを付与する必要があります。これを行わないと、日本語ユーザーのキャッシュが英語ユーザーに配信されるという悲劇的な事態を招きます。

2. URL構造との整合性
最近のトレンドとして、URLのパス(例: /ja/about, /en/about)でロケールを表現する手法があります。この場合、Accept-Languageヘッダーはあくまで「初回アクセス時」や「言語設定が未指定のユーザー」に対するヒントとして扱うべきです。パスにロケールが含まれている場合は、ヘッダーよりもパスを優先する設計にしてください。

3. ユーザー設定の永続化
ブラウザのAccept-Languageは、ユーザーがOSの設定を変更しない限り固定です。しかし、Webアプリケーション上でユーザーが「英語表示」を選択した場合、その設定はデータベースに保存し、ログイン時にはブラウザのヘッダーよりも優先させるべきです。これを怠ると、ユーザー体験を著しく損ないます。

4. Gettextの動的ロケール設定のオーバーヘッド
Gettextのロケール設定は、プロセス辞書(Process Dictionary)に保存されます。Phoenixは各リクエストを個別のプロセスで処理するため、この設計は非常に効率的ですが、非同期処理(Task.asyncなど)を行う場合は、ロケール情報が引き継がれないことに注意してください。別プロセスでGettextを使う場合は、明示的にロケールを渡すか、`Gettext.put_locale`を再実行する必要があります。

まとめ:多言語対応は「信頼」の構築

Accept-Languageヘッダーを正しく処理するということは、単に文字列をパースすることではありません。ユーザーが「どの言語で情報を得たいか」という意図を正確に読み取り、それをアプリケーション全体のコンテキストとして反映させることです。

多くの開発者がこのプロセスを疎かにし、デフォルト言語の固定や場当たり的な判定で済ませていますが、真にプロフェッショナルなPhoenixアプリケーションを目指すのであれば、以下のステップを徹底してください。

・Plugによる一元管理:すべてのリクエストでロケール解決を確実に実行する。
・ホワイトリストの厳格化:サポート外の言語に対する安全なフォールバックを実装する。
・優先順位の明確化:データベース(ユーザー設定)>URLパス>Accept-Languageヘッダーの順で評価する。
・キャッシュ戦略の適正化:Varyヘッダーの制御を忘れない。

Phoenixの強力な並行処理能力とGettextの柔軟性を活かせば、複雑な多言語対応も非常にクリーンに実装可能です。あなたのアプリケーションが、世界中のユーザーにとって「母国語のように自然に使える」存在になることを期待しています。

コメント

タイトルとURLをコピーしました