Accept-Language とは何か
HTTPには Accept-Language というリクエストヘッダがある。
私の場合、以下の値が設定されている。
ja,zh-CN;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6,zh-TW;q=0.5
Webサイトが複数の自然言語に対応している場合、このヘッダを見てどの言語で表示するか決める訳だが、上の例だと、
- 日本語(ja)
- 大陸中国語(zh-CN)
- 中国語(zh)
- アメリカ英語(en-US)
- 英語(en)
- 台湾中国語(zh-TW)
の順序を指定したことになる。Accept-Language ヘッダからこの順序を導くプログラムを書いてみる。
雑な実装
優先順位は各言語タグ(zh-CN などの部分)の後ろにある q=0.9
のような重みで決定するのが正しいのだが、多くのブラウザではこの部分を無視しても解釈を間違えないよう言語タグをソートした状態でAccept-Languageヘッダの値としている。なので、雑な実装としては言語タグの登場順をそのまま優先順位とすればよい。
.js
function acceptLanguage(header) {
return header.match(/[a-z][\w-]+/g); // 言語タグだけ取り出す
}
真面目な実装
けれどこのやり方は以下のようなヘッダには使えない。
zh-TW ; q = 0.8 , en-US;q=0.9,en;q=0.90, ja,zh-CN,zh
先のものと優先順位は同じなのだが、重みで並び替えが必要な上に、重みの値に同じものがあり、その場合は先に出てきたものを優先しなければならない(ですよね)。さらには許されそうな箇所には空白文字を挿入している。
このような場合でも正しく優先順位を得れるよう、真面目に実装してみた。JavaScriptの sort()
は安定ではないらしいので、バケツソートを使いました。
.js
function acceptLanguage(header) {
let bucket ={}; // バケツソートの器
header = header.replace(/^\s+/g,'').replace(/\s+$/,'');
// 前後の空白を捨てる
for (lang of header.split(/\s*,\s*/)) { // , で分割
let [ key, value ] = lang.split(/\s*;\s*q\s*=\s*/);
// ;q= でキー・バリューに分割
value = +(value || 1); // バリューのデフォルト値は 1
if (isNaN(value)) continue; // 数値でない場合はスキップ
if (! bucket[`${value}`]) bucket[`${value}`] = [];
bucket[`${value}`].push(key); // バケツに入れる
}
let rv = []; // バケツソートして優先度を得る
for (let q of Object.keys(bucket).sort((x,y)=>(+y)-(+x))) {
rv = rv.concat(bucket[q]);
}
return rv;
}