internationalization
略して i18n
go-i18n
v2.x の動くサンプルが欲しい。しかも Go v1.15 以降で動作確認済みの。
- github.com/nicksnyder/go-i18n @ GitHub
TOML じゃたまらんので JSON が良い。
2021/03/07 現在の go-i18n
のバージョンは v2.1.1 なのですが、公式のサンプルをみても、なんか「従来バージョン(v1.x)を理解していた人向け」なのかと思うほど、前提条件がわらんちんなのです。
「Golang go-i18n v2 動作サンプル」とググっても go-i18n
v1.x 時代のものや、Go v1.15 以上で動かなかったり、typo
で動作しないものが多いので自分のググラビリティとして。
【追記】 2024/06/02 現在、go-i18n
は v2.4.0 になっておりリポジトリも整理され、とても見やすくなりました。
また、最新の main
ブランチでは、下位互換のため v2
ディレクトリに設置されていたパッケージが、ルートに移動しました。スッキリしたものの、今後のリリースでインポートエラーが出るかもしれません。その時は、この記事も更新します。
とはいえ、この記事は依然と有効なので自分でも試していただいて、「go-i18n
良いじゃん」と思ったら、ぜひスターを付けに行ってあげてください。
🐒 本記事で紹介している go-i18n はサード・パーティー製のパッケージです。
Go(以下 golang)の純正モジュールには、テキスト関連の処理に特化した golang.org/x/text というモジュールがあります。
その中の golang.org/x/text/message パッケージで、ローカライズ(地域化)機能を持たせられます。例えば 1000
という数値を、日本なら 1,000
、フランスなら 1 000
といった地域にあわせた表記に変換する機能です。
実は、golang.org/x/text
のドキュメントの Translation セクションを読むと、この地域化機能と catalog.Builder 型の構造体を利用すれば独自の catalog
(対応辞書)を作れたり、plural.Selectf() 関数で複数形にも対応できそうなのです。
しかし、こちらのドキュメントもいまいち使い方がわからんのです。ʕ◔ϖ◔ʔ
とりあえず、この記事は go-i18n
に注力し、golang.org/x/text
を使った国際化の方法がわかりしだい別記事にしたいと思います。「いいね」が 10 超えたら重い腰をあげると思います。
TL; DR (今北産業)
-
Bundle
が辞書を保持するもの。 -
Localizer
が辞書を引くもの。- 入力(メッセージ ID や設定)から、対応するメッセージを辞書から引っ張ってくる関数が
Localize()
です。MustLocalize()
のようにMust*
が付く関数は、エラー発生時にエラーを返さずパニクるタイプです。
- 入力(メッセージ ID や設定)から、対応するメッセージを辞書から引っ張ってくる関数が
-
go-i18n
のバージョンによってBundle
やLocalizer
に渡す言語指定方法が異なる。- この記事は
go-i18n
v2.1 のサンプルです。 - とりま、動くサンプルをオンラインでみる @ Go Playground
- この記事は
🐒 言語指定の書式について
現在の go-i18n
では、言語の指定時に使われる en-US
や ja-JP
は tag
(言語タグ)と呼ばれ、一般的に「IETF 言語タグ」を指します。
IETF 言語タグとは、インターネットの標準化団体の 1 つである IETF によって RFC 5646 と RFC 4647 で制定された言語タグです。
しかし、習慣的に BCP47
といった言い方もされます。BCP
とは、制定前の「その時点でベスト」とされる BCP(Best Current Practice
)のうち 47 番目に「言語タグの指定」のベスト・プラクティスとされたためで、当時の習慣が残っているだけです。参照・参考にする場合は RFC の方を利用してください。
また、IETF 言語タグは "en-US"
や "ja-JP"
のように "<language>-<REGION>"
のようなハイフン構成になっているので、OS の Locale
などのアンダースコア(アンダーバー)を使った "ja_JP"
や "ja_JP.UTF-8"
と記法が異なるので注意が必要です。 ←(俺
TS; DR (kwsk)
... うん?国際化?
日本語でアプリを作ったものの、海外の人にも需要がでてきたので「国際化対応が必要」と言われると、嫌〜な臭いを感じると思います。
しかし「国際化対応」言っても、
- 言語ごとに用語辞書を用意しておく。
- OS の Locale などで、実行環境に合わせて使用する言語の辞書を決める。
- 用語の識別子(メッセージ ID)を指定すると、指定した辞書の言語で該当用語が返ってくる。
- それを Print する。
と言うだけの、単純なものです。簡単とは言ってない
つまり、イメージとしては連想配列に近いものです。「言語ごとに配列(map
)を用意しておき、該当言語の配列のキーを指定すると値が取得できる」ようなもの、と考えると入りやすいと思います。
以下は map
(Go の連想配列)で簡単に実装したものです。
en := map[string]string{
"helloworld": "hello, world",
}
ja := map[string]string{
"helloworld": "こんにちは, 世界",
}
fmt.Println(en["helloworld"])
fmt.Println(ja["helloworld"])
// Output:
// hello, world
// こんにちは, 世界
- オンラインで動作をみる @ Go Playground
Internationalization
と Localization
の違い
アプリを多言語対応しようとすると、Internationalization
や Localization
といった用語が出てきます。
単純に日本語に訳すと Internationalization
は「国際化」で、Localization
は「地域化」です。アプリにおいては、どちらも「他の言語でも表示できる機能」です。何が違うのでしょうか。
「多言語表示対応」を「国際化対応」とした場合、基本的に同じことです。その場合、Internationalization
は「国際化対応の準備」で、Localization
は「国際化対応の適用」という意味合いが強くなります。
つまり「他の言語でも表示できる環境を準備(機能実装)する」ことを Internationalization
と呼び、英語表示を日本語表示に対応させるなど「特定地域に最適化(言語実装)する」ことを Localization
と呼びます。
Internationalization
の方が、より汎用的な作業なので「グローバル対応」とも呼ばれます。「国際化」を「グローバル化」と呼ぶのも、「よりグローバル(汎用的)に対応できるようにする」を格好よく言っているだけなのです。
例えば、リファクタリングにおける Internationalization
は「表示は従来通りだが、辞書さえ用意すれば表示言語をいつでも切り替えられるようにする」という意味になり、「特定言語の辞書作成と表示確認」が Localization
ということになります。
IME のいらない ASCII コード圏(特に英語圏)のリポジトリで Internationalization
の issue
騒ぎが発生することがあります。この場合、UTF-8
対応させることなどが Internationalization
になり、日本語対応が Localization
ということになります。
さて、国際化・地域化を進めるには、先の例のように連想配列を使って「俺様国際化対応」させても良いのですが、専用のライブラリを使うことで空いた時間を辞書作成に費やすことができます。
Go の Internationalization
向けパッケージ
Linux には GNU Gettext という老舗の C 言語のライブラリがあります。PHP(特に Wordpress)ユーザーの場合、翻訳関数 "_()
" を目にしたことがあると思いますが、アレの原点です。
Go 言語には、国際化対応(internationalization
)するのに有名な go-i18n
と言うパッケージがあります。
-
go-i18n
パッケージ- github.com/nicksnyder/go-i18n @ GitHub
これをモジュールとして組み込んで、文書の出力の箇所を翻訳関数に置き換えれば、辞書を用意するだけで他言語に対応する準備が整います。
しかし、このパッケージ、(v2.1.1 時代の)公式のサンプルがわかりづらいのです。
そこで、実際に動作するシンプルなサンプルを見た方が早いと思うので、まずは以下のソースをご覧ください。
ヘルプなどの固定メッセージを JSON 形式で定義して利用する
package main
import (
"encoding/json"
"fmt"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func main() {
// デフォルト言語を英語に設定
bundle := i18n.NewBundle(language.English)
// 辞書の拡張子 ".json" に対してパーサーエンジンを json.Unmarshal にセットする
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
// JSON の辞書を登録
bundle.MustParseMessageFileBytes([]byte(`{"MSG001": "Hello World!"}`), "en.json")
bundle.MustParseMessageFileBytes([]byte(`{"MSG001": "Hola Mundo!"}`), "es.json")
bundle.MustParseMessageFileBytes([]byte(`{"MSG001": "ようこそ、世界!"}`), "ja.json")
// 辞書から引きたい内容(MSG001 を引きたい)
referTo := &i18n.LocalizeConfig{MessageID: "MSG001"}
// 辞書を引く
{
// 英語の辞書を使う
localizer := i18n.NewLocalizer(bundle, "en-US")
fmt.Println(localizer.MustLocalize(referTo))
}
{
// スペイン語の辞書を使う
localizer := i18n.NewLocalizer(bundle, "es-ES")
fmt.Println(localizer.MustLocalize(referTo))
}
{
// 日本語の辞書を使う
localizer := i18n.NewLocalizer(bundle, "ja-JP")
fmt.Println(localizer.MustLocalize(referTo))
}
{
// 未定義の辞書を使う(デフォルトの英語を使う)
localizer := i18n.NewLocalizer(bundle, "ko-KR")
fmt.Println(localizer.MustLocalize(referTo))
}
}
// Output:
// Hello World!
// Hola Mundo!
// ようこそ、世界!
// Hello World!
- オンラインで動作をみる @ Go Playground
複数形・単数形バリエーション
上記 JSON を使った例のように、通常は「タイトルやヘルプ・メッセージといった、単純な置き換えで十分」というケースが多いと思います。
しかし「●件のメッセージがあります」といった場合、言語によっては単数形・複数形を加味しないと不自然な言語もあります。
下記は、翻訳時(Localize 時)に PluralCount
の値よって単数形・複数形を使い分けをする例です。
ぶっちゃけ、登録用語は 1 つだし、JSON でなく構造体を使っているので「if
文で分岐させた方が早えぇな」と感じるかもしれません。
しかし「メッセージ ID による単純な置き換え」に限界が来た時に、メッセージ(用語)にバリエーションを持たせることができることに注目してください。
package main
import (
"fmt"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func main() {
// 辞書セットを作成しデフォルトを英語に設定
bundle := i18n.NewBundle(language.English)
// 使う辞書の言語設定
loc := i18n.NewLocalizer(bundle, language.English.String())
// 辞書用語を設定(単数形・複数形対応)
// i18n.Message 型は Localize から呼び出された際の個数情報(PluralCount)
// によって単数(One:)、複数(Other:)でメッセージ内容が変わる。
// "One" と "Other" にある "{{.Name}}" と "{{.Count}}" は、
// Localize() から呼び出された際に渡される map のキー名。(下記「辞書翻訳の実行」参照)
message := &i18n.Message{
// メッセージ ID(この設定を呼び出すキー)
ID: "Emails",
// メモ
Description: "The number of unread emails a user has",
// 単数形用の構文定義(いわるゆテンプレート)
One: "{{.Name}} has {{.Count}} email.",
// 複数形用の構文定義(いわるゆテンプレート)
Other: "{{.Name}} has {{.Count}} emails.",
}
// 個数を指定(単数・複数でメッセージ内容が変わるかの確認)
emailCount := 2
// 辞書翻訳の実行
// MustLocalize と Localize は同じですが、MustLocalize はエラー時に
// パニックになります。
translation := loc.MustLocalize(&i18n.LocalizeConfig{
// デフォルトで使う辞書用語を定義
DefaultMessage: message,
// 辞書の用語(のテンプレート)に流し込むデータ。
// "Name", "Count" を持つ辞書登録(用語)すべてに "KEINOS" と個数
// をテンプレートとしてマッピング(割り当てを)する。
TemplateData: map[string]interface{}{
"Name": "KEINOS",
"Count": emailCount,
},
// データの個数情報
PluralCount: emailCount,
})
fmt.Println(translation)
}
// Output: KEINOS has 2 emails.
- オンラインで動作をみる @ Go Playground(Go version: 1.16)