go标准库不内置i18n,官方推荐golang.org/x/text/message方案;需用message.printer按请求构造以支持复数、性别、rtl等cldr规则,严禁硬编码map或拼接key,必须统一注册全语言键值并用language.parseacceptlanguage解析accept-language。

Go 标准库不内置 i18n 支持,golang.org/x/text 是官方推荐、生产可用的方案;别用第三方包硬套 JSON/YAML 翻译文件,容易在多语言切换、复数规则、RTL 适配上翻车。
用 message.Printer 做运行时翻译,而不是手动查 map
很多人写 translations["zh"]["welcome"] 这种硬编码结构,看似简单,实则无法处理复数(如 “1 file” / “3 files”)、性别(如德语动词变位)、文字方向(如阿拉伯语 RTL 渲染顺序)。官方 message.Printer 封装了 CLDR 数据和格式化逻辑,直接对接 golang.org/x/text/message。
- 先定义
message.Catalog,用message.SetString注册各语言键值对(支持嵌套命名空间,如"auth.login.success") - 按请求语言创建
message.Printer实例:传入language.Make("zh")或language.Make("en-US") - 调用
printer.Sprintf("welcome", "Alice"),自动选语言、插值、处理复数 —— 不用手动if lang == "zh"
message.SetString 的 key 必须全局唯一,且不能含空格或特殊符号
key 是编译期静态标识符,不是运行时字符串拼接来源。比如 message.SetString(language.English, "user.deleted", "User deleted") 合法;但 message.SetString(lang, "user."+op, ...) 会漏注册、无法校验、导致 fallback 失效。
- key 建议全小写 + 点分隔(
"form.validation.required"),避免"User Deleted!"这类带空格/标点的写法 - 所有语言版本必须注册相同 key,缺一个就会 fallback 到默认语言(通常是 English),但不会报错 —— 容易漏测
- 若需动态 key(如来自数据库字段名),应预定义白名单映射表,而非放行任意字符串
HTTP 请求中提取语言首选项,优先用 Accept-Language 而非 URL 参数
URL 里塞 ?lang=ja 看似方便,但破坏缓存、无法被搜索引擎识别、且与浏览器语言设置脱节。标准做法是解析 Accept-Language header,用 language.ParseAcceptLanguage 得到有序语言列表。
-
language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))返回[]language.Tag,按权重降序 - 遍历该切片,用
catalog.Supports(lang)检查是否已注册对应语言 —— 注意:不是检查字符串相等,而是用language.Match匹配区域变体(如zh-CN匹配zh) - fallback 到默认语言时,务必显式使用
language.Und(而非空 Tag),否则Printer可能 panic
模板中嵌入 printer.Sprintf 时,避免在循环内反复创建 Printer
每次调用 message.NewPrinter 都会复制内部状态,高频渲染(如列表页每行一个翻译)会导致 GC 压力上升。正确做法是把 Printer 实例作为请求上下文变量传递,或在 handler 层初始化后注入模板。
- 不要在
html/template的{{.Printer.Sprintf "item.count" .Count}}中直接暴露Printer—— 安全风险 + 类型泄漏 - 建议封装为模板函数:
func translate(lang language.Tag, key string, args ...interface{}) string,内部复用Printer实例 - 若用
text/template渲染 CLI 输出,注意Printer默认输出 UTF-8,Windows 控制台需提前调用os.Setenv("GOEXPERIMENT", "utf8strings")(Go 1.22+)
最麻烦的不是写翻译函数,而是确保每个 message.SetString 调用都覆盖全部语言、且 key 在所有地方拼写一致 —— 错一个字母,线上就显示英文 fallback,还很难 grep 出来。











