Localized IvalidatableObject

Topics: Writing modules
Mar 1, 2013 at 8:19 AM
Is it possible to somehow localize (translate) the error messages from the validate method in a a model that uses the IValidateableObject?

In the example below I would like to be able to translate the message "The password and the confirmpassword did not match"
public class RegisterModel : IValidatableObject
    {
        [Required]
        public int? CustomerId { get; set; }
        [Required]
        public string Verifier { get; set; }
        [Required]
        public string EmailAsUsername { get; set; }
        [Required]
        public string Password { get; set; }
        [Required]
        public string ConfirmPassword { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
            if (!Password.Equals(ConfirmPassword))
                yield return new ValidationResult("The password and the confirmpassword did not match");
        }
    }
Mar 2, 2013 at 9:03 AM
Mar 3, 2013 at 10:07 AM
I ended up using the trick/fix/hack from this discussion: http://orchard.codeplex.com/discussions/266275

Where I created an extension method like this:
public static void LocalizeValidationErrorMessages(this ModelStateDictionary dictionary, Localizer T)
        {
            foreach (var state in dictionary.Values)
            {
                if (state.Errors.Count <= 0) {
                    continue;
                }
                
                //Rebuild the ModelErrorCollection and re-assign it after because the ModelError.ErrorMessage is get{} only.
                var errorCollection = new ModelErrorCollection();
                foreach (var e in state.Errors)
                {
                    var error = new ModelError(T(e.ErrorMessage).ToString());
                    errorCollection.Add(error);
                }
                state.Errors.Clear();
                foreach (var e in errorCollection) state.Errors.Add(e); //doesn't have an AddRange
            }    
        }
Which is called from the controller like this (passes in its own localizer):
if (!ModelState.IsValid) {
    ModelState.LocalizeValidationErrorMessages(T);
    //Redisplay view
}
The localized error messages must be added manually to the localization files though.
This fix/hack works but more ideal would be to inject the Localizer from the DefaultModelBinder, but I have not come that far yet :-)
Mar 17, 2013 at 6:37 PM
Edited Mar 17, 2013 at 6:38 PM
@vgillestad:

I have been trying to figure out how to get localization to work. Assuming your doing something like
@Html.EditorFor(m=>m.ConfirmPassowrd)
@Html.ValidationMessageFor(m=>m.ConfirmPassowrd)
in your view, how do you write out the localized error message manually in the DB (I don't literally mean how do I get into the DB - what convention do you use in the DB)?

Let's say you have MyModule.Models.RegisterModel, how do you write it out for:
#:  <--WHAT GOES HERE?
#| msgid "" <--WHAT GOES INSIDE THE QUOTES?
msgid ""
msgstr ""
which is what the .po file structure is like?

I wish this were more intuitive or that there was a better explanation.

I have just tried decorating my properties with [LocalizedRequired], but this throws an error about not taking 0 arguments (and putting ErrorMessage="" does not work). I then tried [Required], but still nothing.

Thanks.
Mar 17, 2013 at 7:06 PM
This is how my model looks like:
public class RegisterModel : IValidatableObject
    {
        [Required, DisplayName("CustomerId")]
        public int? CustomerId { get; set; }
        [Required, DisplayName("Verifier")]
        public string Verifier { get; set; }
        [Required, DisplayName("EmailAsUsername")]
        public string EmailAsUsername { get; set; }
        [Required, DisplayName("Password")]
        public string Password { get; set; }
        [Required, DisplayName("ConfirmPassword")]
        public string ConfirmPassword { get; set; }

        //Error messages in the validate method requires the use of LocalizeValidationErrorMessages to be localized.
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
            if (!EmailAsUsername.IsValidEmail())
                yield return new ValidationResult("You must spesify a valid email.", new[] { "EmailAsUsername" });
            if (!Password.Equals(ConfirmPassword))
                yield return new ValidationResult("The two provided passwords did not match.", new[] { "Password" });
        }
    }
This is my localization file:
#: System.ComponentModel.DataAnnotations.RequiredAttribute
#| msgid "The {0} field is required."
msgid "The {0} field is required."
msgstr "Feltet {0} er påkrevd."

#: Enoro.Framework.Models.Users.RegisterModel
#| msgid "CustomerId"
msgid "CustomerId"
msgstr "kundenummer"

#: Enoro.Framework.Models.Users.RegisterModel
#| msgid "Verifier"
msgid "Verifier"
msgstr "verifiserer"

#: Enoro.Framework.Models.Users.RegisterModel
#| msgid "EmailAsUsername"
msgid "EmailAsUsername"
msgstr "brukernavn (e-post)"

#: Enoro.Framework.Models.Users.RegisterModel
#| msgid "Password"
msgid "Password"
msgstr "passord"

#: Enoro.Framework.Models.Users.RegisterModel
#| msgid "ConfirmPassword"
msgid "ConfirmPassword"
msgstr "bekreft passord"

#: Enoro.Framework.Controllers.AccountController
#| msgid "You must spesify a valid email."
msgid "You must spesify a valid email."
msgstr "Du må angi en gyldig e-postadresse."

#: Enoro.Framework.Controllers.AccountController
#| msgid "The two provided passwords did not match."
msgid "The two provided passwords did not match."
msgstr "De to angitte passordene var ikke like."
I hope this answers some of your questions?
I actually think you can take away the entire "#:" line. Have not tested it to much though :-)
Mar 17, 2013 at 9:09 PM
Edited Mar 17, 2013 at 9:13 PM
EDIT Sorry - made a mistake. Going to try this out now.
Nov 15, 2013 at 4:39 PM
Thanks, vgillestad, for sharing your approach. It works well.

I had another error message I wanted to translate: “The value ‘{0}’ is not valid for {1}.” This error crops up for instance when entering text into an integer field (serverside validation). The message is constructed in the DefaultModelBinder in MVC (method GetValueInvalidResource(controllerContext)).

I ended up creating a resource file in Orchard.Web, like this post suggested: http://code-inside.de/blog-in/2012/01/23/fix-the-value-x-is-not-valid-for-foo-in-asp-net-mvc/

Do you know a better way to solve this?
I could not imagine how to somehow derive from the DefaultModelBinder and overwrite some methods, just to apply the Localizer to the template string for this error. I would rather not have to add a line to the Global.asax and a resource file to Orchard.Web, but instead solve this problem via the po-files.
Nov 18, 2013 at 7:34 AM
Your question made me find out that I have the same problem. I have no solution for it though. Maybe this issue is one for the "Orchard Issues" section? Because the string never hits the localizer.

You could of course check the modelstate in you controller for this errormessage and translate it "manually" but that would not be a good solution.

Please keep me posted if you find any solution!