Twitter GitHub

The Go Blog 日本語訳

The Go Blogの日本語訳を公開しています。修正は https://github.com/ymotongpoo/goblog-ja/ まで。

Goにおける言語とロケールのマッチング (Language and Locale Matching in Go)

Goにおける言語とロケールのマッチング

Language and Locale Matching in Go By Marcel van Lohuizen

はじめに

ウェブサイトのような、ユーザーインターフェースで複数の言語をサポートするアプリケーションを考えてみましょう。 ユーザーが望む言語のリストがある場合、アプリケーションはどの言語を表示すべきか決めなければなりません。 このとき、アプリケーションがサポートする言語とユーザーが好む言語の間で最適な組み合わせを選ばなければなりません。 この記事ではなぜこの決定が難しいか、そしてどうやって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”)、変数の副タグ(オックスフォード英語大辞典でのスペルのための “-oxendict”)、あるいは 拡張副タグ(電話帳順のための “-u-co-phonebk”)が続きます。 もっとも一般的な形式は副タグが省略された形、たとえば “az-Latn-AZ” であれば “az” です。

言語タグがもっとも使われる場所は、システムがサポートしている言語の一覧からユーザーが好みの言語を選択するときでしょう。 たとえば、(アフリカーンス語が選択できない場合に)アフリカーンス語を望んでいるユーザーに対しシステムはオランダ語を表示するという決定をする場合です。 このような対応は言語間の包含性に関するデータを参照するというプロセスが関わってきます。 この対応から得られたタグは、その後言語特有のリソース、例えば翻訳、並び順、タイトルなどの大文字小文字を自動で変えるアルゴリズムなどを 取得するために使われます。これらの処理はまた別の対応を必要とします。たとえば、ポルトガル語には決まった並び順がないため、 並び順を処理するパッケージはデフォルトのもの、すなわち「ルート」の言語の並び順にフォールバックすることになるでしょう。

言語の対応に関する厄介な性質

言語タグを扱うには注意が必要です。その理由は、自然言語の境界があいまいであること、言語タグの標準の発展の歴史によるもの、などが挙げられます。 この節では言語タグを扱う際の厄介な側面をいくつかご紹介します。

異なる言語コードのタグが同じ言語を表している

歴史的かつ政治的な理由で、多くの言語コードは時とともに変遷していて、古い言語コードと新しい言語コードが共存していました。 しかし、古いものも新しいものも同じ言語を参照しています。たとえば、標準中国語を表す公式な言語コードは “cmn” ですが、 “zh” がずっと 広く使われています。 “zh” は公式にはいわゆるマクロ言語として中国語全般を表すために予約されています。 マクロ言語用の言語タグは、しばしばその言語群の中でもっとも良く話されている言語のコードとして使われます。

言語コードの対応だけでは不十分

たとえばアゼルバイジャン語(“az”)は国によって異なる文字で書かれています。”az-Latn” はラテン文字、”az-Arab” はアラビア文字、 “az-Cyrl” はキリル文字です。もし “az-Arab” を単純に “az” に置き換えてしまうと、結果としてラテン文字が表示されて、 アラビア文字で書かれたアゼルバイジャン語しか理解できない人には意味のないものになってしまうでしょう。

異なる地域であることも異なる文字が使われる可能性を示唆します。たとえば “zh-TW” と “zh-SG” はそれぞれ繁体字中国語、 簡体字中国語であることを示しています。他の例としては “sr” (セルビア語)はデフォルトではキリル文字ですが、 “sr-RU” (ロシアで書かれるセルビア語)はラテン文字なのです!似た例はキルギスや他の言語でも見られます。

副タグを無視すると、ユーザーにとって意味の分からないものが表示されるかもしれません。

ユーザーが選択していない言語が最適な場合もある

もっとも普及したノルウェー語(“nb”)は、デンマーク語と見分けが付きません。もしノルウェー語が選択できないのであれば、 デンマーク語が次点として選ばれるべきでしょう。同様に、スイスのドイツ語(“gsw”)を選択したユーザーに ドイツ語(“de”)が表示されても問題無いでしょう。しかし、その逆はまったく当てはまりません。 ウイグル語を選択したユーザーに対しては英語よりも中国語にフォールバックさせたほうが良いでしょう。 ユーザーが選択した言語がサポートされていない場合に、必ずしも英語にフォールバックすることが最適ではないのです。

翻訳よりも言語の選択の方が重要

あるユーザーがデンマーク語を第1の選択肢に、ドイツ語を第2の選択肢にしたとしましょう。 もしアプリケーションがドイツ語を選択するならば、コンテンツをドイツ語に翻訳するだけではなく、照合処理も(デンマーク語ではなく)ドイツ語の ルールを使わなければいけません。そうしないと、たとえば動物のリストを並べるときに “Bär” (日:クマ)が “Äffin” (日:メスザル)よりも 前に来てしまいます。

ユーザーが選択した言語の中からサポートする言語を選択するというのは、ハンドシェイクアルゴリズムに似ています。 まずあなたがやり取りするためのプロトコル(言語)を決定し、そのあとはセッションが続く間はすべてのコミュニケーションを そのプロトコルで行うことに終始します。

フォールバックに「親」の言語を使うのは簡単なことではない

あなたのアプリケーションがアンゴラのポルトガル語(“pt-AO”)をサポートしているとしましょう。 golang.org/x/text 内のパッケージでは、この方言に対しての、照合や表示といった、特別なサポートはありません。 そのような状況で採るべき正しい行動は、もっとも近い親方言を対応させることです。言語は階層的な関係にあり、特定の言語には、 より一般的な親の方言があります。たとえば、“en-GB-oxendict” の親は “en-GB” であり、その親は “en” で、さらにその親は未定義の言語を表す “und” となります。これはルート言語としても知られています。照合においては、ポルトガル語には特定の並び順がないため、collateパッケージは ルート言語の並び順を選択するでしょう。displayパッケージでアンゴラのポルトガル語に最も近い親は、ヨーロッパポルトガル語(“pt-PT”)で、 よりわかりやす “pt” ではありません。こちらはブラジルのポルトガル語を表します。

一般的に、親子関係は簡単ではありません。もう少し例を挙げましょう。 “es-CL” の親は “es-419” で、 “zh-TW” の親は “zh-Hant”、その親は “und” です。 祖語を選択しようと単純に副タグを取っただけの処理をすると、ユーザーが理解できない「方言」を選択してしまうかもしれません。

Goでの言語のマッチング

Goのパッケージ golang.org/x/text/language は言語タグに関するBCP 47の標準を実装し、 共通ロケールデータレポジトリ(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”の形に直してくれます。

language.Make はエラーを返しません。エラーを返したとしてもデフォルト言語を使うのが普通なので、エラーを返さないというのは より便利な形と言えます。自分でエラーを扱うのであれば Parse を使いましょう。

HTTPの Accept-Language ヘッダはしばしばユーザーが望む言語を渡す方法として使われます。 ParseAcceptLanguage 関数は そのヘッダをパースして言語タグのスライスへ変換し、望ましい順番に並べます。

デフォルトでは、languageパッケージはタグを正規化しません。たとえば、言語タグが「圧倒的大多数」によく使われているのであれば、 BCP 47が推奨する形に言語タグの文字列を消したりしません。同様に、CLDRの推奨する形も無視します。 つまり “cmn” は “zh” に置換されませんし、“zh-Hant-HK” は “zh-HK” に簡約化されません。言語タグを正規化してしまうと、 ユーザーの意図などの役に立つ情報まで捨ててしまいかねません。かわりに正規化は Matcher で扱われます。 プログラマが望めば、正規化に関するオプションもすべて利用できます。

ユーザーが望む言語をサポートしている言語に対応させる

Matcher はユーザーが望む言語をサポートしている言語に対応させます。ユーザーは言語の対応に関するすべてのややこしい事柄に 関わりたくないのであれば、 Matcher が選んだ言語を利用すること強く推奨します。

Match メソッドはユーザーが望む言語タグを(BCP 47の拡張からなる)ユーザー設定を経由してサポートされている言語タグへと渡します。 それゆえ、Match から返されたタグを言語特有のリソースを取得するために使うことが重要です。たとえば “de-u-co-phonebk” は ドイツ語での電話帳の並び順を要求します。この拡張は対応時には無視されますが、collateパッケージがそれぞれの並び順の変数を選択するときに使われます。

Matcher はアプリケーションがサポートする言語で初期化されます。これらの言語は通常翻訳先となる言語です。 この言語のセットは通常は固定されていて、これによって Matcher がアプリケーション起動時に作成できます。 MatcherMatch の初期化コストを下げパフォーマンスを改善するために最適化されています。

languageパッケージはもっともよく使用される言語タグの事前定義済みセットを提供しています。これはアプリケーションがサポートする言語セットとして 使うことが出来ます。ユーザーは一般的に、サポートされる言語にピッタリと合致するタグを選ばなければいけないという心配をしなくてすみます。 たとえば、アメリカ英語(“en-US”)は、デフォルトでアメリカ英語となる、より一般的な英語(“en”)と相互互換的に使うことが出来ます。 Matcher にも同様のことが当てはまります。アプリケーション側では両方の言語に対応してもよく、特有のアメリカのスラングを “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”(アメリカ英語)です。 良い対応がないため、matcherはフォールバック先の言語(サポート言語のリストの先頭)を使用します。

ユーザー設定で “hr”(クロアチア語)を選択した場合、最適な対応は “sr-Latn”(ラテン文字で書かれたセルビア語)です。 理由は、同じ文字で書かれている場合、セルビア語とクロアチア語はお互い理解できる言語だからです。

ユーザー設定で “ru, mo”(ロシア語、次点でモルダビア語)を選択した場合、最適な対応は “ro”(ルーマニア語)です。 理由はモルダビア語は正規化すると “ro-MD”(モルドバでのルーマニア語)に分類されるからです。

ユーザー設定で “zh-TW”(台湾での標準中国語)を選択した場合、最適な対応は”zh-Hans”(簡体字で書かれた標準中国語)ではなく、 “zh-Hant”(繁体字で書かれた標準中国語)です。

ユーザー設定で ”af, ar”(アフリカーンス語、次点でアラビア語)を選択した場合、最適な対応は “nl”(オランダ語)です。 どちらの設定も直接はサポートされていませんが、オランダ語は、フォールバック言語の英語の設定された言語に対する近さよりも、 アフリカーンス語にずっと近く対応しています。

ユーザー設定で “pt-AO, id”(アンゴラのポルトガル語、次点でインドネシア語)を選択した場合、最適な対応は “pt-PT”(ヨーロッパのポルトガル語) であり、 “pt”(ブラジルのポルトガル語)ではありません。

ユーザー設定で “gsw-u-co-phonebk”(スイスのドイツ語で、照合は電話帳の順を使用)を選択した場合、最適な対応は “de-u-co-phonebk(ドイツ語で、照合は電話帳の順を使用)となります。ドイツ語はサーバーの言語リスト内ではスイスのドイツ語に最適な対応であり、 電話帳順の照合というオプションは持ち越されます。

信頼スコア

Goではルールベースの消去法を使った粗い信頼スコアを使っています。 言語の対応は Exact、High(Exactではないが明確な曖昧さはない)、Low(おおよそ対応しているかもしれないし、していないかもしれない)、Noに 分類されます。複数の言語が同等に対応した場合には、タイブレークのルールが順番に実行されます。複数の言語が同等に対応した場合には、 最初の言語が返されます。これらの信頼スコアは、たとえば比較的弱い対応を拒否するときに役に立ちます。 ほかにも、たとえば言語タグから最適な地域や文字のスコアを付けるときにも使われます。

他のプログラミング言語での実装では、より細やかな、変数スケールのスコアリングをしています。 私たちは、Goでの実装は粗いスコアリングにすることで、より簡潔な実装で、よりメンテナンスしやすく、より速くなることがわかり、 それにより、より多くのルールを扱えることとなりました。

サポートされた言語を表示する

golang.org/x/text/language/display パッケージは言語タグを たくさんの言語で名前をつけることが出来ます。このパッケージには「自分自身」のタグ名を自分の言語で表示できるようにもなっています。

たとえば

    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プログラミングを楽しんでください。

By Marcel van Lohuizen