The Go Blog
Goにおける言語とロケールのマッチング
はじめに
ユーザーインターフェースで複数の言語をサポートするウェブサイトなどのアプリケーションを考えてみましょう。ユーザーが好みの言語のリストを持って到着すると、アプリケーションはユーザーへの表示にどの言語を使用すべきかを決定する必要があります。これには、アプリケーションがサポートする言語とユーザーが好む言語の間の最良の一致を見つける必要があります。この投稿では、これがなぜ難しい決定であるか、そしてGoがどのように役立つかを説明します。
言語タグ
ロケール識別子とも呼ばれる言語タグは、使用されている言語や方言を機械が読み取れるようにした識別子です。これらに関する最も一般的な参照はIETF BCP 47標準であり、Goライブラリもこの標準に従っています。以下に、BCP 47言語タグとそのタグが表す言語または方言の例をいくつか示します。
| タグ | 説明 |
|---|---|
| en | 英語 |
| en-US | アメリカ英語 |
| cmn | 標準中国語 |
| zh | 中国語、通常は標準中国語 |
| nl | オランダ語 |
| nl-BE | フラマン語 |
| es-419 | ラテンアメリカスペイン語 |
| az, az-Latn | 両方ともラテン文字で書かれたアゼルバイジャン語 |
| az-Arab | アラビア文字で書かれたアゼルバイジャン語 |
言語タグの一般的な形式は、言語コード(上記の「en」、「cmn」、「zh」、「nl」、「az」)の後に、オプションのスクリプト(「-Arab」)、地域(「-US」、「-BE」、「-419」)、異体字(Oxford English Dictionaryの綴りのための「-oxendict」)、および拡張機能(電話帳ソートのための「-u-co-phonebk」)のサブタグが続きます。サブタグが省略された場合は、最も一般的な形式が想定されます。例えば、「az」は「az-Latn-AZ」を意味します。
言語タグの最も一般的な使用方法は、ユーザーの言語設定のリストに従って、システムがサポートする言語のセットから選択することです。例えば、アフリカーンス語を好むユーザーが、アフリカーンス語が利用できない場合に、システムがオランダ語を表示するのが最適であると判断するような場合です。このようなマッチングの解決には、相互の言語理解可能性に関するデータを参照する必要があります。
このマッチングの結果として得られるタグは、その後、翻訳、ソート順序、大文字/小文字変換アルゴリズムなどの言語固有のリソースを取得するために使用されます。これには別の種類のマッチングが伴います。例えば、ポルトガル語には特定のソート順序がないため、照合パッケージはデフォルトの、つまり「ルート」言語のソート順序にフォールバックする場合があります。
言語マッチングの厄介な性質
言語タグの扱いは難しいです。これは、人間の言語の境界が明確に定義されていないこと、そして進化する言語タグ標準の遺産によるものです。このセクションでは、言語タグを扱う上での厄介な側面をいくつか示します。
異なる言語コードを持つタグが同じ言語を示すことがある
歴史的および政治的な理由により、多くの言語コードは時間の経過とともに変更され、古いレガシーコードと新しいコードの両方を持つ言語が残されています。しかし、現在使用されている2つのコードでも同じ言語を指すことがあります。例えば、標準中国語の公式言語コードは「cmn」ですが、「zh」はこの言語を表す最も一般的に使用される指定子です。「zh」は、中国語のグループを識別するいわゆるマクロ言語のために公式に予約されています。マクロ言語のタグは、グループ内で最も話されている言語と互換的に使用されることがよくあります。
言語コードのみのマッチングでは不十分
例えば、アゼルバイジャン語(「az」)は、話されている国によって異なる文字で書かれます。ラテン文字の場合は「az-Latn」(デフォルトの文字)、アラビア文字の場合は「az-Arab」、キリル文字の場合は「az-Cyrl」です。「az-Arab」を単に「az」に置き換えると、結果はラテン文字になり、アラビア語形式しか知らないユーザーには理解できないかもしれません。
また、地域によって異なるスクリプトが暗示されることもあります。例えば、「zh-TW」と「zh-SG」はそれぞれ繁体字と簡体字漢の使用を意味します。別の例として、「sr」(セルビア語)はデフォルトでキリル文字ですが、「sr-RU」(ロシアで書かれるセルビア語)はラテン文字を意味します!同様のことがキルギス語や他の言語にも言えます。
サブタグを無視すると、ユーザーにギリシャ語を提示するのと同じことになりかねません。
最良の一致がユーザーによってリストされていない言語である可能性がある
ノルウェー語(「nb」)の最も一般的な書かれた形式は、デンマーク語に非常によく似ています。ノルウェー語が利用できない場合、デンマーク語が良い次善の選択肢となるかもしれません。同様に、スイスドイツ語(「gsw」)を要求するユーザーは、ドイツ語(「de」)が提示されても喜ぶ可能性が高いですが、その逆は決して当てはまりません。ウイグル語を要求するユーザーは、英語よりも中国語にフォールバックする方が喜ぶかもしれません。他にも多くの例があります。ユーザーが要求した言語がサポートされていない場合、英語にフォールバックするのが常に最善の策とは限りません。
言語の選択は翻訳以上のものを決定する
ユーザーがデンマーク語を第一希望、ドイツ語を第二希望として要求したとします。アプリケーションがドイツ語を選択した場合、ドイツ語の翻訳を使用するだけでなく、ドイツ語の照合(デンマーク語ではない)も使用する必要があります。そうしないと、例えば動物のリストで「Bär」が「Äffin」の前に並べ替えられてしまうかもしれません。
ユーザーの希望する言語からサポートされている言語を選択することは、握手のアルゴリズムのようなものです。まず、どのプロトコルで通信するか(言語)を決定し、セッション期間中はすべての通信でこのプロトコルに固執します。
言語の「親」をフォールバックとして使用するのは簡単ではない
アプリケーションがアンゴラポルトガル語(「pt-AO」)をサポートしているとします。照合や表示など、golang.org/x/text のパッケージには、この方言に特化したサポートがない場合があります。このような場合の正しい対処法は、最も近い親の方言にマッチングさせることです。言語は階層的に配置されており、それぞれの特定の言語にはより一般的な親があります。例えば、「en-GB-oxendict」の親は「en-GB」であり、その親は「en」であり、その親は未定義の言語「und」、つまりルート言語です。照合の場合、ポルトガル語には特定の照合順序がないため、照合パッケージはルート言語のソート順序を選択します。表示パッケージがサポートするアンゴラポルトガル語に最も近い親は、より明白なブラジルポルトガル語(「pt」)ではなく、ヨーロッパポルトガル語(「pt-PT」)です。
一般に、親子の関係は自明ではありません。いくつかの例を挙げると、「es-CL」の親は「es-419」であり、「zh-TW」の親は「zh-Hant」であり、「zh-Hant」の親は「und」です。サブタグを単に削除して親を計算すると、ユーザーにとって理解できない「方言」を選択してしまう可能性があります。
Goにおける言語マッチング
Goパッケージ golang.org/x/text/language は、言語タグのBCP 47標準を実装し、Unicode Common Locale Data Repository (CLDR) で公開されているデータに基づいて使用する言語を決定するためのサポートを追加しています。
以下は、ユーザーの言語設定とアプリケーションがサポートする言語をマッチングするプログラムのサンプルであり、以下で説明します。
package main
import (
"fmt"
"golang.org/x/text/language"
"golang.org/x/text/language/display"
)
var userPrefs = []language.Tag{
language.Make("gsw"), // Swiss German
language.Make("fr"), // French
}
var serverLangs = []language.Tag{
language.AmericanEnglish, // en-US fallback
language.German, // de
}
var matcher = language.NewMatcher(serverLangs)
func main() {
tag, index, confidence := matcher.Match(userPrefs...)
fmt.Printf("best match: %s (%s) index=%d confidence=%v\n",
display.English.Tags().Name(tag),
display.Self.Name(tag),
index, confidence)
// best match: German (Deutsch) index=1 confidence=High
}
言語タグの作成
ユーザーが指定した言語コード文字列から language.Tag を作成する最も簡単な方法は、language.Make を使用することです。これは、形式が不適切な入力からでも意味のある情報を抽出します。例えば、「en-USD」は、USD が有効なサブタグではないにもかかわらず、「en」という結果になります。
Make はエラーを返しません。エラーが発生した場合でもデフォルトの言語を使用するのが一般的なので、この方が便利です。エラーを手動で処理するには Parse を使用します。
HTTP の Accept-Language ヘッダーは、ユーザーが希望する言語を渡すためによく使用されます。ParseAcceptLanguage 関数は、それを言語タグのスライスとして解析し、優先順位で並べ替えます。
デフォルトでは、言語パッケージはタグを正規化しません。例えば、「圧倒的多数」で一般的な選択肢であるスクリプトを排除するというBCP 47の推奨に従いません。同様に、CLDRの推奨も無視します:「cmn」は「zh」に置き換えられず、「zh-Hant-HK」は「zh-HK」に簡略化されません。タグの正規化は、ユーザーの意図に関する有用な情報を破棄する可能性があります。正規化は代わりに Matcher で処理されます。プログラマが正規化を希望する場合は、さまざまな正規化オプションが利用可能です。
ユーザーが優先する言語とサポートされている言語のマッチング
Matcher は、ユーザーが優先する言語とサポートされている言語をマッチングします。言語のマッチングに関するあらゆる複雑な問題に対処したくない場合は、Matcher を使用することを強くお勧めします。
Match メソッドは、優先タグから選択されたサポートタグへユーザー設定(BCP 47拡張から)を渡すことができます。したがって、言語固有のリソースを取得するには、Match によって返されたタグを使用することが重要です。例えば、「de-u-co-phonebk」はドイツ語の電話帳順の並べ替えを要求します。この拡張機能はマッチングには無視されますが、照合パッケージによって対応するソート順バリアントを選択するために使用されます。
Matcher は、アプリケーションがサポートする言語(通常は翻訳が存在する言語)で初期化されます。このセットは通常固定されており、起動時にマッチャーを作成できます。Matcher は、初期化コストを犠牲にして、Match のパフォーマンスを向上させるように最適化されています。
言語パッケージは、サポートされているセットを定義するために使用できる、最も一般的に使用される言語タグの事前定義されたセットを提供します。ユーザーは通常、サポートされている言語に選択する正確なタグについて心配する必要はありません。例えば、AmericanEnglish(「en-US」)は、デフォルトでアメリカ英語であるより一般的なEnglish(「en」)と互換的に使用できます。マッチャーにとってはすべて同じです。アプリケーションは両方を追加することもでき、「en-US」用により具体的なアメリカのスラングを許可します。
マッチング例
次の Matcher とサポートされている言語のリストを考えてみましょう。
var supported = []language.Tag{
language.AmericanEnglish, // en-US: first language is fallback
language.German, // de
language.Dutch, // nl
language.Portuguese // pt (defaults to Brazilian)
language.EuropeanPortuguese, // pt-pT
language.Romanian // ro
language.Serbian, // sr (defaults to Cyrillic script)
language.SerbianLatin, // sr-Latn
language.SimplifiedChinese, // zh-Hans
language.TraditionalChinese, // zh-Hant
}
var matcher = language.NewMatcher(supported)
さまざまなユーザー設定に対して、このサポートされている言語のリストとのマッチングを見てみましょう。
「he」(ヘブライ語)というユーザー設定の場合、最良の一致は「en-US」(アメリカ英語)です。良い一致がないため、マッチャーはフォールバック言語(サポートリストの最初)を使用します。
「hr」(クロアチア語)というユーザー設定の場合、最良の一致は「sr-Latn」(ラテン文字のセルビア語)です。なぜなら、同じ文字で書かれれば、セルビア語とクロアチア語は相互に理解できるからです。
「ru, mo」(ロシア語、次にモルドバ語)というユーザー設定の場合、最良の一致は「ro」(ルーマニア語)です。なぜなら、モルドバ語は現在、公式には「ro-MD」(モルドバのルーマニア語)として分類されているからです。
「zh-TW」(台湾の標準中国語)というユーザー設定の場合、最良の一致は「zh-Hant」(繁体字で書かれた標準中国語)であり、「zh-Hans」(簡体字で書かれた標準中国語)ではありません。
「af, ar」(アフリカーンス語、次にアラビア語)というユーザー設定の場合、最良の一致は「nl」(オランダ語)です。どちらの優先言語も直接サポートされていませんが、オランダ語はフォールバック言語である英語よりもアフリカーンス語に著しく近い一致です。
「pt-AO, id」(アンゴラポルトガル語、次にインドネシア語)というユーザー設定の場合、最良の一致は「pt-PT」(ヨーロッパポルトガル語)であり、「pt」(ブラジルポルトガル語)ではありません。
「gsw-u-co-phonebk」(電話帳照合順のドイツ語)というユーザー設定の場合、最良の一致は「de-u-co-phonebk」(電話帳照合順のドイツ語)です。ドイツ語はサーバーの言語リストでスイスドイツ語の最良の一致であり、電話帳照合順のオプションは引き継がれています。
信頼度スコア
Goは、ルールベースの除外による粗い粒度の信頼度スコアリングを使用します。マッチはExact、High(正確ではないが既知の曖昧さはない)、Low(おそらく正しいマッチだがそうでない可能性もある)、またはNoに分類されます。複数のマッチがある場合、順に実行される一連の同点解消ルールがあります。複数の同等のマッチがある場合は、最初のマッチが返されます。これらの信頼度スコアは、例えば比較的弱いマッチを拒否するために役立つ場合があります。これらは、例えば言語タグから最も可能性の高い地域やスクリプトをスコアリングするためにも使用されます。
他の言語での実装では、よりきめ細かく、可変スケールのスコアリングを使用することがよくあります。Goの実装では、粗い粒度のスコアリングを使用することで、実装がよりシンプルで、保守性が高く、高速になり、より多くのルールを処理できるようになることがわかりました。
サポートされている言語の表示
golang.org/x/text/language/display パッケージを使用すると、多くの言語で言語タグに名前を付けることができます。また、タグをその言語で表示するための「Self」ネーマーも含まれています。
例
var supported = []language.Tag{
language.English, // en
language.French, // fr
language.Dutch, // nl
language.Make("nl-BE"), // nl-BE
language.SimplifiedChinese, // zh-Hans
language.TraditionalChinese, // zh-Hant
language.Russian, // ru
}
en := display.English.Tags()
for _, t := range supported {
fmt.Printf("%-20s (%s)\n", en.Name(t), display.Self.Name(t))
}
出力します
English (English)
French (français)
Dutch (Nederlands)
Flemish (Vlaams)
Simplified Chinese (简体中文)
Traditional Chinese (繁體中文)
Russian (русский)
2列目では、大文字化の違いに注目してください。これは、それぞれの言語の規則を反映しています。
まとめ
一見すると、言語タグはうまく構造化されたデータのように見えますが、人間の言語を記述しているため、言語タグ間の関係構造は実際には非常に複雑です。特に英語を話すプログラマは、言語タグの文字列操作以外何も使わずにアドホックな言語マッチングを記述したがる傾向があります。上記で説明したように、これはひどい結果を生む可能性があります。
Go の golang.org/x/text/language パッケージは、シンプルで使いやすい API を提供しながら、この複雑な問題を解決します。ぜひお試しください。
次の記事: Go 1.6 がリリースされました
前の記事: Go の6年間
ブログインデックス