ginとvalidator.v9でリクエストバインド時に多言語対応する(ボツ実装)
当初これならイケると思っていましたが、ginの対応状況やら、validator.v9の恩恵やらを考え直して、v8を継続して使いつつ、universal-translatorを使って多言語対応をやってみる方向にしました(これについても今後記事にしようと思います)。
しかし、せっかく書いたのに捨てるのはもったいないので、ボツ実装ですが記事として公開します。
---
2019年5月8日時点でginはvalidator.v9に対応できていません。
validator.v9の機能であるTranslatorの恩恵を受ける事ができない為、自前実装でリクエストパラメーターのバインド時にエラーメッセージ多言語化ができるようにしてみます。
実現する為に参考にした記事は以下です。
・go-playground/validator - How can I translate fieldName?
ginでは、以下のように構造体のbindingタグを使うことによって、バインド時にvalidator.ValidationErrorsを捕捉する事ができます。
func main() {
r := gin.Default()
r.GET("/hello", func(gc *gin.Context) {
req := struct {
Hello string `binding:"required"`
}{}
if err := gc.BindQuery(&req); err != nil {
gc.String(http.StatusBadRequest, err.Error())
return
}
gc.String(http.StatusOK, "hello")
})
r.Run(":8888")
}
$ curl -XGET http://localhost:8888/hello
Key: '.Hello' Error:Field validation for 'Hello' failed on the 'required' tag
$ curl -XGET http://localhost:8888/hello?Hello=yukpiz
hello
しかしginが対応しているvalidator.v8には、Translateの機能がなく、エラーメッセージの多言語対応で詰まってしまいます。
そこで、この記事ではginのbindingタグではなくvalidator.v9のvalidateタグを利用して、ginはバインド機能だけを担いつつ、多言語対応をやってみます。
以下は実際に動かしてみたソースコードです。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-playground/locales/en_US"
"github.com/go-playground/locales/ja_JP"
ut "github.com/go-playground/universal-translator"
"gopkg.in/go-playground/validator.v9"
)
func main() {
r := gin.Default()
trans := ut.New(ja_JP.New(), ja_JP.New(), en_US.New())
ja, _ := trans.GetTranslator("ja_JP")
_ = ja.Add("Hello", "こんにちは", false)
en, _ := trans.GetTranslator("en_US")
_ = en.Add("Hello", "Hello", false)
validate := validator.New()
validate.RegisterTranslation("required", ja, func(ut ut.Translator) error {
return ut.Add("required", "{0}は必須項目です", false)
}, TransFunc)
validate.RegisterTranslation("required", en, func(ut ut.Translator) error {
return ut.Add("required", "{0} is required", false)
}, TransFunc)
r.GET("/hello", func(gc *gin.Context) {
req := struct {
Hello string `validate:"required"`
}{}
if err := gc.BindQuery(&req); err != nil {
gc.Status(http.StatusBadRequest)
return
}
if err := validate.Struct(req); err != nil {
verrs := errs.(validator.ValidationErrors)
gc.String(http.StatusBadRequest, verrs[0].Translate(en))
return
}
gc.String(http.StatusOK, "success!")
})
r.Run(":8888")
}
func TransFunc(ut ut.Translator, fe validator.FieldError) string {
fld, _ := ut.T(fe.Field())
t, err := ut.T(fe.Tag(), fld)
if err != nil {
return fe.(error).Error()
}
return t
}
$ curl -XGET http://localhost:8888/hello
こんにちはは必須項目です
$ curl -XGET http://localhost:8888/hello?Hello=yukpiz
success!
実際に色々と試してみたソースコードはリポジトリに置いてあります。
実際に運用しようとした実装ではhandlerでのバインド機能のラッピングやRegisterTranslationやエラー判定などパッケージに切り分けていますが、参考にはできると思います。
---
ざっくり説明。
trans := ut.New(ja_JP.New(), ja_JP.New(), en_US.New())
ja, _ := trans.GetTranslator("ja_JP")
_ = ja.Add("Hello", "こんにちは", false)
en, _ := trans.GetTranslator("en_US")
_ = en.Add("Hello", "Hello", false)
validate := validator.New()
validate.RegisterTranslation("required", ja, func(ut ut.Translator) error {
return ut.Add("required", "{0}は必須項目です", false)
}, TransFunc)
validate.RegisterTranslation("required", en, func(ut ut.Translator) error {
return ut.Add("required", "{0} is required", false)
}, TransFunc)
ここではTranslatorの初期設定を行っています。
ut.New()ではUniversalTranslatorを取得しています。これは第一引数にフォールバックロケール(ユーザー言語が該当しなかった場合にデフォルトで使用されるロケール)と、サポートするロケールを第二引数以降に可変長で渡す事ができます。
次にTranslatorをロケールごとに取得してフィールド名称を登録しています。Translator.Add()の第一引数がkeyになっており、登録したkeyを使って、翻訳ワードを引くことができます。
validatorにはRegisterTranslation()を使ってTranslatorを登録する事ができます。この設定をしておくことによって、validator.ValidationErrorsから翻訳を直接引くことができるようになります。
func TransFunc(ut ut.Translator, fe validator.FieldError) string {
fld, _ := ut.T(fe.Field())
t, err := ut.T(fe.Tag(), fld)
if err != nil {
return fe.(error).Error()
}
return t
}
ここでは設定したTranslatorとvalidatorが吐き出してくるFieldErrorで翻訳ワードを抽出しています。
fe.Field()ではエラーの起きた構造体フィールド名(ここではHello)を取得できます。さらにfe.Tag()ではvalidatorがエラー判定にした構造体タグ(ここではrequired)が取得できます。
これらを組み合わせてut.T("required", "Hello")を引くと、「こんにちはは必須項目です」と埋め込まれた状態の翻訳ワードを得ることができます。
---
とここまで色々と書いてみましたが、設定周りの実装がかなりややこしくなってしまうのと(翻訳ワードやパターンが増えるほどさらに)、validator自体にtranslatorが依存することで、役割が見えなくなってしまうと感じました。
そこでvalidator.v9の実用は諦め、gin側でのv8のバリデーションを任せつつ、そこから吐き出されるvalidator.ValidationErrorsをHandler側で捕捉・分解してTranslatorに渡すという実装に落ち着きそうです(このあたりの実装についてもそのうち書きます)。
この記事が気に入ったらサポートをしてみませんか?