应使用 language.parseacceptlanguage 解析 accept-language 头,它自动排序、过滤非法标签并标准化权重;解析后需用 language.newmatcher 匹配白名单语言,再按请求创建 *i18n.localizer 实例,不可全局复用或手动字符串分割。

如何从 HTTP 请求头提取 Accept-Language 并解析成可用语言标签
Gin 本身不内置语言解析逻辑,Accept-Language 是个逗号分隔、带权重(q=0.8)的字符串,比如 "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"。直接用 strings.Split() 会漏掉权重、忽略空格和 q 值,导致选错默认语言。
- 推荐用标准库
net/http的ParseAcceptLanguage()—— 它已处理好排序和权重归一化,返回按优先级降序排列的[]string - 注意:该函数只解析语言标签(如
"zh-CN"),不校验是否存在对应翻译文件,也不做区域 fallback(比如"zh-CN"找不到时自动试"zh") - 实际使用时建议封装一层 fallback 逻辑:先查完整标签(
"zh-CN"),再截取主语言("zh"),最后兜底到配置的默认语言(如"en")
用 go-i18n/v2 实现 Gin 中间件自动注入本地化实例
go-i18n/v2 是目前最活跃的 Go 国际化库,支持 JSON 翻译文件、模板函数和运行时加载。关键不是“怎么加翻译”,而是“怎么让每个请求拿到正确的 localizer 实例”。
- 不要在全局初始化一个
Localizer—— 它是线程安全但不带上下文感知;必须为每个请求基于语言标签新建或复用缓存的Localizer - 中间件里用
c.Set("localizer", loc)存入上下文,后续 handler 通过c.MustGet("localizer").(*i18n.Localizer)取出 - 翻译调用统一用
loc.Localize(&i18n.LocalizeConfig{MessageID: "user_not_found"}),避免硬编码语言参数 - JSON 文件路径需固定结构:
locales/zh-CN.all.json、locales/en-US.all.json,否则Bundle.LoadMessageFile()会静默失败
为什么不能依赖 Gin 的 c.ClientIP() 或 UA 做语言推断
浏览器语言设置(Accept-Language)才是 W3C 标准的、用户显式表达的偏好。用 IP 归属地或 UA 检测不仅不准(海外华人用中文浏览器但 IP 在美国),还违反 GDPR 和国内《个人信息保护法》对“默认开启定位/识别”的限制。
- IP 库对小语种国家覆盖差(如哈萨克斯坦、越南部分地区常被标为
"en") - 移动端 UA 字符串极简,基本不含语言信息;桌面端部分浏览器(如旧版 Safari)UA 甚至不带 locale
- 若真要兜底,应在
Accept-Language解析为空时,才 fallback 到 cookie(如lang=zh)或 URL 参数(?lang=ja),而非 IP
JSON 翻译文件里嵌套结构与复数规则的实际写法
Go 的 go-i18n/v2 支持 ICU MessageFormat,但很多人卡在 JSON 结构写错导致 Localize() 返回空字符串。
- 消息 ID 必须是顶层 key,不能嵌套在对象里:
{"user_not_found": "用户未找到"}✅,{"errors": {"user_not_found": "..."}}❌ - 复数用
plural关键字:"item_count": "{count, plural, one {# 条} other {# 条}},其中count是传入LocalizeConfig的TemplateData字段 - 变量插值用
{name},不是$name或{{name}};且变量名区分大小写,Localize(&i18n.LocalizeConfig{TemplateData: map[string]interface{}{"Name": "Alice"}})中{name}不会匹配
翻译文件没生效?先检查 Bundle.FindMessageInBundles() 是否返回非 nil 的 *message.Message,再确认 Localizer.Localize() 的 error 是否为 nil —— 很多时候是 JSON key 拼错或结构不对,错误却被吞掉了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











