golang.org/x/text/message 不适合框架级多语言,因其 NewPrinter 创建后 language.Tag 固化,无法按请求动态切换语言;仅适用于短文本格式化,不支持复数、词序、RTL 及区域变体处理。

别用 golang.org/x/text/message 做框架级多语言——它不支持按请求切换语言,硬上只会输出错语言且查不出原因。
为什么 message.NewPrinter 不能用于 Web 框架文案
它创建后内部的 language.Tag 就固化了,改 r.Header、换 context、甚至重启服务都无效。所有 handler 共用一个全局 printer,必然导致中文请求里混出英文按钮或错误提示。
- 适合场景仅限短文本格式化:比如
printer.Sprint(1234567.89)输出带千分位的数字 - 不适合按钮文案、模板渲染、API 错误消息等需绑定请求语言的文本
- 没有复数、词序、RTL、区域变体(如
zh-CNvszh-Hans)处理能力
go-i18n/v2 的资源加载必须严格符合 BCP 47 + JSON Schema
文件名错一个字符、字段少一个字母、路径层级不对,bundle.LoadMessageFile 或 bundle.ParseFS 都会静默失败——不报错,只返回空字符串或原始 key。
- 文件名必须是
active.zh-Hans.json(不是zh.json、zh_CN.json、zh-hans.json) - 路径要匹配
os.DirFS("./locales"):文件必须放在./locales/active.zh-Hans.json - JSON 结构必须含
description和translation字段:{"login.title": {"description": "page title", "translation": "登录"}} - 只加载
active.*.json,inactive.*.json被设计为自动跳过
HTTP 请求中解析 Accept-Language 的唯一安全方式
浏览器发来的 Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7 不是普通逗号分隔字符串,手动 strings.Split 或正则提取会丢权重、误判变体(如把 zh-Hans 当作和 zh-CN 不兼容),最终静默 fallback 到默认语言。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 必须用
language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))得到有序[]language.Tag - 白名单提前定义:
supported := []language.Tag{language.Chinese, language.English} - 用
matcher := language.NewMatcher(supported)初始化一次,再调matcher.Match(candidate)获取归一化后的 tag(如zh-CN→zh-Hans) - 结果建议存入
req.Context(),避免每次 handler 都重复解析(内部有 map 查找开销)
*i18n.Localizer 必须每请求新建,但 *i18n.Bundle 必须全局单例
*i18n.Bundle 是线程安全的、负责管理所有语言资源,应该在启动时一次性 LoadMessageFile 所有支持语言;而 *i18n.Localizer 是轻量、非线程安全、按需绑定语言的实例,必须每个请求生成一个。
- 别在
init()里建全局Localizer,也别缓存或复用它 - 每个请求拿到匹配后的
language.Tag后,立刻调bundle.NewLocalizer(tag) -
Localize返回空或原 key,90% 是因为资源没加载成功,而不是翻译没写对 - 所有
i18n.LoadMessageFile调用后必须检查err != nil,否则后续Localize会 panic:“no message found for key”
最易被忽略的点是:JSON 文件里缺 description 字段、文件名大小写/连字符不规范、路径比 os.DirFS 根目录多或少了一层——这些都不会报错,只会让 Localize 默默返回 key 本身,debug 成本极高。

















