github.com/nicksnyder/go-i18n/v2/i18n是目前最稳的落地选择,因其支持按请求动态切换语言、热重载、cldr规则及严格资源校验;而golang.org/x/text/message不支持运行时切换,仅适用于编译期格式化。

用 github.com/nicksnyder/go-i18n/v2/i18n 是目前最稳的落地选择,别碰 golang.org/x/text/message 做动态语言切换——它不支持运行时改语言,硬上只会输出错语言还查不出原因。
为什么不能直接用 language.ParseAcceptLanguage 的结果当语言 ID
浏览器发来的 Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8 经 language.ParseAcceptLanguage 解析后,得到的是带权重、已排序的 []language.Tag,比如 [zh-cn zh en-us]。但你的系统只支持 zh-Hans 和 en,直接拿 zh-cn 去加载资源会失败(active.zh-CN.json ≠ active.zh-Hans.json)。
必须走 matcher 匹配:
- 预定义白名单:
supported := []language.Tag{language.Chinese, language.English} - 初始化一次:
matcher := language.NewMatcher(supported) - 匹配请求 tag:
matched, _ := matcher.Match(acceptTags...),返回的是归一化后的language.Tag,比如zh-Hans - 这个
matched才能安全传给bundle.NewLocalizer
active.zh-Hans.json 文件名和结构必须完全合规
go-i18n/v2 对文件名和 JSON 结构极其敏感,错一个字符就静默失败(返回空字符串或原始 key,不 panic,极难排查)。
正确写法:
- 文件名必须是
active.zh-Hans.json(不是zh.json、zh_Hans.json、zh-hans.json) - 路径要对:如果用
os.DirFS("./locales"),那文件必须在./locales/active.zh-Hans.json - JSON 内容必须含
description和translation字段:{"login.title": {"description": "page title", "translation": "登录"}} - 不能简写成:
{"login.title": "登录"}—— 这会导致解析成功但后续Localize返回空
每次 HTTP 请求都该新建 *i18n.Localizer,但别每次都重载 bundle
*i18n.Bundle 是线程安全的,且负责管理所有语言资源,应该全局单例初始化;而 *i18n.Localizer 是轻量、非线程安全、按需绑定语言的实例,必须每个请求生成一个。
典型错误:
- 在
init()里创建一个全局localizer→ 所有请求都用同一个语言 - 每次请求都调
bundle.LoadMessageFile→ 文件重复加载,CPU 和 I/O 白耗,且可能因并发导致状态混乱 - 把
localizer存进 struct 字段 → 多个 handler 共享,语言混串
正确姿势:
func handler(w http.ResponseWriter, r *http.Request) {
langTag := parseAndMatchLang(r) // 上面说的 matcher.Match 结果
localizer := bundle.NewLocalizer(langTag) // ✅ 每次新造
msg, _ := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "login.title",
})
fmt.Fprint(w, msg)
}
Localize 返回空或原始 key?先开 WithDebug(true)
常见现象:localizer.Localize 返回 "login.title" 而不是 "登录",但没报错。这不是 bug,是 go-i18n/v2 的默认 fallback 行为:找不到翻译就返回 key。
调试方法:初始化 bundle 时加调试开关:
bundle := i18n.NewBundle(language.English) bundle.WithDebug(true) // ✅ 启用后,缺失翻译会 log.Warn 输出具体 missing key + lang
这能立刻暴露两类问题:
- 文件根本没加载(
LoadMessageFile路径错 / 权限不足 / 格式非法) - key 名不一致(模板里写
login.title,但 JSON 里是login_page.title)
上线前务必关掉 WithDebug,但它在开发期是定位 i18n 问题最快的方式。











