go-i18n/v2 是最稳妥的 Go 国际化方案,支持运行时语言切换、热重载、CLDR 复数规则与 RTL 适配;需用 language.ParseAcceptLanguage 解析 Accept-Language,显式声明白名单并用 language.NewMatcher 匹配,JSON 文件名须为 active.zh-CN.json 且含 description 字段,Localizer 应按请求创建而非全局复用,参数名大小写敏感且需传原始值供模板格式化。

go-i18n/v2 是当前最稳妥的 Go 国际化方案,它支持运行时按请求切换语言、热重载(文件系统下)、CLDR 复数规则、RTL 适配,且 JSON/TOML 结构清晰、错误反馈明确。别用 golang.org/x/text/message 做动态语言切换——它初始化后 language.Tag 就锁死了,换 locale 无效。
为什么 Accept-Language 解析必须用 language.ParseAcceptLanguage
浏览器发来的 Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7 不是简单逗号分隔字符串,而是带权重、含非法标签、需标准化的序列。手写 strings.Split 或正则会漏掉权重排序、误判 zh-Hans 和 zh-CN 的兼容性,最终静默 fallback 到默认语种。
- 必须调
language.ParseAcceptLanguage(r.Header.Get("Accept-Language")),它返回已排序、过滤、标准化的[]language.Tag - 白名单要显式声明:
supported := []language.Tag{language.Chinese, language.English, language.Japanese},别用字符串切片 - 匹配必须用
matcher := language.NewMatcher(supported),再调matcher.Match(tag);直接==会失败 - 解析结果建议缓存到
r.Context(),避免每次 handler 都重复调用——内部有 map 查找开销
go-i18n/v2 加载 JSON 文件的命名和结构陷阱
bundle.ParseFS 找不到翻译?大概率不是路径错,而是文件名或 JSON 结构不合规——它不报错,只静默返回空字符串或原始 key。
- 文件名必须是
active.zh-CN.json,不能是zh.json、zh_CN.json、cn.json或active_zh-CN.json - 路径必须匹配
os.DirFS("./locales")的根:即文件得在./locales/active.zh-CN.json - JSON 必须是外层对象,每个 key 是 message ID,value 是
{"description": "...", "translation": "..."};缺description字段也会静默失败 - 只加载
active.*.json,inactive.*.json被忽略——这是设计行为,不是 bug
每个请求该用哪个 *i18n.Localizer 实例
*i18n.Localizer 不是全局单例,也不能每次请求都重建 *i18n.Bundle——前者不安全,后者性能差。正确做法是复用 Bundle,按需生成 Localizer。
- 全局只持有一个
*i18n.Bundle,启动时一次性调bundle.LoadMessageFileFS加载所有active.*.json - 每个请求根据解析出的
language.Tag,调bundle.NewLocalizer(tag)创建轻量 localizer(只是 map 查找) - 别把 localizer 塞进
context.WithValue全链路透传——类型不安全、易漏、难调试;推荐在中间件里生成,作为参数传给 handler - Localize 方法签名是
localizer.Localize(&i18n.LocalizeConfig{...}),不是localize("key")——v2 版本 API 彻底重构了
动态参数传入时最容易踩的坑
参数名大小写敏感、类型未对齐、格式化逻辑前置,都会导致占位符原样输出,比如渲染成 "Hi {Name}" 而非 "Hi Alice"。
立即学习“go语言免费学习笔记(深入)”;
- 翻译模板里写
"welcome": "Hi {Name}",代码里必须传map[string]interface{}{"Name": "Alice"}——"name"或"NAME"都不匹配 - 数字、时间、货币等不要在 Go 层拼好再传:比如
"price": "¥{Price} (tax included)"应传原始float64,让模板引擎处理格式化(需配合{Price, number, currency}语法) - 别在
fmt.Sprintf里拼多语言文本——中文看似能跑,但德语词序、俄语格变、阿拉伯语 RTL 会直接崩掉
active.zh-CN.json 少了个 description 字段、Accept-Language 解析没走 language.ParseAcceptLanguage、或者把 *i18n.Localizer 当全局变量用了——这些地方一错,就只剩空字符串和原始 key 在日志里沉默。


















