本教程面向想在 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 对多语言文件进行提取、合并、更新。
goi18n extract:从 Go 源码中提取 i18n.Message 字面量,生成“源语言(active)”消息文件(通常是active.en.*)。goi18n merge:把已有的 active/translate 文件进行合并,生成翻译模板或把翻译合并回 active 文件,常用来生成translate.*或更新active.*。
merge 是最容易产生困惑的命令:它既可以用于“从 active 生成 translate 模板”,也可以“把 translate 的翻译合并回 active”。
安装 goi18n 命令工具
go-i18n 本身只是一个库,文本的提取与合并依赖命令行工具 goi18n。
执行安装命令:
go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest
安装完后,你可以检查帮助:
goi18n -help
如果输出了命令说明,表示安装成功。
核心概念:active 与 translate 文件
在 go-i18n 中:
- active.xx.toml
→ 已经完成翻译、系统实际使用的语言文件。
例如:
active.en.toml(英文)active.es.toml(西班牙语)
- translate.xx.toml
→ 待翻译文件,goi18n 会把“缺失的文本”放进这里。
翻译完成后,用户需要手动改名为
active.xx.toml。
你可以把它理解成:
| 文件类型 | 用途 |
|---|---|
| active.xx.toml | 已翻译、已生效 |
| translate.xx.toml | 待翻译草稿 |
初次使用:在 Go 代码中写国际化文本
你需要在 Go 代码中写 i18n.Message,例如:
&i18n.Message{
ID: "HelloPerson",
Other: "Hello {{.Name}}",
}
这些结构体就是 goi18n 提取翻译文本的来源。
第一次生成语言文件(英文)
运行:
goi18n extract
它会扫描你的 Go 代码,并生成:
active.en.toml
文件内容可能类似:
[HelloPerson]
other = "Hello {{.Name}}"
这就是你的“主语言文件”(一般是英文)。
extract 命令用法和参数:
- usage:
goi18n extract [options] [paths] - 参数:
-sourceLanguage用于指定源语言标签(如en、zh-Hant-CN,默认en);-outdir指定输出目录(默认当前目录.);-format指定生成文件的格式,支持json、toml和yaml,默认toml。
添加一种新语言(例如西班牙语)
1)创建一个空文件
你需要手动创建文件:
translate.es.toml
内容先留空即可。
2)让 goi18n 把英文内容填进去
执行:
goi18n merge active.en.toml translate.es.toml
运行后,translate.es.toml 会变成:
[HelloPerson]
hash = "sha1-xxxxx"
other = "Hello {{.Name}}"
这表示:
- goi18n 自动将英文内容复制到待翻译文件
hash用来检测未来文本是否被修改
merge 命令用法和参数:
- usage:
goi18n merge [options] [message files] - 参数和 extract 命令参数相同
3)手动翻译 translate 文件
把内容改成你要的翻译,例如:
# active.es.toml(翻译完成后需要改名!)
[HelloPerson]
hash = "sha1-xxxxx"
other = "Hola {{.Name}}"
4)将翻译后的文件改名为 active 文件
更名:
translate.es.toml → active.es.toml
至此,西班牙语已准备就绪。
在 Go 代码中加载语言文件
示例(使用 toml):
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFile("active.en.toml")
bundle.LoadMessageFile("active.es.toml")
加载后就能通过 localizer 获取翻译文本。
当你新增了新的 i18n 文本时
每次新增消息(例如新增一个 Message.ID),你必须重新同步所有语言文件。
步骤 1:重新提取默认语言文本
goi18n extract
更新后的 active.en.toml 会包含新增的文本。
步骤 2:更新所有翻译文件(生成 translate.xx.toml)
执行:
goi18n merge active.*.toml
goi18n 会自动生成:
translate.es.toml
translate.fr.toml
translate.zh.toml
...
这些文件包含“缺失的翻译内容”。
步骤 3:翻译 translate 文件
步骤 4:将翻译文件合并回 active 文件
执行:
goi18n merge active.*.toml translate.*.toml
goi18n 会自动更新各语言的 active 文件:
active.es.toml(新增的翻译项被合并)
active.fr.toml
active.zh.toml
之后你的项目就可以使用最新翻译了。
go-i18n 工作流总览
# 1. 提取英文(生成/更新 active.en.toml)
goi18n extract
# 2. 生成/更新 各语言 translate 文件
goi18n merge active.*.toml
# 3. 翻译 translate.xx.toml(手工)
# 4. merge 翻译内容回 active.xx.toml
goi18n merge active.*.toml translate.*.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/








