LoginSignup
10
5

【Golang】go-i18n/v2 の動作サンプル(2021年03月版)【国際化対応】

Last updated at Posted at 2021-03-06

internationalization 略して i18n

go-i18n v2.x の動くサンプルが欲しい。しかも Go v1.15 以降で動作確認済みの。

TOML じゃたまらんので JSON が良い。

2021/03/07 現在の go-i18n のバージョンは v2.1.1 なのですが、公式のサンプルをみても、なんか「従来バージョン(v1.x)を理解していた人向け」なのかと思うほど、前提条件がわらんちんなのです。

「Golang go-i18n v2 動作サンプル」とググってgo-i18n v1.x 時代のものや、Go v1.15 以上で動かなかったり、typo で動作しないものが多いので自分のググラビリティとして。

🐒   本記事で紹介している 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* が付く関数は、エラー発生時にエラーを返さずパニクるタイプです。
  • go-i18n のバージョンによって BundleLocalizer に渡す言語指定方法が異なる。

🐒   言語指定の書式について
言語の指定時に使われる en-USja-JPtag(言語タグ)と呼ばれ、一般的に「IETF 言語タグ」を指します。
IETF 言語タグとは、インターネットの標準化団体の 1 つである IETF によって RFC 5646RFC 4647 で制定された言語タグです。しかし、習慣的に BCP47 といった言い方もされます。BCP とは、制定前の「その時点でベスト」とされる BCPBest Current Practice)のうち 47 番目に「言語タグの指定」のベスト・プラクティスとされたためで、当時の習慣が残っているだけです。参照・参考にする場合は RFC の方を利用してください。
また、IETF 言語タグは "en-US""ja-JP" のように "<language>-<REGION>" のようなハイフン構成になっているので、OS の Locale などのアンダースコア(アンダーバー)を使った "ja_JP""ja_JP.UTF-8" と記法が異なるので注意が必要です。 ←(俺<

TS; DR (マスター、Go v1.15 以上で効く動くものをくれ)

... うん?国際化?

英語でアプリを作ったものの、日本人にも需要がでてきたので「国際化対応が必要」と言われると、嫌〜な臭いを感じると思います。

しかし「国際化対応」言っても、

  1. 言語ごとに用語辞書を用意しておく。
  2. OS の Locale などで、実行環境に合わせて使用する言語の辞書を決める。
  3. 用語の識別子(メッセージ ID)を指定すると、指定した辞書の言語で該当用語が返ってくる。
  4. それを 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
// こんにちは, 世界

InternationalizationLocalization の違い

アプリを多言語対応しようとすると、InternationalizationLocalization といった用語が出てきます。

単純に日本語に訳すと Internationalization は「国際化」で、Localization は「地域化」です。アプリにおいては、どちらも「他の言語でも表示できる機能」です。何が違うのでしょうか。

「多言語表示対応」を「国際化対応」とした場合、基本的に同じことです。その場合、Internationalization は「国際化対応の準備」で、Localization は「国際化対応の適用」という意味合いが強くなります。

つまり「他の言語でも表示できる環境を準備(機能実装)する」ことを Internationalization と呼び、英語表示を日本語表示に対応させるなど「特定地域に最適化(言語実装)する」ことを Localization と呼びます。

Internationalization の方が、より汎用的な作業なので「グローバル対応」とも呼ばれます。「国際化」を「グローバル化」と呼ぶのも、「よりグローバル(汎用的)に対応できるようにする」を格好よく言っているだけなのです。

例えば、リファクタリングにおける Internationalization は「表示は従来通りだが、辞書さえ用意すれば表示言語をいつでも切り替えられるようにする」という意味になり、「特定言語の辞書作成と表示確認」が Localization ということになります。

IME のいらない ASCII コード圏(特に英語圏)のリポジトリで Internationalizationissue 騒ぎが発生することがあります。この場合、UTF-8 対応させることなどが Internationalization になり、日本語 IME 対応が Localization ということになります。

さて、国際化・地域化を進めるには、先の例のように連想配列を使って「俺様国際化対応」させても良いのですが、専用のライブラリを使うことで空いた時間を辞書作成に費やすことができます。

Go の Internationalization 向けパッケージ

Linux には GNU Gettext という老舗の C 言語のライブラリがあります。PHP(特に Wordpress)ユーザーの場合、翻訳関数 "_()" を目にしたことがあると思いますが、アレの原点です。

Go 言語には、国際化対応(internationalization)するのに有名な go-i18n と言うパッケージがあります。

これをモジュールとして組み込んで、文書の出力の箇所を翻訳関数に置き換えれば、辞書を用意するだけで他言語に対応する準備が整います。

しかし、このパッケージ、公式のサンプルがわかりづらいのです。

そこで、実際に動作するシンプルなサンプルを見た方が早いと思うので、まずは以下のソースをご覧ください。

ヘルプなどの固定メッセージを JSON 形式で定義して利用する

JSON辞書を使った例(メッセージIDで引く。シンプルでオススメ)
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.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!

複数形・単数形バリエーション

上記 JSON を使った例のように、通常は「タイトルやヘルプ・メッセージといった、単純な置き換えで十分」というケースが多いと思います。

しかし「●件のメッセージがあります」といった場合、言語によっては単数形・複数形を加味しないと不自然な言語もあります。

下記は、翻訳時(Localize 時)に PluralCount の値よって単数形・複数形を使い分けをする例です。

ぶっちゃけ、登録用語は 1 つだし、JSON でなく構造体を使っているので「if 文で分岐させた方が早えぇな」と感じるかもしれません。

しかし「メッセージ ID による単純な置き換え」に限界が来た時に、メッセージ(用語)にバリエーションを持たせることができることに注目してください。

基本(i18n.LocalizeConfigの設定情報で引く)
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.

関連文献

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5