Skip to content

Commit 4b3eedc

Browse files
Dean KarnDean Karn
authored andcommitted
Merge branch 'i18n-custom-errors' into v9
2 parents 6f98212 + 110c863 commit 4b3eedc

File tree

12 files changed

+2332
-360
lines changed

12 files changed

+2332
-360
lines changed

README.md

Lines changed: 55 additions & 321 deletions
Large diffs are not rendered by default.

doc.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,7 @@ based on tags.
55
It can also handle Cross-Field and Cross-Struct validation for nested structs
66
and has the ability to dive into arrays and maps of any type.
77
8-
Why not a better error message?
9-
Because this library intends for you to handle your own error messages.
10-
11-
Why should I handle my own errors?
12-
Many reasons. We built an internationalized application and needed to know the
13-
field, and what validation failed so we could provide a localized error.
14-
15-
if fieldErr.Field() == "Name" {
16-
switch fieldErr.Tag()
17-
case "required":
18-
return "Translated string based on field + error"
19-
default:
20-
return "Translated string based on field"
21-
}
22-
8+
see more examples https://github.com/go-playground/validator/tree/v9/examples
239
2410
Validation Functions Return Type error
2511

errors.go

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import (
55
"fmt"
66
"reflect"
77
"strings"
8+
9+
"github.com/go-playground/universal-translator"
810
)
911

1012
const (
1113
fieldErrMsg = "Key: '%s' Error:Field validation for '%s' failed on the '%s' tag"
1214
)
1315

16+
type ValidationErrorsTranslations map[string]string
17+
1418
// InvalidValidationError describes an invalid argument passed to
1519
// `Struct`, `StructExcept`, StructPartial` or `Field`
1620
type InvalidValidationError struct {
@@ -39,18 +43,40 @@ func (ve ValidationErrors) Error() string {
3943

4044
buff := bytes.NewBufferString("")
4145

42-
var err *fieldError
46+
var fe *fieldError
4347

4448
for i := 0; i < len(ve); i++ {
4549

46-
err = ve[i].(*fieldError)
47-
buff.WriteString(err.Error())
50+
fe = ve[i].(*fieldError)
51+
buff.WriteString(fe.Error())
4852
buff.WriteString("\n")
4953
}
5054

5155
return strings.TrimSpace(buff.String())
5256
}
5357

58+
func (ve ValidationErrors) Translate(ut ut.Translator) ValidationErrorsTranslations {
59+
60+
trans := make(ValidationErrorsTranslations)
61+
62+
var fe *fieldError
63+
64+
for i := 0; i < len(ve); i++ {
65+
fe = ve[i].(*fieldError)
66+
67+
// // in case an Anonymous struct was used, ensure that the key
68+
// // would be 'Username' instead of ".Username"
69+
// if len(fe.ns) > 0 && fe.ns[:1] == "." {
70+
// trans[fe.ns[1:]] = fe.Translate(ut)
71+
// continue
72+
// }
73+
74+
trans[fe.ns] = fe.Translate(ut)
75+
}
76+
77+
return trans
78+
}
79+
5480
// FieldError contains all functions to get error details
5581
type FieldError interface {
5682

@@ -118,6 +144,13 @@ type FieldError interface {
118144
//
119145
// // eg. time.Time's type is time.Time
120146
Type() reflect.Type
147+
148+
// returns the FieldError's translated error
149+
// from the provided 'ut.Translator' and registered 'TranslationFunc'
150+
//
151+
// NOTE: is not registered translation can be found it returns the same
152+
// as calling fe.Error()
153+
Translate(ut ut.Translator) string
121154
}
122155

123156
// compile time interface checks
@@ -128,6 +161,7 @@ var _ error = new(fieldError)
128161
// with other properties that may be needed for error message creation
129162
// it complies with the FieldError interface
130163
type fieldError struct {
164+
v *Validate
131165
tag string
132166
actualTag string
133167
ns string
@@ -166,8 +200,18 @@ func (fe *fieldError) StructNamespace() string {
166200
// Field returns the fields name with the tag name taking precedence over the
167201
// fields actual name.
168202
func (fe *fieldError) Field() string {
169-
// return fe.field
203+
170204
return fe.ns[len(fe.ns)-int(fe.fieldLen):]
205+
// // return fe.field
206+
// fld := fe.ns[len(fe.ns)-int(fe.fieldLen):]
207+
208+
// log.Println("FLD:", fld)
209+
210+
// if len(fld) > 0 && fld[:1] == "." {
211+
// return fld[1:]
212+
// }
213+
214+
// return fld
171215
}
172216

173217
// returns the fields actual name from the struct, when able to determine.
@@ -202,3 +246,23 @@ func (fe *fieldError) Type() reflect.Type {
202246
func (fe *fieldError) Error() string {
203247
return fmt.Sprintf(fieldErrMsg, fe.ns, fe.Field(), fe.tag)
204248
}
249+
250+
// Translate returns the FieldError's translated error
251+
// from the provided 'ut.Translator' and registered 'TranslationFunc'
252+
//
253+
// NOTE: is not registered translation can be found it returns the same
254+
// as calling fe.Error()
255+
func (fe *fieldError) Translate(ut ut.Translator) string {
256+
257+
m, ok := fe.v.transTagFunc[ut]
258+
if !ok {
259+
return fe.Error()
260+
}
261+
262+
fn, ok := m[fe.tag]
263+
if !ok {
264+
return fe.Error()
265+
}
266+
267+
return fn(ut, fe)
268+
}

examples/translations/main.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/go-playground/locales/en"
7+
"github.com/go-playground/universal-translator"
8+
"gopkg.in/go-playground/validator.v9"
9+
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
10+
)
11+
12+
// User contains user information
13+
type User struct {
14+
FirstName string `validate:"required"`
15+
LastName string `validate:"required"`
16+
Age uint8 `validate:"gte=0,lte=130"`
17+
Email string `validate:"required,email"`
18+
FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
19+
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
20+
}
21+
22+
// Address houses a users address information
23+
type Address struct {
24+
Street string `validate:"required"`
25+
City string `validate:"required"`
26+
Planet string `validate:"required"`
27+
Phone string `validate:"required"`
28+
}
29+
30+
// use a single instance , it caches struct info
31+
var (
32+
uni *ut.UniversalTranslator
33+
validate *validator.Validate
34+
)
35+
36+
func main() {
37+
38+
// NOTE: ommitting allot of error checking for brevity
39+
40+
en := en.New()
41+
uni = ut.New(en, en)
42+
43+
// this is usually know or extracted from http 'Accept-Language' header
44+
// also see uni.FindTranslator(...)
45+
trans, _ := uni.GetTranslator("en")
46+
47+
validate = validator.New()
48+
en_translations.RegisterDefaultTranslations(validate, trans)
49+
50+
translateAll(trans)
51+
translateIndividual(trans)
52+
translateOverride(trans) // yep you can specify your own in whatever locale you want!
53+
}
54+
55+
func translateAll(trans ut.Translator) {
56+
57+
type User struct {
58+
Username string `validate:"required"`
59+
Tagline string `validate:"required,lt=10"`
60+
Tagline2 string `validate:"required,gt=1"`
61+
}
62+
63+
user := User{
64+
Username: "Joeybloggs",
65+
Tagline: "This tagline is way too long.",
66+
Tagline2: "1",
67+
}
68+
69+
err := validate.Struct(user)
70+
if err != nil {
71+
72+
// translate all error at once
73+
errs := err.(validator.ValidationErrors)
74+
75+
// returns a map with key = namespace & value = translated error
76+
// NOTICE: 2 errors are returned and you'll see something surprising
77+
// translations are i18n aware!!!!
78+
// eg. '10 characters' vs '1 character'
79+
fmt.Println(errs.Translate(trans))
80+
}
81+
}
82+
83+
func translateIndividual(trans ut.Translator) {
84+
85+
type User struct {
86+
Username string `validate:"required"`
87+
}
88+
89+
var user User
90+
91+
err := validate.Struct(user)
92+
if err != nil {
93+
94+
errs := err.(validator.ValidationErrors)
95+
96+
for _, e := range errs {
97+
// can translate each error one at a time.
98+
fmt.Println(e.Translate(trans))
99+
}
100+
}
101+
}
102+
103+
func translateOverride(trans ut.Translator) {
104+
105+
validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
106+
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
107+
}, func(ut ut.Translator, fe validator.FieldError) string {
108+
t, _ := ut.T("required", fe.Field())
109+
110+
return t
111+
})
112+
113+
type User struct {
114+
Username string `validate:"required"`
115+
}
116+
117+
var user User
118+
119+
err := validate.Struct(user)
120+
if err != nil {
121+
122+
errs := err.(validator.ValidationErrors)
123+
124+
for _, e := range errs {
125+
// can translate each error one at a time.
126+
fmt.Println(e.Translate(trans))
127+
}
128+
}
129+
}

struct_level.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func (v *validate) ReportError(field interface{}, fieldName, structFieldName, ta
117117

118118
v.errs = append(v.errs,
119119
&fieldError{
120+
v: v.v,
120121
tag: tag,
121122
actualTag: tag,
122123
ns: v.str1,
@@ -132,6 +133,7 @@ func (v *validate) ReportError(field interface{}, fieldName, structFieldName, ta
132133

133134
v.errs = append(v.errs,
134135
&fieldError{
136+
v: v.v,
135137
tag: tag,
136138
actualTag: tag,
137139
ns: v.str1,

translations.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package validator
2+
3+
import "github.com/go-playground/universal-translator"
4+
5+
// TranslationFunc is the function type used to register or override
6+
// custom translations
7+
type TranslationFunc func(ut ut.Translator, fe FieldError) string
8+
9+
// RegisterTranslationsFunc allows for registering of translations
10+
// for a 'ut.Translator' for use withing the 'TranslationFunc'
11+
type RegisterTranslationsFunc func(ut ut.Translator) error

0 commit comments

Comments
 (0)