用 Gin 做多语言应选用 go-i18n/v2,严格遵循 BCP 47 规范:Accept-Language 必须用 language.ParseAcceptLanguage 解析,资源文件名须为 active.zh-CN.json 且路径正确,JSON 结构需含 description 字段,每个请求新建 Localizer 而非复用,中间件统一注入翻译函数 T。

用 Gin 做多语言,别碰 golang.org/x/text/message——它不支持运行时切换语言,message.NewPrinter 一创建就固化了语言,后续改 header、换 context 都无效。真正能跑通的只有 go-i18n/v2,且必须严格按 BCP 47 规范组织资源、解析请求头、构造 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:
matcher := language.NewMatcher(supported),别每次请求都 new - 匹配结果是
language.Tag类型,不是字符串——传给bundle.NewLocalizer时直接用,别再转成"zh-CN" - 建议缓存解析结果:用
sync.Map缓存Accept-Language字符串到language.Tag的映射,避免重复解析开销
go-i18n/v2 资源文件命名和结构不能错一个字符
bundle.ParseFS 静默忽略所有不合规文件,不报错,只返回空字符串或原始 key。这不是 bug,是设计行为。
- 文件名必须是
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,值必须是
{"description": "xxx", "translation": "yyy"} - 缺
description字段、字段名写成msg或value、外层不是对象结构,全都会被跳过 - 只加载前缀为
active.的文件,inactive.*或translate.*被自动忽略
每个 HTTP 请求必须新建 *i18n.Localizer,不能复用
*i18n.Localizer 不是 goroutine-safe 的,但也不需要每次都重建 *i18n.Bundle——后者初始化成本高,前者轻量可按需构造。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 全局只持有一个
bundle := i18n.NewBundle(language.English),启动时一次性调bundle.ParseFS(fs, "locales/active.*.json")加载全部资源 - 每个请求中,根据解析出的
langTag调localizer := bundle.NewLocalizer(langTag)——这个操作很轻,不用缓存 - 调
localizer.Localize(&i18n.LocalizeConfig{MessageID: "auth.login.title"}),注意是方法调用,不是函数 - 别把
localizer塞进context.WithValue全链路透传——类型不安全、易漏、难调试;推荐在中间件里生成后作为参数传给 handler 闭包
Gin 中间件里怎么安全注入翻译能力
直接在 handler 里每请求 new localizer 也行,但更推荐统一收口到中间件,避免重复逻辑。
- 中间件里解析
Accept-Language得到langTag,再调bundle.NewLocalizer(langTag) - 不要返回
localizer.Localize函数本身——它签名复杂,且容易漏传*i18n.LocalizeConfig - 推荐封装一个闭包函数:
T := func(id string) string { return localizer.Localize(&i18n.LocalizeConfig{MessageID: id}) } - 通过
c.Set("T", T)注入,handler 里用c.MustGet("T").(func(string) string)("welcome")调用 - 若用 struct handler 模式,可把
*i18n.Localizer作为字段注入,方法接收器直接调用l.localizer.Localize(...)
最常被忽略的是:文件名大小写、连字符顺序、description 字段缺失、active. 前缀遗漏——这些都不会报错,只会让 Localize 返回空字符串或原始 key,排查时容易卡在“为什么没生效”上,而不是检查命名规范本身。

















