go-i18n/v2是当前最稳的极简国际化方案,因其API明确无全局变量、Bundle全局单例、Localizer每请求新建、JSON资源扁平化、fallback强制显式声明且严格遵循BCP 47语言标签规范。

gin 本身不带翻译能力,要实现“极简国际化”,核心不是堆功能,而是选对库、压平配置路径、避开 i18n.Handler() 这类隐式中间件带来的上下文污染和语言 fallback 混乱。
用 go-i18n/v2 替代 gin-gonic/contrib/i18n
gin-gonic/contrib/i18n 已归档多年,文档缺失、不支持 Go module v2+、SetMessage 接口直接写死全局状态,多语言切换时容易串 locale。而 go-i18n/v2 是当前维护最活跃的 i18n 库,API 明确、无全局变量、支持 lazy load 和 bundle 隔离。
- 它把翻译资源封装成
*i18n.Bundle实例,每个实例独立管理语言包,不会互相干扰 - 加载文件用
bundle.ParseFS或bundle.ParseDir,支持 JSON/YAML/TOML,推荐 JSON(无需额外依赖) - 翻译函数返回
string而非error,失败时默认回退到 key 本身,避免 panic 或空字符串
Bundle 初始化必须指定 fallback locale
不设 fallback,Localize 在找不到目标语言时会 panic。这不是 bug,是设计——它强制你面对“缺译”问题。
- 初始化时必须调用
bundle.RegisterUnmarshalFunc("json", json.Unmarshal) -
bundle.NewBundle(language.English)中传入的是 fallback 语言,不是默认语言 - 真正生效的语言由后续
localizer.Localize(&i18n.LocalizeConfig{...})的Language字段决定 - 如果传入
language.Japanese但没加载ja.json,就会 fallback 到 English,而不是静默失败
在 Context 里存 Localizer,别存 Bundle
把 *i18n.Bundle 放进 c.Set("i18n", bundle) 是常见错误。Bundle 是 heavy 对象,且含 unmarshaler 等不可序列化字段;每次请求都 new 一个 *i18n.Localizer 才是正解。
- 从请求头读
Accept-Language:用language.ParseAcceptLanguage(c.GetHeader("Accept-Language")) - 构造 localizer:
l := bundle.Localizer(langs...),langs是按优先级排序的language.Tag切片 - 存进 context:
c.Set("localizer", l),体积小、线程安全、可复用 - 模板中调用:
{{.Localize "welcome_message"}},需提前注册 template func
JSON 翻译文件命名必须匹配 language.Tag 字符串
en.json 和 en-US.json 是两个完全不同的 tag,language.Make("en") ≠ language.Make("en-US")。Go 的 x/text/language 对 tag 匹配非常严格。
- 推荐统一用 BCP 47 基础语言码:
en.json、zh.json、ja.json(而非zh-Hans.json) - 如果需要区分简繁,再加一层:
zh-Hans.json+zh-Hant.json,但 fallback 必须明确,比如zh-Hans→zh→en - 文件内容格式必须是扁平 key-value:
{"welcome_message": "Welcome!"},嵌套结构不被原生支持 - 不要用
.ini或.toml—— 它们需要额外 unmarshaler,增加出错面,JSON 最省心
Localizer 实例轻量、Bundle 全局单例、locale 解析走标准库、翻译键不带嵌套——这四条守住,就真算极简了。


















