go-playground / validator

:100:Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving
MIT License
16.06k stars 1.29k forks source link

err.Translate(trans) only works if translator passed via param (go-playground/validator) #1274

Open fasaya opened 1 month ago

fasaya commented 1 month ago

I'm trying to translate the validation errors message because I don't like the default error message. When I pass the trans variable via param from controllers to a helper (to handle validation error), err.Translate(trans) worked just like what I want.

Here is the controllers/handler

package handler

import (
    "base-trade-rest/api/request"
    "base-trade-rest/core/helpers"
    "base-trade-rest/core/model"
    "base-trade-rest/core/service"
    "net/http"

    "github.com/gin-gonic/gin"
    en "github.com/go-playground/locales/en"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    en_translations "github.com/go-playground/validator/v10/translations/en"
)

type ProductHandler struct {
    ProductService service.IProductService
    Validate       *validator.Validate
    Trans          ut.Translator
}

func NewProductHandler(productService service.IProductService) *ProductHandler {

    // NOTE: omitting allot of error checking for brevity
    en := en.New()
    uni := ut.New(en, en)

    // this is usually know or extracted from http 'Accept-Language' header
    // also see uni.FindTranslator(...)
    trans, _ := uni.GetTranslator("en")

    validate := validator.New()
    en_translations.RegisterDefaultTranslations(validate, trans)

    var productHandler = ProductHandler{
        ProductService: productService,
        Validate:       validate,
        Trans:          trans,
    }
    return &productHandler
}

func (h *ProductHandler) Store(ctx *gin.Context) {
    var request request.ProductCreateRequest

    err := ctx.ShouldBind(&request)
    if err != nil {
        helpers.CreateFailedResponse(ctx, http.StatusBadRequest, err.Error())
        return
    }

    err = h.Validate.Struct(request)
    if err != nil {
        helpers.HandleValidationError(ctx, err, h.Trans)
        return
    }

    // Get authenticated user
    userData := helpers.GetAuthUser(ctx)

    product := model.Product{
        Name:     request.Name,
        ImageURL: nil,
        UserID:   userData.ID,
    }

    newUser, err := h.ProductService.CreateProduct(&product)
    if err != nil {
        helpers.CreateFailedResponse(ctx, http.StatusBadRequest, err.Error())
        return
    }

    helpers.CreateSuccessfulResponse(ctx, http.StatusOK, "Product successfully created", newUser)
}

And the helpers

package helpers

import (
    "strings"

    "github.com/gin-gonic/gin"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
)

func getValidationErrors(err error, trans ut.Translator) map[string]string {
    var errors = make(map[string]string)

    errs := err.(validator.ValidationErrors)
    for _, e := range errs {
        errors[strings.ToLower(e.Field())] = e.Translate(trans)
    }

    return errors
}

func HandleValidationError(ctx *gin.Context, err error, trans ut.Translator) {
    CreateValidationErrorResponse(ctx, getValidationErrors(err, trans))
}

The getValidationErrors() returns just like i expected

{
    "name": "Name is a required field"
}

But because i dont want to repeat the code in every controllers/handlers, i created a helper and init the trans there

package helpers

import (
    "strings"

    "github.com/gin-gonic/gin"
    en "github.com/go-playground/locales/en"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    en_translations "github.com/go-playground/validator/v10/translations/en"
)

// use single instances, it caches struct info
var trans ut.Translator

func init() {
    en := en.New()
    uni := ut.New(en, en)
    trans, _ = uni.GetTranslator("en")

    validate := validator.New()
    en_translations.RegisterDefaultTranslations(validate, trans)
}

func getValidationErrors(err error) map[string]string {
    var errors = make(map[string]string)

    errs := err.(validator.ValidationErrors)
    for _, e := range errs {
        errors[strings.ToLower(e.Field())] = e.Translate(trans)
    }

    return errors
}

func HandleValidationError(ctx *gin.Context, err error) {
    CreateValidationErrorResponse(ctx, getValidationErrorsTest(err))
}

And call it from controllers like this without passing the h.Trans

err = h.Validate.Struct(request)
if err != nil {
        helpers.HandleValidationError(ctx, err)
return
}

But it returns something like this which i don't want

{
    "name": "Key: 'ProductCreateRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag"
}

Is there something i missed or is this a wrong way?