结论:Golang微服务国际化应选用github.com/nicksnyder/go-i18n/v2/i18n;golang.org/x/text/message不支持运行时语言切换,仅适用于静态本地化场景。

直接说结论:Golang 微服务做国际化,github.com/nicksnyder/go-i18n/v2/i18n 是唯一能落地的选择;golang.org/x/text/message 不支持运行时语言切换,硬上只会返回错语言且查不出原因。
为什么不能用 golang.org/x/text/message 做微服务多语言
它不是为动态语言切换设计的。message.NewPrinter 一创建,language.Tag 就固化了——改请求头、换 context、甚至重启服务都没用。你看到的“没切成功”,其实是“根本没这个能力”。
- 适合场景:数字千分位、日期格式化等短文本本地化
- 不适合场景:API 错误提示、HTML 模板文案、按钮文字等需按请求绑定语言的内容
- 常见现象:注册了翻译,但
printer.Printf("login.title")始终输出英文 key,因为漏了message.Catalog绑定,或误以为 printer 支持热重载
go-i18n/v2 的资源加载必须满足三个硬约束
Bundle 加载失败不 panic、不报错,只静默返回空字符串或原始 key,排查成本极高。根源几乎都出在这三点:
- 文件名必须是
active.zh-CN.json(不是zh.json、zh_CN.json、cn.json,大小写和连字符必须严格匹配 BCP 47) - 路径必须与
os.DirFS("./locales")完全一致:文件得放在./locales/active.zh-CN.json,不能少一层或多一层目录 - JSON 结构必须含
description和translation字段:{"login.title": {"description": "page title", "translation": "登录"}}✅;{"login.title": "登录"}❌
Accept-Language 解析和语言匹配不能跳步
浏览器 Header 的 Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8 不是字符串列表,手动 strings.Split 会丢权重、错判变体(比如把 zh-Hans 当作不兼容),最终 fallback 到默认语言却找不到原因。
立即学习“go语言免费学习笔记(深入)”;
- 必须用
language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))解析,得到已排序、标准化的[]language.Tag - 白名单要用
[]language.Tag构建:supported := []language.Tag{language.Chinese, language.English},别用字符串数组 - 匹配必须走
matcher := language.NewMatcher(supported),再调matcher.Match(tag);返回的matched才是归一化后的合法标签(如zh-Hans),可安全传给bundle.NewLocalizer - 建议将解析结果缓存到
req.Context(),避免重复调用——ParseAcceptLanguage内部有 map 查找开销
微服务中 Localizer 必须每请求新建,Bundle 必须全局单例
*i18n.Bundle 负责管理所有语言资源,初始化成本高,必须在启动时一次性 LoadMessageFile 所有 active.*.json;而 *i18n.Localizer 是轻量、非线程安全、按需绑定语言的实例,复用会导致并发错译。
- 全局只持一个
*i18n.Bundle,启动时加载全部支持语言的资源文件 - 每个 HTTP 请求在中间件里调
bundle.NewLocalizer(matchedTag)创建新实例,不要缓存或复用 - 别把
Localizer塞进context.WithValue全链路透传——类型不安全、易漏、难调试;推荐在中间件生成后,作为参数注入 handler 或 struct 方法接收器 - 微服务间调用时,语言上下文需通过
X-Request-LanguageHeader 或 gRPCmetadata显式透传,不能依赖下游自动解析
最常被忽略的点:配置里的 default_language 和 supported_languages 必须用 BCP 47 标签(如 "zh-CN"),不能写 "zh_CN" 或 "chinese";locales_path 路径若带 .. 或多余斜杠,os.DirFS 就会加载失败,而 bundle 不报错——这正是线上静默失效的高频原因。


















