目次
Go(Golang)i18n:国際化パターンとライブラリ
Goはグローバルユーザーにサービスを提供するバックエンドサービス、CLI、APIとしてますます人気を集めています。Goの標準ライブラリには他のエコシステムほど包括的なi18nフレームワークは含まれていませんが、golang.org/x/text、go-i18n、そして慎重なアーキテクチャの組み合わせにより、堅牢な多言語Goアプリケーションを実現できます。
このガイドでは、Goのi18nツールキットを網羅的に解説します。x/textを使用したロケール対応フォーマットからgo-i18nによる翻訳管理まで、さらにGoサービスとCLIにおけるi18nの構造化パターンも紹介します。
Goのi18nエコシステム概要
Goのi18nツールは複数のパッケージに分散しています:
| パッケージ | 目的 |
|---|---|
golang.org/x/text/language | BCP 47言語タグの解析とマッチング |
golang.org/x/text/message | i18n対応のPrintfスタイルフォーマットメッセージ |
golang.org/x/text/number | 数値フォーマット(複数形、通貨) |
golang.org/x/text/collate | ロケール対応の文字列ソート |
golang.org/x/text/unicode/norm | Unicode正規化 |
github.com/nicksnyder/go-i18n/v2 | 翻訳ファイル管理、複数形処理 |
github.com/BurntSushi/toml | TOML解析(一般的な翻訳フォーマット) |
golang.org/x/textによる言語タグ解析
golang.org/x/text/languageパッケージはBCP 47言語タグを実装しています。これはHTTP Accept-Languageヘッダー、HTML lang属性、ロケール文字列で使用されるロケール識別子の標準です。
言語タグの解析とマッチング
package main
import (
"fmt"
"golang.org/x/text/language"
)
func main() {
// 言語タグを解析する
tag, err := language.Parse("zh-Hant-TW")
if err != nil {
panic(err)
}
fmt.Println(tag) // zh-Hant-TW
// 言語マッチング:サポートされている言語から最適なマッチを探す
supported := []language.Tag{
language.English,
language.French,
language.SimplifiedChinese,
language.TraditionalChinese,
}
matcher := language.NewMatcher(supported)
// ユーザーがzh-TW(繁体字中国語、台湾)を要求する
t, _, _ := matcher.Match(language.MustParse("zh-TW"))
fmt.Println(t) // zh-Hant → 繁体字中国語にマッチ
// ユーザーがes-MX(メキシコスペイン語)を要求する
t, _, _ = matcher.Match(language.MustParse("es-MX"))
fmt.Println(t) // en → 英語にフォールバック(スペイン語は非対応)
}
Accept-Languageヘッダーの解析
import (
"net/http"
"golang.org/x/text/language"
)
var supported = []language.Tag{
language.English,
language.French,
language.German,
language.Japanese,
}
var matcher = language.NewMatcher(supported)
func getLocale(r *http.Request) language.Tag {
// Accept-Languageヘッダーを解析する
accept := r.Header.Get("Accept-Language")
tags, _, err := language.ParseAcceptLanguage(accept)
if err != nil || len(tags) == 0 {
return language.English
}
tag, _, _ := matcher.Match(tags...)
return tag
}
go-i18n:翻訳管理
go-i18nは翻訳管理で最も広く使われているGoライブラリです。以下をサポートしています:
- JSON、TOML、YAML翻訳ファイル
- 複数形処理のためのICUメッセージフォーマット
- テンプレート補間
- 複数の翻訳ファイルの読み込み
インストールとセットアップ
go get github.com/nicksnyder/go-i18n/v2@latest go get github.com/BurntSushi/toml
翻訳ファイルの構造
# locales/en.toml
[welcome]
description = "新規ユーザー向けウェルカムメッセージ"
one = "ようこそ、{{.Name}}さん!{{.Count}}件の新着通知があります。"
other = "ようこそ、{{.Name}}さん!{{.Count}}件の新着通知があります。"
[item_count]
description = "リスト内のアイテム数"
zero = "アイテムなし"
one = "{{.Count}}件のアイテム"
other = "{{.Count}}件のアイテム"
# locales/fr.toml
[welcome]
one = "Bienvenue, {{.Name}} ! Vous avez {{.Count}} nouvelle notification."
other = "Bienvenue, {{.Name}} ! Vous avez {{.Count}} nouvelles notifications."
[item_count]
zero = "Aucun élément"
one = "{{.Count}} élément"
other = "{{.Count}} éléments"
go-i18nの初期化
package i18n
import (
"embed"
"encoding/json"
"github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
//go:embed locales/*.toml
var localeFS embed.FS
var bundle *i18n.Bundle
func Init() error {
bundle = i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// すべてのロケールファイルを読み込む
entries, err := localeFS.ReadDir("locales")
if err != nil {
return err
}
for _, entry := range entries {
if _, err := bundle.LoadMessageFileFS(localeFS, "locales/"+entry.Name()); err != nil {
return fmt.Errorf("ロケール %s の読み込みエラー: %w", entry.Name(), err)
}
}
return nil
}
// NewLocalizerは指定された言語タグのローカライザーを作成します
func NewLocalizer(langs ...string) *i18n.Localizer {
return i18n.NewLocalizer(bundle, langs...)
}
メッセージの翻訳
package handlers
import (
"net/http"
"myapp/i18n"
)
type WelcomeData struct {
Name string
Count int
}
func WelcomeHandler(w http.ResponseWriter, r *http.Request) {
// リクエストの言語に対するローカライザーを取得する
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(accept, "en")
// 複数形処理付きの単純なメッセージ
itemCount := 3
msg, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "item_count",
PluralCount: itemCount,
TemplateData: map[string]interface{}{
"Count": itemCount,
},
})
if err != nil {
msg = "不明な数のアイテム" // フォールバック
}
// 名前と複数形を含むメッセージ
welcome, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "welcome",
PluralCount: itemCount,
TemplateData: WelcomeData{
Name: "Alice",
Count: itemCount,
},
})
fmt.Fprintf(w, "%s\n%s\n", welcome, msg)
}
数値と通貨のフォーマット
golang.org/x/text/messageパッケージはロケール対応の数値フォーマットを提供します:
package main
import (
"fmt"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/number"
)
func main() {
// ロケール固有のプリンターを作成する
enPrinter := message.NewPrinter(language.English)
dePrinter := message.NewPrinter(language.German)
jaPrinter := message.NewPrinter(language.Japanese)
amount := 1234567.89
// ロケール対応の数値フォーマット
enPrinter.Printf("%v\n", number.Decimal(amount))
// 1,234,567.89
dePrinter.Printf("%v\n", number.Decimal(amount))
// 1.234.567,89
jaPrinter.Printf("%v\n", number.Decimal(amount))
// 1,234,567.89
// パーセンテージのフォーマット
enPrinter.Printf("%.2%\n", number.Percent(0.8527))
// 85.27%
dePrinter.Printf("%.2%\n", number.Percent(0.8527))
// 85,27 %
}
日時のフォーマット
Goのtimeパッケージは基本的な日付フォーマットを処理しますが、ロケール対応の日付フォーマットにはx/textまたはサードパーティライブラリが必要です:
import (
"time"
"golang.org/x/text/language"
"golang.org/x/text/language/display"
)
func formatDateLocale(t time.Time, locale language.Tag) string {
// Goの組み込み時間フォーマットは固定レイアウト文字列を使用します
// ロケール固有のフォーマットには、strftime相当のライブラリを使用するか
// CLDRデータからパターンを生成してください
switch locale {
case language.English:
return t.Format("January 2, 2006")
case language.German:
return t.Format("2. January 2006") // ドイツ語の日付フォーマット
case language.Japanese:
return t.Format("2006年01月02日")
default:
return t.Format(time.RFC3339)
}
}
プロダクション品質のロケール対応日付フォーマットには、曜日名や月名にgithub.com/goodsign/mondayなどのライブラリを使用してください。
GoウェブサービスにおけるI18nの構造化
ミドルウェアベースのロケール検出
package middleware
import (
"context"
"net/http"
"golang.org/x/text/language"
"myapp/i18n"
)
type contextKey string
const localizerKey contextKey = "localizer"
var supported = []language.Tag{
language.English,
language.French,
language.German,
language.Japanese,
}
var matcher = language.NewMatcher(supported)
func LocaleMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// まずURLクエリパラメーターを確認する(例:?lang=fr)
lang := r.URL.Query().Get("lang")
// 次にCookieを確認する
if lang == "" {
if cookie, err := r.Cookie("lang"); err == nil {
lang = cookie.Value
}
}
// Accept-Languageヘッダーにフォールバックする
if lang == "" {
lang = r.Header.Get("Accept-Language")
}
// ローカライザーを作成してコンテキストに付与する
localizer := i18n.NewLocalizer(lang, "en")
ctx := context.WithValue(r.Context(), localizerKey, localizer)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// GetLocalizerはコンテキストからローカライザーを取得します
func GetLocalizer(ctx context.Context) *i18n.Localizer {
l, ok := ctx.Value(localizerKey).(*i18n.Localizer)
if !ok {
return i18n.NewLocalizer("en")
}
return l
}
翻訳可能なコンテンツとしてのエラーメッセージ
package errors
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
)
// AppErrorは翻訳可能なメッセージを持つエラーです
type AppError struct {
MessageID string
TemplateData interface{}
Err error
}
func (e *AppError) Error() string {
return e.MessageID // 内部識別子
}
func (e *AppError) Localize(localizer *i18n.Localizer) string {
msg, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: e.MessageID,
TemplateData: e.TemplateData,
})
if err != nil {
return e.MessageID // キーにフォールバック
}
return msg
}
// 使用例
var ErrNotFound = &AppError{MessageID: "error.not_found"}
var ErrUnauthorized = &AppError{MessageID: "error.unauthorized"}
Go CLIアプリケーションのi18n
CLIアプリケーションには独自のi18n要件があります:システムロケールを検出し、適切に出力をフォーマットし、ターミナルエンコーディングを処理する必要があります。
package main
import (
"os"
"golang.org/x/text/language"
"myapp/i18n"
)
func detectSystemLocale() string {
// LANG環境変数を確認する(Unix/Linux/macOS)
if lang := os.Getenv("LANG"); lang != "" {
// LANGは通常「en_US.UTF-8」形式です
// 言語タグを抽出する
parts := strings.Split(lang, ".")
if len(parts) > 0 {
// 「en_US」を「en-US」(BCP 47)に変換する
return strings.ReplaceAll(parts[0], "_", "-")
}
}
// LC_ALL、LC_MESSAGESを確認する
for _, env := range []string{"LC_ALL", "LC_MESSAGES"} {
if lang := os.Getenv(env); lang != "" {
return strings.ReplaceAll(strings.Split(lang, ".")[0], "_", "-")
}
}
return "en" // 英語をデフォルトとする
}
func main() {
locale := detectSystemLocale()
localizer := i18n.NewLocalizer(locale, "en")
// すべての出力にローカライザーを使用する
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "cli.welcome",
}))
}
抽出と管理ツール
go-i18nは翻訳可能な文字列を抽出するためのgoi18nコマンドラインツールを提供しています:
# CLIツールをインストールする go install github.com/nicksnyder/go-i18n/v2/goi18n@latest # Goソースから文字列を抽出する(active.en.tomlを作成) goi18n extract -format toml -outdir locales ./... # 既存の翻訳に新しい文字列をマージする # (新規または変更された文字列のみを含むtranslate.fr.tomlを作成) goi18n merge -format toml -outdir locales locales/active.en.toml locales/active.fr.toml # 翻訳後、translate.fr.tomlをactive.fr.tomlにマージする goi18n merge -format toml -outdir locales locales/active.fr.toml locales/translate.fr.toml
このワークフローは継続的な翻訳更新のためのCI/CDローカリゼーション自動化とうまく統合できます。
複雑な言語の複数形処理
Go-i18nはサポートされているすべての言語に対してCLDRルールを通じて複数形処理を自動的に処理します:
# locales/ru.toml(ロシア語 - 4つの複数形)
[item_count]
zero = "Нет элементов"
one = "{{.Count}} элемент" # 1、21、31...
few = "{{.Count}} элемента" # 2-4、22-24...
many = "{{.Count}} элементов" # 5-20、25-30...
other = "{{.Count}} элемента" # 小数、その他
# locales/ar.toml(アラビア語 - 6つの複数形)
[item_count]
zero = "لا عناصر"
one = "{{.Count}} عنصر"
two = "{{.Count}} عنصران"
few = "{{.Count}} عناصر"
many = "{{.Count}} عنصرًا"
other = "{{.Count}} عنصر"
複数形ルールの詳しい解説は、言語間の複数形ルールをご参照ください。
GoにおけるI18nのテスト
package i18n_test
import (
"testing"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func TestPluralForms(t *testing.T) {
bundle := setupTestBundle(t)
tests := []struct {
lang string
count int
expected string
}{
{"en", 0, "No items"},
{"en", 1, "1 item"},
{"en", 2, "2 items"},
{"ru", 1, "1 элемент"},
{"ru", 2, "2 элемента"},
{"ru", 5, "5 элементов"},
{"ru", 11, "11 элементов"}, // 注意:11は「many」を使用し、「one」ではない
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%s_%d", tt.lang, tt.count), func(t *testing.T) {
localizer := i18n.NewLocalizer(bundle, tt.lang)
got, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "item_count",
PluralCount: tt.count,
TemplateData: map[string]interface{}{"Count": tt.count},
})
if err != nil {
t.Fatalf("ローカライズエラー: %v", err)
}
if got != tt.expected {
t.Errorf("取得値 %q、期待値 %q", got, tt.expected)
}
})
}
}
包括的なテスト戦略については、i18nテストツール、戦略、および自動化をご参照ください。
better-i18nでアプリをグローバルに展開しましょう
better-i18nはAI駆動の翻訳、gitネイティブのワークフロー、グローバルCDNデリバリーを一つの開発者ファーストプラットフォームに統合しています。スプレッドシートの管理をやめて、あらゆる言語でのリリースを始めましょう。