gin原生不支持多语言,需通过golang.org/x/text/language和message包实现轻量级i18n:解析accept-language或query参数获取language.tag,中间件中为每个请求创建并注入*message.printer实例,结合gotext工具链编译期生成翻译绑定文件,确保按请求动态翻译。

如何用 Gin 原生支持多语言?
Gin 本身不内置国际化(i18n)能力,gin.Default() 或 gin.New() 都不会自动加载语言包、解析 Accept-Language、切换翻译器。想实现“极简”翻译,必须自己搭一层轻量胶水——核心是复用 golang.org/x/text/language 和 golang.org/x/text/message,避免引入 heavy 的 i18n 框架(比如 go-i18n 或 lingo)。
关键不是“用什么库”,而是“怎么让翻译逻辑贴近请求生命周期”。常见错误是把翻译器做成全局单例然后硬编码语言,结果无法按用户请求动态切换。
- 翻译器必须绑定到
*gin.Context,而不是全局变量 - 语言标识应优先从
Accept-Language头解析,fallback 到 URL query(如?lang=zh)或 cookie - 不要在 handler 外预渲染翻译文本——翻译必须发生在 handler 内,才能拿到当前请求的语言上下文
怎么注册一个 per-request 翻译器?
最轻的方案是写一个中间件,在 c.Set() 中存入已配置好的 *message.Printer 实例。这个实例由 message.NewPrinter() 创建,传入当前请求匹配出的 language.Tag。
示例代码片段(非完整初始化,仅展示核心逻辑):
func i18nMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tag, _ := language.Parse(c.GetHeader("Accept-Language"))
// fallback: c.Query("lang") → language.Make(...)
p := message.NewPrinter(tag, message.WithTemplateFuncs(templateFuncs))
c.Set("printer", p)
c.Next()
}
}
注意:message.NewPrinter 是线程安全的,可复用;但每个请求必须用自己匹配出的 tag 初始化,否则所有请求都走默认语言。
- 别把
message.NewPrinter放在 middleware 外部初始化——那会固定用一个语言 -
language.Parse可能返回 error,建议 fallback 到language.English,而非 panic - 如果用了模板渲染,记得把
p.Printf注册为 template func,否则 HTML 模板里没法调用
翻译字符串怎么组织才不散乱?
不要把翻译文本硬编码在 Go 字符串里,也不要用 map[string]string 手动维护。Golang 官方推荐方式是用 .po 文件 + gotext 工具生成 .go 绑定文件。
流程很简单:
- 写源码时用
fmt.Sprintf("Hello %s", name)→ 改成p.Sprintf("Hello %s", name) - 运行
gotext extract -out locales/en_US.gotext.json -srclang=en . - 复制生成的 JSON,翻译成
zh_CN.gotext.json等 - 运行
gotext generate,产出locales/xx_XX/locales_xx_XX.go
生成的 Go 文件里是 var translations = map[string]message.Translation{...},message.NewPrinter 会自动加载它们。不用手写 switch/case,也不用 runtime 加载 JSON 文件——编译期就固化了。
容易踩的坑:gotext extract 默认只扫 .go 文件,如果你在 HTML 模板里写了 {{ .Printer.Sprintf "Save" }},它不会被提取。解决方案:统一用 Go 层做翻译,模板里只接收已翻译好的字符串。
为什么中文翻译总显示英文?
最常见的原因是语言 tag 匹配失败:language.Make("zh-CN") 和 language.Make("zh") 在 message 的匹配规则里不等价。Gin 接收到 Accept-Language: zh-CN,zh;q=0.9,但你只提供了 zh.gotext.json,没提供 zh-CN.gotext.json,就会 fallback 到英文。
解决办法只有两个:
- 生成翻译文件时,明确指定 tag:用
gotext extract -lang=zh-CN -lang=zh -lang=en - 或者在匹配逻辑里做归一化:把
zh-CN→zh,en-US→en,再传给message.NewPrinter
另一个隐蔽问题:Go 的 message 包对简体/繁体区分很严格。zh-Hans(简体)和 zh-Hant(繁体)必须分别提供翻译文件,不能共用 zh。如果用户浏览器发的是 zh-Hans-CN,而你只准备了 zh.gotext.json,照样 fallback。
极简不等于省事——语言 tag 的层级关系、fallback 链、区域变体,这些绕不开。哪怕只支持中英,也得确认清楚用户实际发的是哪个 tag。











