-
Notifications
You must be signed in to change notification settings - Fork 225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add translation feature. #91
Conversation
Thank you for your attempt to add message translation support! |
you're welcome qiangxue, OK, Each rule can return
and finally, after validating our data, we can set lang on erros and get translation in any language that we want. Are you ok with this changes? |
Hi @qiangxue
everything works the same as before and we have feature-rich validation errors now :) |
@qiangxue can you see my PR, please? |
@mehran-prs I have reviewed your code again. I really like the idea of enhancing the validation error definition so that it supports translation. However, I think i18n/translation doesn't belong to this library. There are existing libraries doing this. Here's what I'm thinking:
I'd like to hear your comments. Also, before you spend time creating a major PR, can we discuss about the design first? I feel bad when denying a PR like this because I know you have spent a lot of time on it. Thank you! |
You're right @qiangxue, it's better to have a translator interface instead of implementing i18n/translator. I had to talk to you before creating PR. In addition to error struct fields that you said, I think it should also have:
I think something like this can be good for us:
var ValidationKeysPrefix="validation_"
// e.g on InRule :
func (r InRule) Key(key string) ErrMessage {
r.translationKey=key
return r
}
type ErrMessage struct {
translationKey string
params map[string]interface{}
plurar interface{}
message string // default message that using for Error() method.
}
// definition of methods that expose params,translationkey, ...
// Translate method on ErrMessage:
func (e ErrMessage) Translate(t Translator) (string, error) {
return t.TranslateSingleFieldErr(e)
}
func (e Errors) Translate(t Translator) (string, error) {
// use t.TranslateStructFieldErr(field string,err) on each error
}
type Translator interface {
TranslateStructFieldErr(field string,err ErrMessage) (string,error)
TranslateSingleFieldErr(err ErrMessage) (string,error)
} Are you ok with something like this? |
I'm thinking about an even simpler solution which is still flexible enough to support i18n. First define an Error struct:
The Error struct implements the error interface and also the methods exposing its fields. This definition should be sufficient for representing most error messages that need to be translated. For each validation rule, modify its For error messages that need more complex translation rules, users can create their own error types and pass them into the rules. |
…ser can specify language on getting the error message.
OK, So I implement it. |
@qiangxue I updated my PR. If you think my repo needs to squash commits, just say. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good start! I left some comments. Thanks!
date.go
Outdated
message: "must be a valid date", | ||
rangeMessage: "the data is out of range", | ||
message: messages["date"], | ||
rangeMessage: messages["date_range"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be cleaner, and we can use the same pattern for other built-in rules:
return DateRule{
layout: layout,
err: NewError("validation_date_invalid", "must be a valid date"),
errRange: NewError("validation_date_range_invalid", "the data is out of range"),
}
In Validate()
, we choose the error and parameterize it, if needed.
The Error()
function simply updates the message in the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, for the built-in rules, we should use a common naming convention for error code: validation_{RuleName}_{ErrorName}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I change all validation codes to follow the same pattern.
error.go
Outdated
@@ -36,6 +53,86 @@ func (e internalError) InternalError() error { | |||
return e.error | |||
} | |||
|
|||
// Code set the error's translation code. | |||
func (e Error) Code(code string) Error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the Go naming convention for getters and setters is:
- getter:
Code() string
- setter:
SetCode(code string)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I change it.
error.go
Outdated
|
||
// Translate get a translator that must implemented Translator and | ||
// return translated errors. | ||
func (e Error) Translate(t Translator) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking to drop the translate methods completely from this library. Since the new Error
struct and the built-in rules carry/expose the error code information, the translation feature can be done separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, so I remove it.
messages.go
Outdated
|
||
package validation | ||
|
||
var messages = map[string]string{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should get rid of this map. It doesn't provide extra value while it requires us to type the key strings in multiple places, which could cause typo issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I remove it, but if users want to see a list of keys to translate, by this map they can simply detect what keys should translate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I understood your thought when I saw this. I have created I18N libraries and I18N-enabled applications before. To determine which messages need to be translated, we created simple regex-based code scanners which can easily find all messages that need translation from a huge code base. I know other I18N systems also support dynamic detection of I18N messages. So I think we don't need to worry too much about finding out what messages need to be translated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right
error_test.go
Outdated
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
// TestTranslator is a mocked struct that implements Translator. | ||
type TestTranslator struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if we drop NewStringValidator
, then we need to define all our string validators with the extra call to the SetCode
method, something like NewStringRule(isEmail,"must be a valid email address").SetCode("validation_emai_invalidl")
, in addition to this, we need to define functions with good signature and deprecate old functions.
Yea, I remove TestTranslator
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean change the signature of NewStringRule
to be the same as NewStringValidator
, since we are upgrading to a new major version.
is/rules.go
Outdated
"gopkg.in/asaskevich/govalidator.v9" | ||
) | ||
|
||
var ( | ||
// Email validates if a string is an email or not. | ||
Email = validation.NewStringRule(govalidator.IsEmail, "must be a valid email address") | ||
Email = validation.NewStringValidator(govalidator.IsEmail, "validation_email_invalid", "must be a valid email address") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we change the error code of these string rules to be something like validation_is_email
so that they are more readable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we do not need to validation
prefix nor invalid
postfix because the translator can append and prepend some strings. we just need to something like invalid_email
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The prefix is like a namespace to prevent potential conflict with other messages in an application.
I didn't say we need invalid
suffix for string rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You right, but I think namespacing can be handled by translators or any other function that want to use error codes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How will a translator do namespacing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Translator gets the translation key(code prop in Error), so it can add every prefix that wants.
something like this:
// user's translator pass error's code and params to translate function:
func translate(code string, params map[string]interface{})string {
// User decide to has "v_" as vaildation error codes prefix.
code:=fmt.Sprintf("v_%s",code)
// Translate...
}
length.go
Outdated
@@ -42,13 +26,15 @@ func Length(min, max int) LengthRule { | |||
func RuneLength(min, max int) LengthRule { | |||
r := Length(min, max) | |||
r.rune = true | |||
return r | |||
|
|||
return r.detectMessage() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some code refactoring is needed to make it more efficient - currently detectMessage()
is called twice.
See the comment in detectMessage()
length.go
Outdated
return nil | ||
} | ||
|
||
func (v LengthRule) detectMessage() LengthRule { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think func buildLengthRuleError(min, max int) Error
can make the dependency of the code more explicit and easier to test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea
error.go
Outdated
} | ||
|
||
// SetParams set the error's params. | ||
func (e Error) SetParams(params map[string]interface{}) Error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also add AddParam(name string, value interface{})
? This will make setting a single parameter (hopefully a main use scenario) easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, Sure
not_nil.go
Outdated
// NotNil is a validation rule that checks if a value is not nil. | ||
// NotNil only handles types including interface, pointer, slice, and map. | ||
// All other types are considered valid. | ||
var NotNil = notNilRule{message: "is required"} | ||
var NotNil = notNilRule{err: NewError("validation_not_nil_invalid", "is required")} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validation_not_nil_required
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about something like is_blank
or required
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have validation_required
. is_blank
is not very clear.
required.go
Outdated
// Required is a validation rule that checks if a value is not empty. | ||
// A value is considered not empty if | ||
// - integer, float: not zero | ||
// - bool: true | ||
// - string, array, slice, map: len() > 0 | ||
// - interface, pointer: not nil and the referenced value is not empty | ||
// - any other types | ||
var Required = requiredRule{message: "cannot be blank", skipNil: false} | ||
var Required = requiredRule{err: NewError("validation_required_is_blank", "cannot be blank"), skipNil: false} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validation_required
required.go
Outdated
|
||
// NilOrNotEmpty checks if a value is a nil pointer or a value that is not empty. | ||
// NilOrNotEmpty differs from Required in that it treats a nil pointer as valid. | ||
var NilOrNotEmpty = requiredRule{message: "cannot be blank", skipNil: true} | ||
var NilOrNotEmpty = requiredRule{err: NewError("validation_nil_or_not_empty_is_blank", "cannot be blank"), skipNil: true} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validation_nil_or_not_empty_required
v.err = v.err.SetMessage(message) | ||
return v | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about adding ErrorStruct() *Error
and drop the following two methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should keep those two functions and add this function also.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why keeping the two functions? If we add the new one, developers can first get Error
and then call the methods of Error
to set Code, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because if we keep those functions, developers need to one call for setting custom error message, by defining this function need to two call, if we define two functions one for setting message, another for getting error, developers can simply set message just like old version, and also can get error if want to set params or any other thing.
I think we should add ErrorStruct() *Error
to all built-in validation rules, so developers can set custom error codes on each validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it more about the overall translation support. Below I'm using ThresholdRule
as an example to explain the new changes needed since ThresholdRule
is one of the most complex rules for the translation support:
var (
// predefined errors that can be customized globally to affect all instances of the same type of the rule
// used in both i18n and non-i18n scenarios
ErrThresholdLessThanRequired = NewError("validation_threshold_less_than_required", "must be less than {{.threshold}}")
ErrThresholdNoGreaterThanRequired = NewError("validation_threshold_no_greater_than_required", "must be no greater than {{.threshold}}")
ErrThresholdGreaterThanRequired = NewError("validation_threshold_greater_than_required", "must be greater than {{.threshold}}")
ErrThresholdNoLessThanRequired = NewError("validation_threshold_no_less_than_required", "must be no less than {{.threshold}}")
)
// used in non-i18n scenarios to customize an error message for a single instance of the rule
func (r ThresholdRule) Error(message string) ThresholdRule {...}
// used in i18n scenarios to customize the error for a single instance of the rule
func (r ThresholdRule) ErrorObject(err error) ThresholdRule {...}
StringRule
is a bit special because it is a base class. Here's the change I'm thinking about:
// used in non-i18n scenarios
func NewStringRule(validator stringValidator, message string) StringRule {...}
// used in i18n scenarios
func NewStringRuleWithError(validator stringValidator, err error) StringRule {...}
// This error declaration allows global customization of the email rule error
// String.ErrorObject() allows error customization of a single rule instance
ErrEmail = NewError("validation_is_email", "must be a valid email address")
Email = validation.NewStringRuleWithError(govalidator.IsEmail, ErrEmail)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about adding
ErrorStruct() *Error
and drop the following two methods?
We can not use ErrorStruct() *Error
because:
- If defining
func (r *MyRule) ErrorStruct() *Error
, this error affects all of the other uses ofMyRule
. - If defining
func (r MyRule) ErrorStruct() *Error
, this error do not has any affect on our rule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, Predefining errors is very good, I change the source like what you said.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for being patient and taking the effort of making all these changes. I think we are very close to the final version.
UPGRADE.md
Outdated
@@ -1,5 +1,21 @@ | |||
# Upgrade Instructions | |||
|
|||
## Upgrade from 3.x to 4.x | |||
* If you want to support i18n for your defined string rules, instead of `NewStringRule` use the `NewStringRuleWithError` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the upgrade instructions, we should only describe the BC-breaking changes and the instructions on how to upgrade. Here we should list the messages which contain parameter placeholders because they are changed to named ones.
The addition of NewStringRuleWithError
is part of the new I18N feature, which should be included in the release description.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I change it.
date.go
Outdated
// ErrDateInvalid is the error that returns in case of an invalid date. | ||
ErrDateInvalid = NewError("validation_date_invalid", "must be a valid date") | ||
// ErrDateOutOfRange is the error that returns in case of an invalid date. | ||
ErrDateOutOfRange = NewError("validation_date_out_of_range", "the data is out of range") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change it to the date is out of range
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.
} | ||
|
||
// ErrorObject sets the error struct that is used when the value being validated is not a valid date.. | ||
func (r DateRule) ErrorObject(err Error) DateRule { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter type should be error
instead of Error
. The former would allow users to pass in other kinds of error implementation which may support more advanced I18N features (such as plural form).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I agree with you, but if I change it, in all other methods of rules that set error properties we must assert that if it is Error
then set its value, otherwise return an error. everything can be complicated unless we change the Error
type to be an interface with our needed methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we need an interface here:
type Error interface {
Error() string
Code() string
Message() string
SetMessage(string)
Params() map[string]interface{}
SetParams(map[string]interface{})
}
type ErrorObject struct {...} // the current Error struct is renamed as ErrorObject or something better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@qiangxue I think Error interface should be something like this:
type Error interface {
Error() string
Code() string
SetCode(string) Error
Message() string
SetMessage(string) Error
Params() map[string]interface{}
SetParams(map[string]interface{}) Error
AddParam(string,string) Error
}
this is because if we do not return Error and get a pointer in seters, so in NewError
we have to return a pointer to the ErrorObject
, so all predefined errors will be pointers.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to keep the interface minimal. That's why SetCode
and AddParams
are not in my design, because they are not used/required anywhere.
Yes, NewError
should return a pointer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if return a pointer, changes to predefined errors affect base error, e.g
myErr=validation.ErrEmail
err.SetParams(map[string]interface{}{"user":"Mehran"})
this line set params on the base error also. we should get a copy of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. We should add a Clone()
method to ErrorObject
(not to the Error
interface).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are assigning the Error
interface to each validation rule's err
property, so on each validation rule creation, we must first check if our predefined error object is cloneable (since our users can change predefined errors and do not implement Clone method) then clone it, otherwise, simply assign it. our rule initialization is finally something like this:
var ErrDateInvalid = NewError("validation_date_invalid", "must be a valid date")
func Date(layout string) DateRule {
return DateRule{
layout: layout,
err: TryToClone(ErrDateInvalid),
rangeErr: TryToClone(ErrDateOutOfRange),
}
}
type CloneableErrorObject interface {
Clone() Error
}
func TryToClone(err Error) Error {
if cloneable, ok := err.(CloneableErrorObject); ok {
return cloneable.Clone()
}
return err
}
I think we should remove all of these complexities and just change the ErrorObject
setters signature to get just copy, not a pointer, e.g:
// SetCode set the error's translation code.
func (e ErrorObject) SetCode(code string) Error {
e.code = code
return e
}
What do you think @qiangxue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. Let's modify the signature SetCode
and SetParams
so that they return Error
. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, you're welcome :)
|
||
// AddParam add parameter to the error's parameters. | ||
func (e *Error) AddParam(name string, value interface{}) { | ||
e.params[name] = value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to check if params
is initialized. Otherwise, it will panic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I fix it.
rangeMessage string | ||
layout string | ||
min, max time.Time | ||
err, rangeErr Error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type should be error
, and ErrorObject()
should also use error
as the parameter type. The reason for this is to support user-defined error types which may allow more advanced i18n features (e.g. plural forms) or other features.
Note that because of this change, Error()
method of the rule needs to be implemented differently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, see my comment on ErrorObject
please.
match.go
Outdated
"regexp" | ||
) | ||
|
||
// ErrValidationMatchInvalid is the error that returns in case of invalid format. | ||
var ErrValidationMatchInvalid = NewError("validation_match_invalid", "must be in a valid format") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you rename it to ErrMatchInvalid
so that it's consistent with other places?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.
string.go
Outdated
// Code sets the rule's translation code (translation key). | ||
func (r StringRule) Code(code string) StringRule { | ||
r.err.SetCode(code) | ||
return r | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should still remove the above two methods ErrParams
and Code
. If a user wants to set them (which is very uncommon), he can still achieve it via ErrorObject()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah they can achieve it via ErrorObject()
, but their code change from e.g :
validation.Email.Code("v_invalid_email")
to
errEmail:=validation.ErrEmail
errEmail.SetCode("v_invalid_email")
validation.Email.ErrorObject(errEmail)
Are you ok with it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can hardly think of any reason that a user would want to change error code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so I remove them.
Great job! Thank you very much for your contribution! |
you're welcome Qiangxue :) |
Hi,
I added the translation feature and also modified all the rules to use this feature.
I assume each rule's
message
property is the custom message, so any user sets it, that rule returns the user's message, otherwise, return the translated message.Each rule can return the translated message if it does not exists return the English message, and if the English message does not exist for that
rule, return rule's default message.
All functions work just like before Except one function:
I Changed
NewStringRule
function signature fromfunc validation.NewStringRule(validation stringValidator,message string)
tofunc validation.NewStringRule(validation stringValidator,ruleName string)
this is because we don't need the message here anymore, just need to get a name for that rule and get the rule's message from the translation map.
I also changed your package version from
3
to4
because ofNewStringRule
function'ssignature changes.