go-i18n/v2 是目前唯一能真正落地的运行时多语言方案,支持动态切语言、cldr复数规则、json/toml热加载、rtl适配,且bundle全局单例、localizer每请求新建。

go-i18n/v2 是目前最稳的运行时方案
别用 golang.org/x/text/message 做页面文案多语言切换——它不支持运行时改语言,message.NewPrinter 一旦创建,language.Tag 就固化了,后续任何 context 或 header 修改都无效。线上看到“切不动语言”,八成是误用了这个包。
github.com/nicksnyder/go-i18n/v2/i18n 是当前唯一能真正落地的运行时方案:支持按请求动态绑定语言、CLDR 复数规则、JSON/TOML 热加载(仅限文件系统)、RTL 渲染适配,且 API 明确区分 *i18n.Bundle(全局单例)和 *i18n.Localizer(每请求新建)。
安装命令必须是:go get github.com/nicksnyder/go-i18n/v2/i18n。其他变体如 v1 或无 /v2 后缀的路径,要么已废弃,要么行为不一致。
Localize 返回空字符串或原始 key 的真实原因
这不是 bug,是配置未满足硬性规范导致的静默失败。常见原因包括:
- 文件名不合法:必须是
active.zh-Hans.json,zh.json、zh_Hans.json、active-zh-hans.json全部被忽略 - 路径错误:用
os.DirFS("./locales"),但文件实际在./i18n/active.en-US.json,加载直接失败 - JSON 结构错:必须是
{"login.title": {"description": "page title", "translation": "登录"}};缺description字段、把translation写成msg或value,都会返回空 - 没调
bundle.LoadMessageFile:v2 不自动扫描目录,不显式加载就等于没注册语言资源 - 语言标签未注册:传给
bundle.NewLocalizer的language.Tag没对应已加载的active.*.json文件
Accept-Language 解析必须用 language.ParseAcceptLanguage
浏览器发来的 Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7 不是普通逗号分隔字符串,而是带权重、可嵌套、需标准化的语言标签序列。手动 strings.Split 或正则提取会丢权重、误判变体(比如把 zh-Hans 当作和 zh-CN 不兼容),最终静默 fallback 到默认语言。
正确做法是:
- 调
language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))得到有序[]language.Tag - 预定义白名单:
supported := []language.Tag{language.Chinese, language.English} - 初始化一次 matcher:
matcher := language.NewMatcher(supported) - 匹配:
matched, _ := matcher.Match(acceptTags),返回归一化后的language.Tag(如zh-Hans) - 结果建议存入
req.Context(),避免每次 handler 都重复解析(内部有 map 查找开销)
每个 HTTP 请求都该新建 Localizer,但别每次都重载 Bundle
*i18n.Bundle 是线程安全的,负责管理所有语言资源,应在应用启动时一次性加载全部 active.*.json 文件;而 *i18n.Localizer 是轻量、非线程安全、按需绑定语言的实例,必须每个请求生成一个。
典型错误是:
- 在中间件里全局缓存
*i18n.Localizer实例,导致并发请求语言混用 - 在 struct 字段里存
*i18n.Localizer,复用时语言错乱 - 把
*i18n.Localizer塞进context.WithValue全链路透传,类型不安全、易漏、难调试
推荐做法:在中间件中解析出 language.Tag,调 bundle.NewLocalizer(tag) 构造实例,然后作为参数传给 handler 闭包,或注入到 handler struct 的方法接收器中。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











