Goブログ

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

Marcel van Lohuizen
2016年2月9日

はじめに

ウェブサイトなどのアプリケーションで、ユーザーインターフェースを複数言語でサポートすることを考えてみましょう。ユーザーが優先する言語のリストを提示すると、アプリケーションはユーザーへの提示に使用する言語を決定する必要があります。これには、アプリケーションがサポートする言語とユーザーが優先する言語の最適な一致を見つけることが必要です。この投稿では、これがなぜ難しい決定であるか、そして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」のようにサブタグが省略されている場合は、最も一般的な形式が想定されます。

言語タグの最も一般的な用途は、ユーザーの言語設定のリストに従って、システムでサポートされている言語のセットから選択することです。たとえば、アフリカーンス語が利用できない場合、アフリカーンス語を好むユーザーにはオランダ語を表示するのが最適であると判断します。このようなマッチングの解決には、相互言語の理解に関するデータを参照する必要があります。

このマッチングの結果得られるタグは、その後、翻訳、ソート順、大文字小文字のアルゴリズムなど、言語固有のリソースを取得するために使用されます。これには、異なる種類のマッチングが関与します。たとえば、ポルトガル語に固有のソート順がない場合、照合パッケージはデフォルトの言語である「ルート」言語のソート順にフォールバックする可能性があります。

言語マッチングの複雑さ

言語タグの処理は困難です。これは、人間の言語の境界が明確に定義されていないことと、進化する言語タグ標準のレガシーによる部分があります。このセクションでは、言語タグの処理におけるいくつかの複雑な側面を示します。

異なる言語コードを持つタグは、同じ言語を示す場合があります

歴史的および政治的な理由から、多くの言語コードは時間の経過とともに変化しており、言語には古いレガシーコードと新しいコードの両方が残されています。しかし、2つの現在のコードも、同じ言語を参照している可能性があります。たとえば、標準語の公式言語コードは「cmn」ですが、「zh」がこの言語の最も一般的に使用される指定子です。コード「zh」は正式にはいわゆるマクロ言語用に予約されており、中国語のグループを識別しています。マクロ言語のタグは、多くの場合、グループ内で最も話されている言語と交換可能に使用されます。

言語コードだけでは不十分です

たとえば、アゼルバイジャン語(「az」)は、話されている国に応じて異なるスクリプトで書かれています。「az-Latn」はラテン文字(デフォルトのスクリプト)、「az-Arab」はアラビア文字、「az-Cyrl」はキリル文字です。「az-Arab」を「az」に置き換えると、結果はラテン文字になり、アラビア文字しか知らないユーザーには理解できない可能性があります。

また、異なる地域は異なるスクリプトを意味する場合があります。たとえば、「zh-TW」と「zh-SG」は、それぞれ繁体字中国語と簡体字中国語の使用を意味します。別の例として、「sr」(セルビア語)はデフォルトでキリル文字ですが、「sr-RU」(ロシアで書かれたセルビア語)はラテン文字を意味します!キルギス語などの他の言語でも同様のことが言えます。

サブタグを無視すると、ギリシャ語が表示される可能性があります。

最適な一致は、ユーザーがリストに挙げていない言語である可能性があります

ノルウェー語の最も一般的な書かれた形式(「nb」)は、デンマーク語と非常によく似ています。ノルウェー語が利用できない場合、デンマーク語が良い第2の選択肢となる可能性があります。同様に、スイスドイツ語(「gsw」)を要求するユーザーは、ドイツ語(「de」)が表示されても満足する可能性がありますが、その逆は当てはまりません。ウイグル語を要求するユーザーは、英語よりも中国語にフォールバックする方が満足する可能性があります。他にも多くの例があります。ユーザーが要求した言語がサポートされていない場合、英語にフォールバックするのは多くの場合最善策ではありません。

言語の選択は、翻訳以上のものを決定します

ユーザーがデンマーク語を第1選択、ドイツ語を第2選択として要求するとします。アプリケーションがドイツ語を選択した場合、ドイツ語の翻訳を使用するだけでなく、ドイツ語(デンマーク語ではない)の照合も使用する必要があります。そうでなければ、たとえば、動物のリストが「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関数は、それを優先順位に従ってソートされた言語タグのスライスに解析します。

デフォルトでは、languageパッケージはタグを正規化しません。たとえば、「圧倒的多数」で一般的な選択肢である場合にスクリプトを削除するというBCP 47の推奨事項に従いません。同様に、CLDRの推奨事項も無視します。「cmn」は「zh」に置き換えられず、「zh-Hant-HK」は「zh-HK」に簡略化されません。タグを正規化すると、ユーザーの意図に関する有用な情報が失われる可能性があります。正規化はMatcherで処理されます。プログラマーがそれでも正規化したい場合、正規化オプションの完全な配列が利用可能です。

ユーザーの優先言語とサポート言語のマッチング

Matcherは、ユーザーの優先言語とサポート言語を照合します。言語の複雑な処理を避けたい場合は、Matcherを使用することを強くお勧めします。

Matchメソッドは、優先タグから選択されたサポートタグに、ユーザー設定(BCP 47拡張機能から)を渡すことができます。したがって、Matchによって返されたタグを使用して言語固有のリソースを取得することが重要です。たとえば、「de-u-co-phonebk」は、ドイツ語の電話帳順序を要求します。拡張機能はマッチングでは無視されますが、照合パッケージによって対応するソート順のバリアントを選択するために使用されます。

Matcherは、アプリケーションでサポートされている言語(通常は翻訳が存在する言語)を使用して初期化されます。このセットは通常固定されているため、起動時にMatcherを作成できます。Matcherは、初期化コストを犠牲にしてMatchのパフォーマンスを向上させるように最適化されています。

languageパッケージは、サポートセットの定義に使用できる、最も一般的に使用される言語タグの事前定義されたセットを提供します。ユーザーは、サポート言語に選択する正確なタグについて心配する必要はありません。たとえば、AmericanEnglish(「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-Hant」(繁体字中国語で書かれた標準語)であり、「zh-Hans」(簡体字中国語で書かれた標準語)ではありません。

ユーザー設定が「af, ar」(アフリカーンス語、次にアラビア語)の場合、最適な一致は「nl」(オランダ語)です。いずれの言語も直接サポートされていませんが、オランダ語はアフリカーンス語に、フォールバック言語である英語よりもはるかに近い言語です。

ユーザー設定が「pt-AO, id」(アンゴラポルトガル語、次にインドネシア語)の場合、最適な一致は「pt-PT」(ヨーロッパポルトガル語)であり、「pt」(ブラジルポルトガル語)ではありません。

ユーザー設定が「gsw-u-co-phonebk」(電話帳照合順のドイツ語スイス)の場合、最適な一致は「de-u-co-phonebk」(電話帳照合順のドイツ語)です。サーバーの言語リストにおいて、ドイツ語はドイツ語スイスに最適な一致であり、電話帳照合順のオプションは引き継がれています。

信頼度スコア

Goは、ルールベースの除外による粗粒度の信頼度スコアリングを使用します。一致は、完全一致、高(完全一致ではないが、既知の曖昧性はない)、低(おそらく正しい一致だが、必ずしもそうではない)、またはなしに分類されます。複数の一致がある場合、一連のタイブレーキングルールが順番に実行されます。複数の一致が同等の場合、最初の一致が返されます。これらの信頼度スコアは、たとえば、比較的弱い一致を拒否するのに役立ちます。また、たとえば、言語タグから最も可能性の高い地域またはスクリプトを採点するためにも使用されます。

他の言語の実装では、より細かい粒度で、可変スケールのスコアリングを使用することがよくあります。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年間
ブログインデックス