go-i18n/v2 是最稳的运行时多语言方案,因其支持按请求动态切换语言,而 golang.org/x/text/message 不支持;localizer.Localize 返回空是因配置不合规:路径错、文件名非法(须 active.zh-CN.json)、JSON 缺 description 或字段名错、未调 LoadMessageFile、语言标签未注册;Accept-Language 必须用 language.ParseAcceptLanguage 解析并匹配白名单;Bundle 全局单例,Localizer 每请求新建;模板中需为每次渲染单独构造 Localizer 实例。

go-i18n/v2 是目前最稳的运行时多语言切换方案,别用 golang.org/x/text/message 做页面文案国际化——它压根不支持按请求动态换语言。
为什么 localizer.Localize 返回空字符串或原 key
这不是 bug,是配置没踩中规范导致的静默失败。常见原因包括:
-
bundle.ParseFS加载路径不对:比如用os.DirFS("./locales"),但文件实际在./i18n/active.zh-CN.json,就会找不到 - 文件名不合法:必须是
active.zh-CN.json,zh.json、zh_CN.json、cn.json全部被忽略 - JSON 结构错:必须是
{"welcome": {"description": "greeting", "translation": "欢迎"}},缺description字段或把translation写成msg都会返回空 - 没显式调用
bundle.LoadMessageFile:v2 不自动扫描目录,不调就等于没加载 - 语言标签未注册:调用
bundle.NewLocalizer时传的language.Tag不在已加载资源里,也不会 fallback,直接空
Accept-Language 解析不能直接取 header 字符串
原始 Accept-Language 可能为空、含非法 tag(如 fr-XX)、带权重(en;q=0.9, zh-CN;q=0.8),硬切第一个或字符串匹配必翻车。
- 必须用
language.ParseAcceptLanguage解析,它自动排序、过滤、标准化 - 解析后要跟白名单比对:
supportedLangs := []language.Tag{language.Chinese, language.English},别用字符串切片 - 匹配用
matcher.Match,不是==;zh-Hans和zh-CN语义不同,但 matcher 能归一化识别 - 结果建议缓存到
req.Context(),避免每次 handler 都重复调用——ParseAcceptLanguage内部有 map 查找开销
每个请求该 new Localizer 还是复用 Bundle
全局只持一个 *i18n.Bundle,启动时一次性加载所有 active.*.json;每个请求用 bundle.NewLocalizer(lang) 构造临时 *i18n.Localizer。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
-
*i18n.Bundle初始化成本高,不能每次请求都 new;*i18n.Localizer很轻量,只是 map 查找,可安全并发使用 - 别把
*i18n.Localizer塞进context.WithValue全链路透传——类型不安全、易漏、难调试 - 推荐在中间件里解析出
lang,然后构造localizer,作为参数传给 handler 闭包,或注入到 struct 方法接收器 - 若用 Gin/Echo,可在 middleware 中
c.Set("localizer", loc),handler 里c.MustGet("localizer").(*i18n.Localizer)
模板里怎么安全调用翻译函数
html/template 不支持闭包捕获上下文,不能把 localizer.Localize 直接塞进 FuncMap 并指望它“记住”当前语言。
- 必须为每次渲染单独构造
*i18n.Localizer实例,再包装成模板函数 - 典型写法:
"T": func(id string, args interface{}) template.HTML { return template.HTML(loc.Localize(&i18n.LocalizeConfig{MessageID: id, TemplateData: args})) } - 注意
LocalizeConfig必须传MessageID,不是translation文本;TemplateData是 map 或 struct,用于占位符填充 - 如果用了
embed.FS打包资源,热重载失效——需手动检查fs.Stat变更并重新LoadMessageFile
最容易被忽略的是 JSON 文件结构和语言标签的 BCP 47 合规性:错一个连字符、少一个字段、命名多一个下划线,bundle.ParseFS 都不会报错,只会静默返回空字符串——你得靠日志或单元测试去卡住这些点。

















