accept-language解析必须用language.parseacceptlanguage:手动切分会丢失权重、误判变体,导致静默fallback;文件名须为active.zh-cn.json,json须含description和translation字段,bundle全局单例,localizer每请求新建。

Accept-Language 解析必须用 language.ParseAcceptLanguage
浏览器发来的 Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8 不是普通字符串,手动 strings.Split 或正则提取会丢权重、误判变体(比如把 zh-Hans 当作不兼容),最终静默 fallback 到默认语言,且无任何报错提示。
正确做法是:
- 调用
language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))得到标准化、已排序的[]language.Tag - 预定义白名单:
supported := []language.Tag{language.Chinese, language.English, language.Japanese} - 初始化一次
matcher := language.NewMatcher(supported),别在 handler 里重复 new - 匹配时用
matched, _ := matcher.Match(acceptTags...),返回归一化后的language.Tag(如zh-Hans→zh) - header 为空或含非法 tag(如
fr-XX)时,ParseAcceptLanguage会静默跳过,不会 panic
go-i18n/v2 资源加载对文件名和 JSON 结构极其敏感
bundle.ParseFS 加载失败时不会报错,只会返回空字符串或原始 key —— 这是最常踩的坑,也是线上 debug 成本最高的点。
必须严格满足以下四条:
- 文件名必须是
active.zh-CN.json(不是zh.json、zh_CN.json、cn.json、zh-CN.json) - 路径必须匹配
os.DirFS("./locales"):文件得放在./locales/active.zh-CN.json,少一层或多一层目录都不行 - JSON 结构必须含
description和translation字段:{"welcome": {"description": "homepage greeting", "translation": "欢迎"}}✅;{"welcome": "欢迎"}❌ - 只加载
active.*.json文件,inactive.*.json被设计为自动忽略,不是 bug
Echo 中间件里每请求新建 *i18n.Localizer
*i18n.Localizer 不是全局单例,也不能复用。它轻量(只是 map 查找),但必须按请求构造,否则并发下语言混用。
推荐在 Echo 中间件中完成三件事:
- 从
c.Request()解析并匹配出language.Tag,缓存到c.Set("lang", tag) - 调用全局已初始化的
bundle.NewLocalizer(tag)得到新实例 - 封装成闭包函数
T := func(msgID string) string { return localizer.Localize(&i18n.LocalizeConfig{MessageID: msgID}) },再存入c.Set("T", T) - 不要用
context.WithValue全链路透传 —— 类型不安全,且 Echo 的c.Get更直接 - 别把
localizer塞进模板上下文直接调用:模板里应只用T("auth.login.title"),而非localizer.Localize(...)
为什么不能用 golang.org/x/text/message 做 Echo 页面文案
message.NewPrinter 一旦创建,内部 language.Tag 就固化了。改 r.Header、换 context、甚至重启服务都无效 —— 所有 handler 共用一个 printer 就必然混语言输出。
它只适合短文本本地化格式化,比如:
-
printer.Sprint(1234567.89)输出带千分位的数字 -
printer.Sprintf("You have %d message", n)带 CLDR 复数规则 - 不适合按钮文案、API 错误提示、HTML 模板渲染等需绑定请求语言的场景
- 注册翻译必须提前调
message.Catalog.SetString,漏掉 key 就静默回退到英文 - 没有资源热加载、无 JSON 支持、不校验结构 —— 维护成本远高于 go-i18n/v2
最易被忽略的是 JSON 文件里 description 字段的存在性,以及文件名中连字符与大小写的 BCP 47 合规性 —— 这两点出错,Localize 一定返回空,且没有任何日志提示。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











