diff --git a/errors.go b/errors.go index 0e4489c..5b13259 100644 --- a/errors.go +++ b/errors.go @@ -5,12 +5,16 @@ import ( "fmt" "reflect" "strings" + + "github.com/go-playground/universal-translator" ) const ( fieldErrMsg = "Key: '%s' Error:Field validation for '%s' failed on the '%s' tag" ) +type ValidationErrorsTranslations map[string]string + // InvalidValidationError describes an invalid argument passed to // `Struct`, `StructExcept`, StructPartial` or `Field` type InvalidValidationError struct { @@ -39,18 +43,32 @@ func (ve ValidationErrors) Error() string { buff := bytes.NewBufferString("") - var err *fieldError + var fe *fieldError for i := 0; i < len(ve); i++ { - err = ve[i].(*fieldError) - buff.WriteString(err.Error()) + fe = ve[i].(*fieldError) + buff.WriteString(fe.Error()) buff.WriteString("\n") } return strings.TrimSpace(buff.String()) } +func (ve ValidationErrors) Translate(ut ut.Translator) ValidationErrorsTranslations { + + trans := make(ValidationErrorsTranslations) + + var fe *fieldError + + for i := 0; i < len(ve); i++ { + fe = ve[i].(*fieldError) + trans[fe.ns] = fe.Translate(ut) + } + + return trans +} + // FieldError contains all functions to get error details type FieldError interface { @@ -118,6 +136,13 @@ type FieldError interface { // // // eg. time.Time's type is time.Time Type() reflect.Type + + // returns the FieldError's translated error + // from the provided 'ut.Translator' and registered 'TranslationFunc' + // + // NOTE: is not registered translation can be found it returns the same + // as calling fe.Error() + Translate(ut ut.Translator) string } // compile time interface checks @@ -128,6 +153,7 @@ var _ error = new(fieldError) // with other properties that may be needed for error message creation // it complies with the FieldError interface type fieldError struct { + v *Validate tag string actualTag string ns string @@ -202,3 +228,23 @@ func (fe *fieldError) Type() reflect.Type { func (fe *fieldError) Error() string { return fmt.Sprintf(fieldErrMsg, fe.ns, fe.Field(), fe.tag) } + +// Translate returns the FieldError's translated error +// from the provided 'ut.Translator' and registered 'TranslationFunc' +// +// NOTE: is not registered translation can be found it returns the same +// as calling fe.Error() +func (fe *fieldError) Translate(ut ut.Translator) string { + + m, ok := fe.v.transTagFunc[ut.Locale()] + if !ok { + return fe.Error() + } + + fn, ok := m[fe.tag] + if !ok { + return fe.Error() + } + + return fn(ut, fe) +} diff --git a/struct_level.go b/struct_level.go index d12d7e9..aefad2f 100644 --- a/struct_level.go +++ b/struct_level.go @@ -117,6 +117,7 @@ func (v *validate) ReportError(field interface{}, fieldName, structFieldName, ta v.errs = append(v.errs, &fieldError{ + v: v.v, tag: tag, actualTag: tag, ns: v.str1, @@ -132,6 +133,7 @@ func (v *validate) ReportError(field interface{}, fieldName, structFieldName, ta v.errs = append(v.errs, &fieldError{ + v: v.v, tag: tag, actualTag: tag, ns: v.str1, diff --git a/translations.go b/translations.go new file mode 100644 index 0000000..b968aaf --- /dev/null +++ b/translations.go @@ -0,0 +1,11 @@ +package validator + +import "github.com/go-playground/universal-translator" + +// TranslationFunc is the function type used to register or override +// custom translations +type TranslationFunc func(ut ut.Translator, fe FieldError) string + +// RegisterTranslationsFunc allows for registering of translations +// for a 'ut.Translator' for use withing the 'TranslationFunc' +type RegisterTranslationsFunc func(ut ut.Translator) error diff --git a/validator.go b/validator.go index 2119152..960bd7b 100644 --- a/validator.go +++ b/validator.go @@ -113,6 +113,7 @@ func (v *validate) traverseField(parent reflect.Value, current reflect.Value, ns v.errs = append(v.errs, &fieldError{ + v: v.v, tag: ct.aliasTag, actualTag: ct.tag, ns: v.str1, @@ -129,6 +130,7 @@ func (v *validate) traverseField(parent reflect.Value, current reflect.Value, ns v.errs = append(v.errs, &fieldError{ + v: v.v, tag: ct.aliasTag, actualTag: ct.tag, ns: v.str1, @@ -323,6 +325,7 @@ OUTER: v.errs = append(v.errs, &fieldError{ + v: v.v, tag: ct.aliasTag, actualTag: ct.actualAliasTag, ns: v.str1, @@ -342,6 +345,7 @@ OUTER: v.errs = append(v.errs, &fieldError{ + v: v.v, tag: tVal, actualTag: tVal, ns: v.str1, @@ -381,6 +385,7 @@ OUTER: v.errs = append(v.errs, &fieldError{ + v: v.v, tag: ct.aliasTag, actualTag: ct.tag, ns: v.str1, diff --git a/validator_instance.go b/validator_instance.go index d9cc909..fd380b2 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -7,6 +7,8 @@ import ( "strings" "sync" "time" + + "github.com/go-playground/universal-translator" ) const ( @@ -53,6 +55,7 @@ type Validate struct { customFuncs map[reflect.Type]CustomTypeFunc aliases map[string]string validations map[string]Func + transTagFunc map[string]map[string]TranslationFunc // map[]map[]TranslationFunc tagCache *tagCache structCache *structCache } @@ -189,6 +192,27 @@ func (v *Validate) RegisterCustomTypeFunc(fn CustomTypeFunc, types ...interface{ v.hasCustomFuncs = true } +func (v *Validate) RegisterTranslation(tag string, ut ut.Translator, registerFn RegisterTranslationsFunc, translationFn TranslationFunc) (err error) { + + if v.transTagFunc == nil { + v.transTagFunc = make(map[string]map[string]TranslationFunc) + } + + if err = registerFn(ut); err != nil { + return + } + + m, ok := v.transTagFunc[ut.Locale()] + if !ok { + m = make(map[string]TranslationFunc) + v.transTagFunc[ut.Locale()] = m + } + + m[tag] = translationFn + + return +} + // Struct validates a structs exposed fields, and automatically validates nested structs, unless otherwise specified. // // It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise. diff --git a/validator_test.go b/validator_test.go index 07fce3f..daa166c 100644 --- a/validator_test.go +++ b/validator_test.go @@ -11,6 +11,11 @@ import ( "time" . "gopkg.in/go-playground/assert.v1" + + "github.com/go-playground/locales/en" + "github.com/go-playground/locales/fr" + "github.com/go-playground/locales/nl" + "github.com/go-playground/universal-translator" ) // NOTES: @@ -6413,3 +6418,145 @@ func TestRequired(t *testing.T) { NotEqual(t, err, nil) AssertError(t, err.(ValidationErrors), "Test.Value", "Test.Value", "Value", "Value", "required") } + +func TestTranslations(t *testing.T) { + en := en.New() + uni := ut.New(en, en, fr.New()) + + trans, _ := uni.GetTranslator("en") + fr, _ := uni.GetTranslator("fr") + + validate := New() + err := validate.RegisterTranslation("required", trans, + func(ut ut.Translator) (err error) { + + // using this stype because multiple translation may have to be added for the full translation + if err = ut.Add("required", "{0} is a required field", false); err != nil { + return + } + + return + + }, func(ut ut.Translator, fe FieldError) string { + + t, err := ut.T(fe.Tag(), fe.Field()) + if err != nil { + fmt.Printf("warning: error translating FieldError: %#v", fe.(*fieldError)) + return fe.(*fieldError).Error() + } + + return t + }) + Equal(t, err, nil) + + err = validate.RegisterTranslation("required", fr, + func(ut ut.Translator) (err error) { + + // using this stype because multiple translation may have to be added for the full translation + if err = ut.Add("required", "{0} est un champ obligatoire", false); err != nil { + return + } + + return + + }, func(ut ut.Translator, fe FieldError) string { + + t, err := ut.T(fe.Tag(), fe.Field()) + if err != nil { + fmt.Printf("warning: error translating FieldError: %#v", fe.(*fieldError)) + return fe.(*fieldError).Error() + } + + return t + }) + + Equal(t, err, nil) + + type Test struct { + Value interface{} `validate:"required"` + } + + var test Test + + err = validate.Struct(test) + NotEqual(t, err, nil) + + errs := err.(ValidationErrors) + Equal(t, len(errs), 1) + + fe := errs[0] + Equal(t, fe.Tag(), "required") + Equal(t, fe.Namespace(), "Test.Value") + Equal(t, fe.Translate(trans), fmt.Sprintf("%s is a required field", fe.Field())) + Equal(t, fe.Translate(fr), fmt.Sprintf("%s est un champ obligatoire", fe.Field())) + + nl := nl.New() + uni2 := ut.New(nl, nl) + trans2, _ := uni2.GetTranslator("nl") + Equal(t, fe.Translate(trans2), "Key: 'Test.Value' Error:Field validation for 'Value' failed on the 'required' tag") + + terrs := errs.Translate(trans) + Equal(t, len(terrs), 1) + + v, ok := terrs["Test.Value"] + Equal(t, ok, true) + Equal(t, v, fmt.Sprintf("%s is a required field", fe.Field())) + + terrs = errs.Translate(fr) + Equal(t, len(terrs), 1) + + v, ok = terrs["Test.Value"] + Equal(t, ok, true) + Equal(t, v, fmt.Sprintf("%s est un champ obligatoire", fe.Field())) + + type Test2 struct { + Value string `validate:"gt=1"` + } + + var t2 Test2 + + err = validate.Struct(t2) + NotEqual(t, err, nil) + + errs = err.(ValidationErrors) + Equal(t, len(errs), 1) + + fe = errs[0] + Equal(t, fe.Tag(), "gt") + Equal(t, fe.Namespace(), "Test2.Value") + Equal(t, fe.Translate(trans), "Key: 'Test2.Value' Error:Field validation for 'Value' failed on the 'gt' tag") +} + +func TestTranslationErrors(t *testing.T) { + + en := en.New() + uni := ut.New(en, en, fr.New()) + + trans, _ := uni.GetTranslator("en") + trans.Add("required", "{0} is a required field", false) // using translator outside of validator also + + validate := New() + err := validate.RegisterTranslation("required", trans, + func(ut ut.Translator) (err error) { + + // using this stype because multiple translation may have to be added for the full translation + if err = ut.Add("required", "{0} is a required field", false); err != nil { + return + } + + return + + }, func(ut ut.Translator, fe FieldError) string { + + t, err := ut.T(fe.Tag(), fe.Field()) + if err != nil { + fmt.Printf("warning: error translating FieldError: %#v", fe.(*fieldError)) + return fe.(*fieldError).Error() + } + + return t + }) + + NotEqual(t, err, nil) + Equal(t, err.Error(), "error: conflicting key 'required' rule 'Unknown' with text '{0} is a required field', value being ignored") +}