Golang自身はi18nの仕組みは用意していません。外部のライブラリの助けを借りる必要があります。何種類かライブラリがあるのでそれのメモ。
とりあえず2つほど、使い方とか見てみました。他によさ気なのがあったらコメントとかで教えて下さい。
go-i18n
JSONで翻訳ファイルを用意して、それを使って変換する。ライブラリの関数を呼ぶと、変換関数(i18n.TranslateFunc
型)が返ってくるので、それを文字列出力時に挟んであげる。
翻訳メッセージ
JSONファイルを使う
[
{
"id": "program_greeting",
"translation": "Hello world"
},
{
"id": "d_days",
"translation": {
"one": "{{.Count}} day",
"other": "{{.Count}} days"
}
},
{
"id": "person_unread_email_count",
"translation": {
"one": "{{.Person}} has {{.Count}} unread email.",
"other": "{{.Person}} has {{.Count}} unread emails."
}
}
]
使い方
単純な置き換えもできるし、パラメータを埋め込んだり、単数形と複数形みたいなこともできる。
fmt.Println(T("program_greeting"))
fmt.Println(T("d_days", 1))
変換関数 T()
はライブラリの関数を呼ぶと取得できる。
標準ライブラリのテンプレートで翻訳機構を使うには、テンプレート内で使える関数を登録してそれを利用する。
import (
"github.com/nicksnyder/go-i18n/i18n"
"text/template"
)
// テンプレート
tmpl := template.Must(template.New("").Parse(`
{{T "program_greeting"}}
{{T "person_unread_email_count" 2 .}}
`))
// 変換関数を設定
T, _ := i18n.Tfunc("en-US")
tmpl.Funcs(map[string]interface{}{
"T": T,
})
// 実行
tmpl.Execute(os.Stdout, struct {
Person string
}{
Person: "Bob",
})
// 実行結果
// Hello world
// Bob has 2 unread emails.
コマンドラインツールもついてくるけど、untraslatedファイルと翻訳済みファイルがどうこう書かれているけど使い方がいまいちわからず。
辞書のロード
イマイチっぽいところがここ。基本的にはJSONファイルのファイルパスを指定して読み込む。
Goのメリットは1バイナリとして配布できるところなので、辞書もバイナリの中に入れちゃいたいところ。
- こちらのサンプルでは、翻訳をひとつずつ登録することで、外部ファイルを使わないロードをしている。ちなみにこのコードは少し古い。ちょっと書き直したのがこちら
- こちらのサンプルでは、もっとアグレッシブで、一旦tmpファイルに落としてからロードしている。
Readerインタフェースのオブジェクトを受け取れるようにしてくれればgo-bindataで埋め込んだデータをそのままロードできるようになるので、後でパッチ送りたい。
i18n4go
IBM製のGoのi18nライブラリ。go-i18nと違うのは、ツールがもろもろのワークフローの中心として使うように整備されている点。
- ソースコードをASTにしてパースして、文字列リテラルを取得してきて辞書を作る(ファイルやディレクトリ単位)
- ソースコードを改変して、文字列リテラルに対してi18n4goを使うように関数呼び出しを挟むコード改変
- ファイルごとに作られた翻訳メッセージファイルを統合
- Google翻訳を呼び出して翻訳(ただし、品質的に人力を推奨とのこと)
プレゼン資料によると、前述のgo-i18nのラッパー的なコードとなっているらしい。JSONにも対応しているけど、.poファイルも使える。変換部分とか辞書のロードとかもろもろは前述のgo-i18nと同じ。
ファイル/ディレクトリの指定が1回に1つしかできないのが難点。複数ディレクトリを一括で指定したり、子ディレクトリを辿って情報収集してくれるとうれしい。そのうち使うことがあるならパッチを送りたい。
go-gettext
C/C++な人にはお馴染みのgettextのgoポーティング。
使っていないけど、ドキュメントを見る限り、たぶん次のような使い方
import (
"github.com/samuel/go-gettext/gettext"
)
func main() {
// [path]/*/LC_MESSAGES/[name].moをまとめてロード
domain, err := gettext.NewDomain("name", "./path")
// 直接翻訳
fmt.Println(domain.GetText("ja", "message_tag"))
// 特定の言語しか使わないならCatalogを取得して使う
// 上の翻訳と同じ結果
catalog := domain.GetCalalog("ja")
fmt.Println(catalog.GetText("message_tag"))
}
枯れたツールではあるので、.mo
を作るところはOmegaTとかTransifexみたいな便利なツールとかがそのまま使えるのはメリット。
ディレクトリのパスで指定するので、ツールと一緒に配布するのがやりにくそうなのと、分割されたパッケージの場合、個別に翻訳ファイルを作ったのを集めてくる手間が必要なのと、一部置き換え的な使い方のメソッドがないのが難点かな。
pongo2tag
ドキュメントテンプレートのpongo用の翻訳タグ。 trans
タグを追加する。
今回は標準ライブラリのテンプレートを使ったので使わずですが、Python系になれていて、pongoを使うなら(DjangoとかJinja2に似ているという噂)、これを使うのは手だと思います。
go一般で辛そうなポイント
もちろん、簡単に行かないところが幾つかあります。
構造体タグにメッセージが入っている
同名のパラメータを何度も使えるようにしたり、short/longの両方のパラメータを使いたかったので、標準ライブラリのflagsではなくて、go-flagsを使っていたのですが、タグに書く形式だとタグの実行時翻訳ができなのでダメですね。かといってflagsへの書き換えもコストデカイので、とりあえず、欲しい言語(今のところ日本語と英語だけ)ごとに、go-flagsに渡す構造体を作るしか無さそう。
分割されたパッケージ
パッケージを細かく分けているので、ログメッセージ出力がいろんなパッケージにある。いろんなパッケージに分散したメッセージを集めると、せっかく分散したのに依存関係が発生しちゃうのがアレゲ。作戦としては2つ。
- エラーレポートを収集する仕組みを一個作って集約はしているので、引数も取れるようにして、エラーレポート収集関数の中に翻訳の仕組みも仕込んでしまう
- go-i18nの方式ならメッセージごとに登録ができるので、各パッケージにメッセージ翻訳の登録のエントリーポイントは作って、i18nライブラリにメッセージを登録する。Tの呼び出しも各パッケージ内に書いちゃう
前者の方式だと、エラーレポート以外のモジュールのi18nライブラリへの依存はなくて済むけど、メッセージは何らかの方法で登録しておかなくてはいけない。モジュールを使うアプリケーション側で常に責任持って全メッセージを登録するという手がある。
今回紹介したライブラリはランタイムは最終的にgo-i18nなので問題ないけど、一括でしかメッセージが読み込めない場合には後者の方法は使えない。
今回の判断
go-i18nでてっとり早く対応することにした。今後アプリを作るなら、最初からi18n4goを使うことを念頭に仕組みを作りたい。