go-i18n/v2 是最稳选择,因 golang.org/x/text/message 不支持运行时语言切换,其 Printer 一旦创建格式即固化;它适合短文本格式化,而非按请求绑定语言的页面文案场景。

go-i18n/v2 是目前最稳的选择,别用 golang.org/x/text/message 做运行时语言切换
它压根不是为动态语言切换设计的:message.NewPrinter 一创建,格式规则就固化了。改 language.Tag 或系统 locale 都没用,printer.Printf("Hello") 还是输出英文——你看到的不是“没切成功”,而是“根本没打算支持”。
- 常见错误现象:注册了翻译但始终 fallback 到源字符串,因为漏了
message.Catalog绑定 - 以为 reload 文件就能生效,结果静默失败(
message.Printer不支持热重载) -
golang.org/x/text/message适合短文本格式化(如数字、日期),但不适合页面文案、错误提示这类需按请求绑定语言的场景 - 真正要的是按请求绑定语言:
go-i18n/v2自带Bundle管理、Localizer实例化、热重载(注意:仅对文件系统有效,embed.FS中需手动检查fs.Stat)
资源文件命名和结构必须严格符合 BCP 47 + JSON Schema
错一个字符,bundle.ParseFS 就会静默失败,返回空字符串而非报错。
- 文件名必须是
active.zh-CN.json,不能是zh.json、zh_CN.json或cn.json - 路径必须匹配
os.DirFS("./locales")下的真实层级,比如./locales/active.en-US.json - JSON 内容必须严格:
{"welcome": {"description": "homepage greeting", "translation": "欢迎"}}✅{"welcome": "欢迎"}❌(缺少description和外层对象结构){"welcome": {"msg": "欢迎"}}❌(字段名必须是translation) - 只加载
active.*前缀的文件,inactive.*会被忽略——这是故意设计,避免误加载未审核翻译
Accept-Language 解析不能直接信 r.Header.Get("Accept-Language")
原始 header 可能为空、超长、含非法 tag(如 en;q=0.9, fr-XX;q=0.8),甚至触发 panic。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确做法是用
language.ParseAcceptLanguage,它自动排序、过滤无效项、做标准化 - 再配合已注册语言集做安全匹配:
先调language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))得到有序候选列表
遍历该列表,用language.MatchStrings(supportedLangs, candidate.String())查第一个合法匹配
没匹配上就 fallback 到默认语言(如language.Make("en")),别硬 fallback 到en-US - 缓存解析结果,避免每次请求都重复调
language.Parse——它不是纯函数,内部有 map 查找开销
HTTP 请求中怎么动态切换语言而不重启服务
不能每次请求都新建 *i18n.Localizer,它不是 goroutine-safe 的,且初始化开销大;也不能全局单例一个 localizer,因为语言要随请求变化。
立即学习“go语言免费学习笔记(深入)”;
- 推荐做法:全局持有一个
*i18n.Bundle,每个请求根据Accept-Language或 URL 参数(如?lang=ja)调用bundle.NewLocalizer创建临时localizer -
bundle.NewLocalizer很轻量,只要bundle已预加载好资源,它只是做一次 map 查找,可安全用于高并发场景 - 如果用了 Gin/Echo,把
localizer注入到context.Context中比塞进 struct field 更清晰,也方便中间件统一处理 - 别把
localizer当成模板函数直接传进去——html/template不支持运行时切换上下文,必须提前把翻译结果或闭包函数传入 data map
最容易被忽略的是:语言标签的语义差异(比如 zh-CN 和 zh-Hans 不等价)、资源文件路径与 FS 前缀的对齐、以及 bundle.ParseFS 对文件名前缀和 JSON schema 的零容忍——它不报错,只沉默返回空字符串。

















