本教程面向想在 Go 项目中使用 go-i18n 这个库来实现国际化(i18n)支持的开发者。覆盖从安装、消息抽取与翻译文件管理、运行时加载、Localizer 使用、复数规则、模板变量与自定义分隔符、内嵌文件(go:embed)、到进阶主题(hash、回退与调试)的完整工作流,并配有大量示例代码与 CLI 使用示例。
概览
go-i18n 将国际化拆成两条主线:
- 运行时库(包 i18n):负责加载翻译文件、管理消息模板、语言匹配、选择复数形式以及模板渲染。
- 工具链(命令 goi18n):负责从源码抽取消息、生成/合并供翻译文件、序列化(toml/json/yaml)等,配合 CLDR 生成的复数规则保证各语言复数行为准确。
特点:
- 支持 CLDR 的 200+ 语言复数规则;
- 支持 text/template 风格的命名变量模板;
- 支持 TOML/JSON/YAML 消息文件格式(可注册自定反序列化);
- 提供 goi18n 工具自动抽取 Go 源代码中的 Message 字面量并生成翻译工作文件。
什么是 CLDR?
CLDR 全称是 “Unicode Common Locale Data Repository”(Unicode 通用区域设置信息库)。它是由 Unicode Consortium 维护的一个权威性数据库,收集、标准化并发布全球各语言与地区有关的本地化数据(locale data)。很多国际化/本地化工具、库和平台都以 CLDR 作为数据来源来保证本地化行为的一致性与正确性,对实现准确的国际化功能(特别是复数处理、格式化、排序等)非常关键。
安装与依赖
在项目中引入 go-i18n(v2)及常用序列化工具。
示例 go.mod 依赖:
require (
github.com/nicksnyder/go-i18n/v2 v2.x.x
github.com/BurntSushi/toml v0.4.1 // 如果使用 toml
gopkg.in/yaml.v3 v3.x.x // 如果使用 YAML
golang.org/x/text v0.x.x // language tag / matcher
)
安装 goi18n CLI(用于提取/合并翻译文件):
go install github.com/nicksnyder/go-i18n/v2/goi18n@latest
概念与核心类型简介
- Bundle:运行时的核心容器。包含默认语言、已加载的消息模板、plural 规则与 language.Matcher。
- Message:消息结构(ID、描述、Hash、Left/RightDelim、Zero/One/Two/Few/Many/Other 字段等),用于在文件里定义或作为 DefaultMessage。
- MessageTemplate:从 Message 生成的运行时模板结构(包含模板源、编译的 template 等,便于本地化渲染)。
- Localizer:本地化器,使用一组语言偏好(例如 URL 参数、Accept-Language)来选择合适语言并渲染消息。
- goi18n:CLI 工具,含 extract(从源代码抽取消息)、merge(为翻译生成/合并消息文件)、marshal(序列化)等功能。
- internal/plural:基于 CLDR 的复数规则数据与 codegen。
开始:一个完整可运行示例
示例:在运行时加载翻译文件并本地化一条消息。
package main
import (
"fmt"
"log"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/BurntSushi/toml"
"golang.org/x/text/language"
)
func main() {
// 1) 创建 Bundle(应用生命周期内使用同一个)
bundle := i18n.NewBundle(language.English)
// 2) 指定如何反序列化 .toml 文件(或 json/yaml)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// 3) 加载翻译文件(例如 active.es.toml)
// 假设 active.es.toml 中有对 MessageID "HelloPerson" 的翻译。
// # 文件名示例: active.es.toml
// [HelloPerson]
// other = "Hola {{.Name}}"
if _, err := bundle.LoadMessageFile("active.es.toml"); err != nil {
log.Fatalf("加载翻译文件失败: %v", err)
}
// 4) 创建 Localizer
// 传入 bundle,并可提供一个或多个语言偏好(可以是 language.Tag、语言字符串或 Accept-Language header)
// 例如只指定 "es" 表示首选西班牙语;也可以写 NewLocalizer(bundle, "es", "en-US") 表示首选 es,回退 en-US。
localizer := i18n.NewLocalizer(bundle, "es")
// 5) 本地化一条消息
// 常用字段说明:
// - MessageID: 要查找的消息 ID(优先使用已加载翻译文件中的翻译)
// - DefaultMessage: 当 MessageID 对应的翻译不存在时作为回退(可以把默认文本放在这里)
// - TemplateData: 传入给模板的变量(模板中用 {{.Name}}、{{.Count}} 等占位)
// - PluralCount: 如果消息包含复数化(one/other 等),传入计数值以便根据语言规则选取正确的复数形式
//
// 回退逻辑:
// 1. Localizer 用 language.Matcher 根据偏好选择最合适的已加载语言(例如 "es" -> active.es.toml)。
// 2. 在选定语言中查找 MessageID 对应的 MessageTemplate 并选择合适的 plural 变体(如果提供 PluralCount)。
// 3. 若选定语言没有对应翻译,则按 language.Matcher 继续回退到其他已加载的语言(例如 en-US -> en),最终回退到 Bundle 的 defaultLanguage。
// 4. 如果所有已加载语言都没有该 MessageID,且 LocalizeConfig 提供了 DefaultMessage,则使用 DefaultMessage(并在 DefaultMessage 中再做 plural/template 渲染)。
// 5. 如果既没有翻译也没有 DefaultMessage,则 Localize 返回错误(或使用 MustLocalize 会 panic)。
//
// 注意:
// - MessageID 和 DefaultMessage 可以二选一(有 ID 的话优先用 ID 去查翻译)。
// - TemplateData 的字段名必须与模板占位符一致(Go text/template 语法)。
// - PluralCount 必须是数值(用来触发 plural 规则),否则会按 other 处理。
//
//
// 下面示例:尝试使用 MessageID "HelloPerson"(如果 active.es.toml 有翻译则使用),
// 并传入模板数据 Name="Alice"。
greeting, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "HelloPerson", // 要查询的消息 ID(优先使用已加载针对 localizer 语言的翻译)
DefaultMessage: &i18n.Message{ // MessageID 未命中时的回退(可选)
ID: "HelloPerson",
Other: "Hello {{.Name}}", // DefaultMessage 使用相同的模板语法
},
TemplateData: map[string]interface{}{ // 提供给模板的数据(键名与模板变量相对应)
"Name": "Alice",
},
// 没有复数情况时可以省略 PluralCount
})
if err != nil {
log.Fatalf("本地化失败: %v", err)
}
fmt.Println(greeting) // 例如: "Hola Alice"(如果 active.es.toml 中 HelloPerson 翻译为 "Hola {{.Name}}")
// 复数示例
// 假设消息文件包含:
// [PersonCats]
// one = "{{.Name}} tiene {{.Count}} gato."
// other = "{{.Name}} tiene {{.Count}} gatos."
//
// 当 Count==1 时 CLDR 规则选择 "one",当 Count==2 时选择 "other"(语言差异化由 CLDR 控制)
s, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "PersonCats",
TemplateData: map[string]interface{}{
"Name": "Carlos",
"Count": 2, // 这是模板中显示的变量
},
PluralCount: 2, // 这是用于决定复数形式的计数(必须传入以触发复数选择)
})
if err != nil {
log.Fatalf("复数本地化失败: %v", err)
}
fmt.Println(s) // 例如: "Carlos tiene 2 gatos."
// 如果你确定消息存在或希望在错误时立即失败,可以使用 MustLocalize。
mustResult := localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "HelloPerson",
TemplateData: map[string]interface{}{
"Name": "Bob",
},
})
fmt.Println("MustLocalize result:", mustResult)
}
消息文件格式(toml/json/yaml)
消息文件命名约定:
- 格式部分:文件名最后一个
.之后的部分(例如.toml、.json、.yaml)。 - 语言 tag:倒数第二个
.之后到文件格式部分之前(例如active.en.toml、en.toml)。
支持两种主要表示法:
1. 简洁形式(当源语言且只有 other 字段时):
# active.en.toml
[HelloPerson]
other = "Hello {{.Name}}"
或更简洁(源语言并且只有 other):
[HelloPerson]
other = "Hello {{.Name}}"
# 有时也支持直接写成:
# HelloPerson = "Hello {{.Name}}"
2. 完整对象形式(含 description、hash、复数各形态、定界符等):
[PersonCats]
description = "Number of cats a person has"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
JSON 示例:
{
"HelloPerson": {
"other": "Hello {{.Name}}"
}
}
注意:
- 支持字段:id(通常由方括号 key 表示)、description、hash、leftdelim、rightdelim、zero、one、two、few、many、other。
- 在源语言文件里,goi18n 的 marshal 会把只有 other 且没有描述、没有自定义 delimiter 的消息写成简洁字符串,以便更易读。
模板与变量、模板定界符
go-i18n 使用 Go 标准库的 text/template 风格语法(默认 {{ 和 }})来渲染模板变量。
示例:
[HelloPerson]
other = "Hello {{.Name}}, you have {{.Count}} messages."
在代码中传入模板数据:
localizer.Localize(&i18n.LocalizeConfig{
MessageID: "HelloPerson",
TemplateData: map[string]interface{}{
"Name": "Bob",
"Count": 5,
},
})
自定义模板分隔符(如果后端模板冲突或想用别的分隔符):
- 在 Message 里或消息文件里设置 leftdelim / rightdelim:
[CustomDelim] leftdelim = "{%" rightdelim = "%}" other = "{% .Name %} has {% .Count %} items." - 当 MessageTemplate 有 LeftDelim/RightDelim 时,库在渲染时会使用这些定界符创建模板。
复数(Plural)支持
go-i18n 支持 CLDR 中的 plural 分类(zero/one/two/few/many/other),并通过 internal/plural 的规则在运行时为每个语言选择正确的形式。
示例消息(支持 plural forms):
[PersonCats]
description = "The number of cats a person has"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
本地化时传入 PluralCount 来选择复数形式:
s, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "PersonCats",
TemplateData: map[string]interface{}{
"Name": "Nick",
"Count": 2,
},
PluralCount: 2, // 标识复数计数,复数规则由语言决定
})
// s => "Nick has 2 cats."(假设翻译对应 English 规则)
注意:
- PluralCount 不必是整数类型(会被识别为数值),但是通常传入整数更直观。
- CLDR 复数规则覆盖 200+ 语言,保证在各语言下的“1 条 vs 多条”等差异被正确处理。
使用 Localizer:HTTP 示例(Accept-Language)
在 Web 应用常见场景中,你会根据用户的请求偏好(表单参数或 Accept-Language header)创建 Localizer:
func handler(w http.ResponseWriter, r *http.Request) {
// 假设 bundle 已在程序启动时创建并加载了消息文件
// lang 可通过 ?lang=xx 明确覆盖
lang := r.FormValue("lang")
accept := r.Header.Get("Accept-Language")
// NewLocalizer 接受 bundle 和一系列语言偏好参数(字符串或 language.Tag)
localizer := i18n.NewLocalizer(bundle, lang, accept)
message, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "PersonCats",
TemplateData: map[string]interface{}{
"Name": "Maria",
"Count": 1,
},
PluralCount: 1,
})
if err != nil {
http.Error(w, "i18n error", http.StatusInternalServerError)
return
}
fmt.Fprintln(w, message)
}
语言匹配策略:
- Bundle 维护一组已加载语言 tag,并使用 golang.org/x/text/language.Matcher 进行标准化匹配(确保 Accept-Language 与已加载的翻译之间的匹配符合预期,例如
en-US回退到en等)。
使用 go:embed 加载消息文件(Go 1.16+)
如果希望把消息文件打包到二进制中,使用 go:embed:
import (
_ "embed"
"embed"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/BurntSushi/toml"
"golang.org/x/text/language"
)
//go:embed locales/active.*.toml
var localesFS embed.FS
func main() {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// LoadMessageFileFS 会读取 embed.FS 中的文件(API 在 README 有示例)
// 例如同目录下有 locales/active.es.toml
if _, err := bundle.LoadMessageFileFS(localesFS, "locales/active.es.toml"); err != nil {
log.Fatal(err)
}
// ...
}
注意:如果你使用的是 goi18n 的 marshal 输出,确保 embed 的路径和文件命名保持与 ParseMessageFileBytes 的解析规则一致。
goi18n CLI:extract / merge 使用示例
安装 goi18n CLI:
go install github.com/nicksnyder/go-i18n/v2/goi18n@latest
约定与注意:
- 源语言文件通常命名为
active.<tag>.<format>,例如active.en.toml(这是常用习惯,goi18n 期望倒数第二段为语言 tag)。 - 翻译人员工作文件通常命名为
translate.<tag>.<format>,例如translate.es.toml。 - 支持的格式:toml / json / yaml(要在运行时注册相应的 UnmarshalFunc)。
- 在代码中使用 i18n.Message 字面量(例如
&i18n.Message{ ID:"X", Other:"..."})或 DefaultMessage 写在 LocalizeConfig 里,便于goi18n extract自动抽取。 - extract 会忽略
*_test.go文件,并且只会抽取那些导入了 “github.com/nicksnyder/go-i18n/v2/i18n” 的源码文件里以字面量形式出现的 Message。
1. 提取源码中的消息(生成源语言 active 文件)
将会扫描 Go 源文件的 AST,提取 i18n.Message 结构字面量并生成一个源消息文件(例如 active.en.toml)。
示例:
var PersonCats = &i18n.Message{
ID: "PersonCats",
Description: "The number of cats a person has",
One: "{{.Name}} has {{.Count}} cat.",
Other: "{{.Name}} has {{.Count}} cats.",
}
# 在项目根运行(默认 sourceLanguage=en, format=toml)
goi18n extract -sourceLanguage en -outdir locales -format toml ./...
extract 会扫描项目(此处为 ./...)的 .go 文件,找到 i18n.Message 字面量并合并成一个消息文件写入指定目录(-outdir)。
输出示例 locales/active.en.toml:
[PersonCats]
description = "The number of cats a person has"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
你在源码中新增了消息或修改了原文需要重新 extract(更新源语言 active 文件)
2. 为翻译生成占位文件(merge)
用于把 source 消息合并到 translate.xx.toml 中,并添加 hash 字段以标记源内容(便于检测源变更)。
示例:
goi18n merge locales/active.en.toml locales/translate.es.toml
生成或更新 locales/translate.es.toml:
[PersonCats]
hash = "sha1-..."
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
翻译人员将 translate.es.toml 翻译后重命名为 active.es.toml 并放入项目中供运行时加载。
3. 常用选项示例
- 输出格式:
-format toml|json|yaml - 指定输出目录:
-outdir locales - 抽取指定路径:
goi18n extract ./cmd ./pkg
翻译工作流(hash 与更新)
- goi18n 的 merge 会为每条消息生成一个 hash(sha1),基于 message description 与 source other 字段(如 merge 中的实现)。
- 当源消息文本发生改变时,hash 会变化,merge 在下次运行时会把更新后的源推送到
translate.*.toml,并新增/更新 hash,提示翻译人员需要重新翻译。
推荐流程:
- add/modify 源代码的 Message 字面量 → run
goi18n extract更新 source (active.en.toml)。 - run
goi18n merge active.en.toml translate.*.toml更新翻译占位文件(翻译人员收到更新标识)。 - 翻译人员翻译后把
translate.xx.toml重命名为active.xx.toml,或提交到翻译仓库。 - 程序启动时加载
active.xx.toml。
测试、本地调试与常见问题
单元测试中使用 Bundle 与 Localizer:
func TestGreeting(t *testing.T) {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.MustLoadMessageFile("testdata/active.es.toml")
localizer := i18n.NewLocalizer(bundle, "es")
got, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "HelloPerson",
TemplateData: map[string]interface{}{"Name": "Test"},
})
if err != nil {
t.Fatalf("localize error: %v", err)
}
if got != "Hola Test" {
t.Errorf("unexpected: %s", got)
}
}
常见问题:
- “找不到翻译”:检查文件命名规则(例如
active.es.toml),以及 bundle 是否注册了对应 format 的 UnmarshalFunc。 - “模板渲染失败”:检查 TemplateData 是否包含模板中引用的字段名、或自定义定界符是否设置正确。
- “复数选择不对”:确认 PluralCount 是否传入,且翻译文件包含对应复数形态(one/few/other 等)。不同语言的规则在 CLDR 中差异很大。
- “并发”问题:Bundle 在运行时用于 Localizer 可并发读取,但修改 Bundle(如动态添加消息)在并发读取时可能不安全——建议应用启动时一次性加载完所有消息。
进阶指南
1. 回退策略(Fallback)
Bundle 的 defaultLanguage 用于最后回退。Localizer 会依照 language.Matcher 选择最佳已加载 language tag;若无翻译,Localizer 可以使用 DefaultMessage 字段(LocalizeConfig)作为回退。
示例:DefaultMessage 用法:
localizer.Localize(&i18n.LocalizeConfig{
MessageID: "MissingID",
DefaultMessage: &i18n.Message{
ID: "MissingID",
Other: "Default fallback message for {{.Name}}",
},
TemplateData: map[string]interface{}{"Name": "Bob"},
})
2. 自定义复数规则
默认使用 internal/plural 中的 CLDR 规则。如果需要覆盖或添加自定义规则(极少见),可以在创建 Bundle 后调整 bundle.pluralRules,或提供自定义 Rules 实现(需熟悉 internal/plural 的类型签名与规则接口)。
3. 性能与并发
解析模板(parse/compile)是有成本的:把模板文本(例如 {{.Name}} has {{.Count}} cats.)解析成模板对象(*template.Template)需要做字符串解析、AST 构建、可能还要绑定 template.Funcs 等,这一步比“渲染”更重。
渲染模板(execute)也有开销(分配 buffer、格式化变量等),但通常低于反复解析。
为了性能,go-i18n 会在创建 MessageTemplate(即 NewMessageTemplate)时把模板解析/编译一次并缓存,后续 Localize 请求直接重用已编译模板,这样每次请求都只做渲染而不用重复解析。
在高并发场景下,最佳实践是:在程序启动时使用 bundle 一次性加载所有 active.*.toml,这会把消息文件解析成 Message 并构建 MessageTemplate(NewMessageTemplate),从而在加载阶段完成模板解析工作。每次处理请求时,只创建 Localizer 并渲染。
不要在每个请求内重新加载文件或重新解析模板;也不要在请求处理路径频繁修改同一个 Bundle(这在库里不是并发安全的)。如果需要动态更新翻译(热加载),使用“原子替换整个 bundle”或用合适的锁/原子包装,避免在被并发读取时修改内部状态。
常用代码片段(速查)
创建 bundle 并注册 toml:
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
加载消息文件(磁盘与 embed):
bundle.LoadMessageFile("locales/active.es.toml")
// or embed FS:
bundle.LoadMessageFileFS(embedFS, "locales/active.es.toml")
新增消息(程序化):
bundle.AddMessages(language.Spanish, &i18n.Message{
ID: "SayHi",
Other: "Hola {{.Name}}",
})
创建 Localizer 并 localize:
localizer := i18n.NewLocalizer(bundle, "es", "en-US;q=0.8")
s, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "PersonCats",
TemplateData: map[string]interface{}{"Name": "Ana", "Count": 3},
PluralCount: 3,
})
使用 DefaultMessage 回退:
s, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "Unknown",
DefaultMessage: &i18n.Message{
ID: "Unknown",
Other: "Default Hello {{.Name}}",
},
TemplateData: map[string]interface{}{"Name": "Joe"},
})
调试与排错建议
- 打开日志或在本地临时输出
bundle.LanguageTags()查看已加载的语言 tags,确认语言是否被正确注册。 - 如果消息未被加载,检查文件名(tag 部分)、注册的 UnmarshalFunc 是否匹配文件格式后缀。
- 使用
localizer.Localize返回的 error 信息进行定位(缺少 template data、模板语法错误、缺少 plural form 等)。 - 当翻译未更新时,确认 merge 过程中 hash 是否发生变化以及翻译人员是否把
translate.xx.toml重命名为active.xx.toml。
参考资料
- go-i18n README 与文档(仓库): https://github.com/nicksnyder/go-i18n
- pkg.go.dev 文档(i18n 包): https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/i18n
- CLDR 复数规则: https://unicode.org/cldr/








