go-i18n/v2 是 Go 中唯一支持运行时切换、热加载及 CLDR 复数规则的多语言方案;需全局单例初始化 bundle,严格按 active.zh-Hans.json 命名、含 description 和 translation 字段的 JSON 结构加载,用 language.ParseAcceptLanguage + NewMatcher 解析 Accept-Language,每请求新建 Localizer 并通过中间件注入 T 函数。

Go 语言中用 Echo 实现多语言,go-i18n/v2 是目前唯一能稳定支撑运行时按请求切换、热加载、CLDR 复数规则的方案;golang.org/x/text/message 不支持动态语言变更,硬套会导致始终返回默认语言且无提示。
bundle 必须全局单例初始化,不能在中间件里反复 ParseFS
所有翻译资源必须在 main() 启动时一次性注入 *i18n.Bundle,否则会内存泄漏、覆盖已有翻译项、或静默丢弃新文件。
- 正确做法:
bundle := i18n.NewBundle(language.English)创建一次,再调bundle.ParseFS(fs, "locales/active.*.json")加载全部文件 -
fs必须是os.DirFS("./locales"),路径不匹配(比如文件实际在./i18n/)则完全不加载 - 文件名必须严格为
active.zh-Hans.json、active.en-US.json——zh.json、zh_Hans.json、active-zh-hans.json全部被忽略 - JSON 结构必须含
description和translation字段:{"login.title": {"description": "page title", "translation": "登录"}},缺任一字段该条目直接丢弃
Accept-Language 解析必须用 language.ParseAcceptLanguage + NewMatcher
浏览器发来的 Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8 是带权重、可嵌套、需标准化的语言标签序列,手动 strings.Split 或正则提取会丢失排序、误判兼容性(如把 zh-Hans 当作不匹配 zh-CN),最终静默 fallback 到 English。
- 先调
language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))得到有序[]language.Tag - 预定义白名单:
supported := []language.Tag{language.Chinese, language.English}(别用字符串切片) - 只初始化一次
matcher := language.NewMatcher(supported),再用matcher.Match(acceptTags)获取归一化后的language.Tag(如zh-Hans) - 结果建议存进
c.Request().Context(),避免每个 handler 都重复解析(内部有 map 查找开销)
Localizer 必须每请求新建,禁止复用或缓存
*i18n.Localizer 实例不是线程安全的,跨请求复用会导致并发翻译结果互相覆盖 —— 比如用户 A 请求中文、用户 B 同时请求英文,B 的语言设置可能污染 A 的响应。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
立即学习“go语言免费学习笔记(深入)”;
- 在 Echo 中间件里,拿到匹配后的
langTag后立即调bundle.NewLocalizer(langTag) - 不要存为全局变量,也不要塞进
echo.Context.Set()后长期持有 - 这个调用开销极小,无需担心性能;真正要防的是复用导致的竞态
- 后续调
localizer.Localize(...)时若返回空字符串或原始 key,99% 是前面 bundle 初始化或文件结构没达标,不是 Localizer 本身问题
Echo 中间件注入 T 函数的典型写法
别让每个 handler 都手动构造 Localizer,用中间件统一生成带上下文的翻译函数并注入 echo.Context。
- 中间件中解析出
langTag后,定义闭包:T := func(messageID string, args ...interface{}) string { return localizer.Localize(&i18n.LocalizeConfig{MessageID: messageID, TemplateData: args}) } - 调
c.Set("T", T),handler 中用c.Get("T").(func(string, ...interface{}) string)调用 - 注意:
args是原始值,模板里用{{.Arg0}}引用,别提前fmt.Sprintf拼接 —— 否则会破坏复数、词序、RTL 等 CLDR 规则 - 如果用 embed.FS,
ParseFS仍可用,但热重载需额外监听 fs.Stat 变化,文件系统下才自动生效
最易被忽略的点是 JSON 文件里漏掉 description 字段,或文件名大小写/连字符错一位 —— 这类错误不会 panic,Localize 直接返回空或原始 key,排查时容易绕远路。建议启动后立刻用 bundle.FindMessage 手动查一条已知 ID,确认是否加载成功。

















