Edited at

Accept-Languageの実装例

More than 5 years have passed since last update.

ここではリントの言葉で話せ


はじめに

Acecpt-LanguageはHTTPヘッダで「できれば日本語か英語で読みたいなー(チラッチラッ」みたいな感じで期待する言語を指定する仕組みのこと。

言語の指定にはIETF言語タグを利用するので、各自調べてほしい。 (これもおもしろい話題だが、今回は対象にしない)


設計

Webサイトをどう多言語化するかは難しい課題だが、とりあえず今回は以下のような戦略をとることにする。


  1. 言語が指定されなかった場合または*が指定されたら、 日本語のコンテンツを返す

  2. コンテンツが存在する言語(中国語zhを除く)が指定されたら、 その言語のコンテンツを返す

  3. コンテンツが存在しない言語が指定されたら、 英語のコンテンツを返す

  4. 中国語(zh)が指定された場合の追加条件


    1. 簡体/繁体のどちらかのコンテンツしかないときは、簡体字または繁体字の存在する方のコンテンツを返す

    2. 用字系(Script)にHansまたはHantが指定されたときは、繁体字または簡体字のコンテンツを返す

    3. 地域(Region)にTWが指定されたときは、繁体字のコンテンツを返す

    4. 上記に合致しなかった場合は 簡体字のコンテンツを返す



もちろんこれは全ての利用者の需要を充足できる戦略ではないだろう。


PHPでの実装

PHPにはLocaleモジュールがあり、これを活用できる。


はじめに

Localeモジュールはサブタグをパースする仕組みだがAccept-Languageの入力形式をよしなに扱ってくれるわけではない。

HTTP/1.1: Header Field Definitionsを読めば適当に分解できる。

手抜き実装だとこんな感じ。

<?php

var_export($_SERVER['HTTP_ACCEPT_LANGUAGE']);
// 'ja;q=1.0, en, zh-TW'

// めんどくさいので`q`値を無視して、順番だけを考慮する
$accept_languages = array_filter(array_map(function($tag){
return array_shift(array_map('trim', explode(';', $tag)));
},
explode(',', $_SERVER[$_SERVER['HTTP_ACCEPT_LANGUAGE']])));

var_export($accept_languages);
// array (
// 0 => 'ja',
// 1 => 'en',
// 2 => 'zh-TW',
// )

q値を考慮したきちんとした実装については後述する。


Locale::lookup()

Locale::lookupは一見して活用できるように見え、実際は サブタグを考慮しなければ、つまり4の条件を満たす必要がない場合 ならば利用できる。


public static string Locale::lookup ( array $langtag , string $locale [, bool $canonicalize = false [, string $default ]] )


$langtagにはコンテンツが存在する言語タグ一覧の配列を、$localeにはサブタグを指定する。

<?php

$locale = 'en-GB';
var_export(Locale::lookup(["zh-Hant-TW", "zh-Hans", "en", "ja"], $locale, true, "ja"));
// 'en'

$locale = 'zh-Hant';
var_export(Locale::lookup(["zh-Hant-TW", "zh-Hans", "en", "ja"], $locale, true, "ja"));
// 'ja'

下のzh-Hantzh-Hant-TWにマッチしないのはRFC 4647 - Matching of Language Tags #3.4. Lookupに紹介される “Example of a Lookup Fallback Pattern” の通りなのだけれど、そもそも微妙なので頼らない方が良い。


Locale::parseLocale()


public static array Locale::parseLocale ( string $locale )


Locale::parseLocaleはPHPらしからず、簡潔なインタフェイスで、きちんと動く。

このメソッドの素晴らしい特徴として、ISO 3166-1 alpha-3 (JPNのような3文字形式) を alpha-2 (JPのような2文字形式)に正規化してくれる。

あへて欠点を挙げるならば、これはあくまでサブタグのパーサなので、 RFC 4647 - Matching of Language Tags にあるようなワイルドカード(*)をサポートしないくらゐか。


ライブラリ

以上のようなことがめんどくさかったのでライブラリ化した。このライブラリはLocale::parseLocale()をラップして、Accept-Language形式からおてがるに最適な言語を選択するおてつだいをする。


  • quality指定(ja;q=1のような形式)をパースしてソート

  • 言語のワイルドカード(*-Latnのような形式)をサポート

$known_languages = ['ja', 'en', 'es', 'ko'];

$strategy = function (array $locale) use ($known_languages) {
$is_wildcard = isset($locale['language']) && $locale['language'] === '*';
if (empty($locale['language']) && !$is_wildcard) {
return null;
}
if ($is_wildcard || $locale['language'] === 'zh') {
if (!empty($locale['region']) && $locale['region'] == 'TW') {
return 'zh_tw';
}
if (!empty($locale['script']) && $locale['script'] == 'Hant') {
return 'zh_tw';
}
if ($locale['language'] === 'zh') {
return 'zh_cn';
}
}

if (in_array($locale['language'], $known_languages)) {
return $locale['language'];
}

return null;
};

$choise = \Teto\HTTP\AcceptLanguage::detect($strategy, $default);

$strategyLocale::parseLocale()形式の連想配列を受け取り、受理しない場合は 偽として評価される値 を、受理する場合は 真として評価される任意の値 を返せば良い。マッチするものがなければ $default をそのまま返す。


あとがき

すこし前に調べたところでは、あのzh.Wpすらワイルドカードはきちんと解釈してないので、僕個人としても、サポートがめんどくさいときはがんばらなくてもいいかなって気持ちも多少はある。とは言ってもがんばりたいときはがんばりたい。