ShouldBindJSON 错误默认是英文,因 validator 未加载中文翻译器、未注册 label 标签读取函数、未正确绑定 translator 实例,导致整个翻译链路失效。

默认的 ShouldBindJSON 错误永远是英文,不手动注册翻译器、不正确断言错误类型、不加 label 标签,就不可能拿到“用户名不能为空”这种提示——这不是配置漏了,而是整个翻译链路根本没跑通。
ShouldBindJSON 报错为什么还是英文?
因为 validator 默认不加载任何翻译器,err.Error() 返回的是原始模板字符串,比如 "Key: 'LoginReq.Username' Error: Field validation for 'Username' failed on the 'required' tag"。它和 Gin 无关,只和你有没有把 zh 翻译器挂到当前 validator 实例上有关。
- 必须在 Gin router 创建后、
router.Run()前调用初始化函数,否则binding.Validator.Engine()可能返回 nil 或新实例 -
uni.GetTranslator("zh")中的 locale 必须是"zh",不是"zh-CN"或"cn",否则静默失败 -
zh_translations.RegisterDefaultTranslations(v, trans)的v必须是同一个*validator.Validate实例,不能自己validator.New() - 导入路径必须是
github.com/go-playground/validator/v10/translations/zh,v9 路径不兼容
字段名显示为 Username 而不是“用户名”
validator 的 Translate() 方法默认用结构体字段名(如 Username)填充提示里的 {0} 占位符,不会自动读取 json:"username"。要让它显示业务名称,得靠 label 标签 + RegisterTagNameFunc 配合。
- 给字段加上
label:"用户名":Username string `json:"username" binding:"required" label:"用户名"` - 在初始化时注册标签读取逻辑:
v.RegisterTagNameFunc(func(f reflect.StructField) string { return f.Tag.Get("label") }) - 不注册该函数的话,即使有
label标签也不会被 validator 识别 - 别名注册不能写成小写
username,必须严格匹配结构体字段名Username
自定义正则校验(如手机号)如何带中文提示?
注册 v.RegisterValidation("phone", ...) 只负责逻辑判断,不自动绑定错误文案。它的错误默认仍走英文模板,必须手动补翻译映射。
- 正则函数里不要调
fl.Param(),直接用fl.Field().String()拿原始值 - 注册完校验规则后,立刻追加翻译:
trans.Add("phone", "{0} 必须是合法手机号", true) -
trans.Add的第一个参数("phone")必须和RegisterValidation的第一个参数完全一致 - 重复调用
trans.Add会 panic,所以true表示允许覆盖,适合在启动阶段一次性注册 - 错误处理时仍需断言:
errs := err.(validator.ValidationErrors),再遍历errs[i].Translate(trans)
TranslateError 函数怎么写才安全?
直接 err.Error() 是最大陷阱;不判断类型就断言会 panic;不遍历每条错误就丢字段上下文。一个健壮的转换函数必须守住三道防线。
- 先用
errors.As(err, &errs)安全判断是否为validator.ValidationErrors,避免 panic - 不是 validator 错误时,原样返回
err.Error(),比如网络超时或 JSON 解析失败 - 是 validator 错误时,逐条调用
fe.Translate(trans),每条返回完整句子,如"手机号格式不正确" - 别自己拼接字符串,
fe.Translate(trans)已包含字段名、规则语义、实际值(部分规则支持) - 全局
trans变量必须非 nil,初始化失败时应提前 abort,不能留空指针
最常被跳过的环节是:没确认 binding.Validator.Engine() 真的拿到了 Gin 内部那个 validator 实例,以及没意识到 label 标签必须配合 RegisterTagNameFunc 才生效——这两步一漏,后面所有翻译代码都只是在给空气注释。


















