はじめに
Acecpt-Language
はHTTPヘッダで「できれば日本語か英語で読みたいなー(チラッチラッ」みたいな感じで期待する言語を指定する仕組みのこと。
言語の指定にはIETF言語タグを利用するので、各自調べてほしい。 (これもおもしろい話題だが、今回は対象にしない)
設計
Webサイトをどう多言語化するかは難しい課題だが、とりあえず今回は以下のような戦略をとることにする。
- 言語が指定されなかった場合または
*
が指定されたら、 日本語のコンテンツを返す - コンテンツが存在する言語(中国語
zh
を除く)が指定されたら、 その言語のコンテンツを返す - コンテンツが存在しない言語が指定されたら、 英語のコンテンツを返す
- 中国語(
zh
)が指定された場合の追加条件 - 簡体/繁体のどちらかのコンテンツしかないときは、簡体字または繁体字の存在する方のコンテンツを返す
- 用字系(Script)に
Hans
またはHant
が指定されたときは、繁体字または簡体字のコンテンツを返す - 地域(Region)に
TW
が指定されたときは、繁体字のコンテンツを返す - 上記に合致しなかった場合は 簡体字のコンテンツを返す
もちろんこれは全ての利用者の需要を充足できる戦略ではないだろう。
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-Hant
がzh-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);
$strategy
はLocale::parseLocale()
形式の連想配列を受け取り、受理しない場合は 偽として評価される値 を、受理する場合は 真として評価される任意の値 を返せば良い。マッチするものがなければ $default
をそのまま返す。
あとがき
すこし前に調べたところでは、あのzh.Wpすらワイルドカードはきちんと解釈してないので、僕個人としても、サポートがめんどくさいときはがんばらなくてもいいかなって気持ちも多少はある。とは言ってもがんばりたいときはがんばりたい。