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!

実際に色々と試してみたソースコードはリポジトリに置いてあります。

yukpiz/go-translator-example

実際に運用しようとした実装では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に渡すという実装に落ち着きそうです(このあたりの実装についてもそのうち書きます)。

この記事が気に入ったらサポートをしてみませんか?