直接用 github.com/nicksnyder/go-i18n/v2/i18n,别碰 golang.org/x/text/message 做页面文案——它不支持运行时换语言,硬上只会返回英文还查不出原因;因其 message.NewPrinter 创建后语言即固化,无法响应请求动态切换,仅适用于短文本格式化,且资源加载、Accept-Language 解析、Bundle/Localizer 复用等均需严格遵循规范,否则静默失败。

直接用 github.com/nicksnyder/go-i18n/v2/i18n,别碰 golang.org/x/text/message 做页面文案——它不支持运行时换语言,硬上只会返回英文还查不出原因。
为什么不能用 golang.org/x/text/message 做动态翻译
这个包的 message.NewPrinter 一创建,语言就锁死了。改 context、换 header、甚至重启服务,printer.Printf("hello") 还是输出原始语言。它只适合短文本格式化(比如 printer.Sprint(1234567.89) 输出带千分位的数字),不适合按钮、错误提示、HTML 模板这类需按请求绑定语言的场景。
常见错误包括:
- 注册了翻译却始终返回原始 key,因为漏了
message.Catalog绑定 - 以为 reload 文件就能生效,实际
Printer不支持热重载 - 用字符串拼接多语言文本(如
"欢迎 " + name + "!"),在德语、阿拉伯语里会因词序、格变化、复数规则直接崩掉
go-i18n/v2 的资源文件必须严格合规
文件名、路径、JSON 结构错一个字符,bundle.ParseFS 就静默失败——不 panic,也不报错,Localize 只返回空字符串或原始 key,极难排查。
立即学习“go语言免费学习笔记(深入)”;
必须满足以下全部条件:
- 文件名必须是
active.zh-Hans.json(不是zh.json、zh_Hans.json、active-zh-hans.json或zh-CN.json) - 路径要匹配
os.DirFS("./locales"):文件得放在./locales/active.zh-Hans.json,不能少一层目录 - JSON 内容必须含
description和translation字段:{"login.title": {"description": "page title", "translation": "登录"}}✅;{"login.title": "登录"}❌ - 只加载
active.*前缀文件,inactive.*被自动忽略——这是设计行为,不是 bug
HTTP 请求中如何安全解析并匹配语言
别直接信 r.Header.Get("Accept-Language"),原始 header 可能为空、超长、含非法 tag(如 fr-XX),甚至触发 panic。必须走标准流程:
- 用
language.ParseAcceptLanguage解析,它自动排序、过滤无效项、标准化标签 - 预定义白名单:
supported := []language.Tag{language.Chinese, language.English} - 初始化一次 matcher:
matcher := language.NewMatcher(supported) - 匹配请求 tag:
matched, _ := matcher.Match(acceptTags),返回归一化后的language.Tag(如zh-Hans) - 缓存解析结果:该函数内部有 map 查找开销,重复调用浪费 CPU;建议用
sync.Map缓存Accept-Language字符串到language.Tag的映射
Bundle 和 Localizer 怎么复用才对
*i18n.Bundle 是线程安全的,负责管理所有语言资源,应该全局单例、启动时一次性 LoadMessageFile 所有支持语言的资源;而 *i18n.Localizer 是轻量、非线程安全、按需绑定语言的实例,必须每个请求生成一个。
典型错误包括:
- 每次请求都重载
bundle:初始化成本高,没必要 - 全局单例一个
localizer:语言无法随请求变化,导致并发下语言错乱 - 把
localizer塞进context.WithValue全链路透传:类型不安全、易漏、难调试
推荐做法:中间件里解析语言、调用 bundle.NewLocalizer(matched) 创建临时实例,然后作为参数传给 handler 闭包,或注入到 struct 方法接收器。
最易被忽略的一点:没有显式调用 bundle.LoadMessageFile,v2 不自动扫描目录,不加载就等于没注册语言资源——这比文件名写错还隐蔽,因为连 warning 都没有。


















