Fixing non-localizable validation messages with JavaScript

There is a nasty globalization bug in ASP.NET MVC: the client-side validation message for numeric is hard coded to a string that is not localizable.

Take the following view model class:

public class SomeViewModel
{
    public int SomeNumber { get; set; }
}

… and this view:

@model SomeViewModel
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
    Html.EnableClientValidation();
    Html.EnableUnobtrusiveJavaScript();
}
@using (Html.BeginForm())
{    
    @Html.EditorFor(vm => vm.SomeNumber)
    @Html.ValidationMessageFor(vm => vm.SomeNumber)
}

If the user enters a non-numeric value into the text box form SomeNumber, client side validation kicks in and displays a message. This is excellent, you can’t have validation for less effort. This message is “The field SomeNumber must be a number.”. Umm. That might need some rewording, especially when you wish to translate the user interface into a language other than English. The trouble is: you can’t.

The string “The field {0} must be a number.” is baked into ASP.NET MVC in a resource file. There is no way to define a different resource file, or overwrite the message in any other way (like there is for server-side default messages by setting DefaultModelBinder.ResourceClassKey). For other messages, like the message for required fields, this is not a big deal because a custom message can be applied by setting a DataAnnotation attribute – however, this is not possible for data type based validation.

The options at hand are:

  • Do without the integration of data types and client side validation, and not use EditorFor. Instead, declare the text box directly, and set the data-val-number attribute that controls the message directly. Yuck – that feels like gearing the progeamming model around a platform bug. I’m not desperate for that yet.
  • Fix ASP.NET MVC. It’s open sourced, you can run your own fixed branch, or at least replace the part that causes the problem. Yes, I might do this, but it causes some maintenance problem – each time a new version of the framework is released without a fix, I’d need to recompile. Sounds like a lot of work.
  • Use AOP to basically do the same. That solutions yields a lot less code, but still has a very tight dependency on the current version of the framework; in addition, I’d need to throw another framework into the project. Maybe.
  • The problem lies within ASP.NET MVC, but in the outcome, it’s just a client-side message. Hence, replacing the non-localized message with a localized one does not have to happen on the server. Let’s see where these messages are applied!

The validation rules and messages for client side validation are defined in HTML5-style “data-” attributes that are attached to the form’s input elements. As an example, this is what ASP.NET emits for the integer value from the example above:

 <input 
	class="text-box single-line" 
	data-val="true" 
	data-val-number="The field SomeNumber must be a number." 
	data-val-required="The SomeNumber field is required." 
	id="SomeNumber" 
	name="SomeNumber"
	type="text"
	value="0" />

There are two JavaScript modules that construct the client side validation:

  • jquery.validate provides all the heavy lifting for evaluating and comparing values, and for displaying the messages.
  • jquery.validate.unobstrusive hooks the validation functionality provided by jquery.validate into the page in an “unobstrusive” way – that is, without any JavaScript code that clutters the page. Instead, attributes on the validated controls declare what should be validated.

When jquery.validate.unobstrusive is loaded, it reads all validation attributes on the current page. This is the first chance to change the content of the attribute. The script is cleverly structured; it attaches a host of adapters that match attributes and then initialize the appropriate jquery.validate function. A set of options is passed through the adapter, containing (among other things) the message. Directly after jquery.validate.unobstrusive is loaded, we can hook into its adapters and change the one for numbers:

(function ($) {
    // Walk through the adapters that connect unobstrusive validation to jQuery.validate.
    // Look for all adapters that perform number validation
    $.each($.validator.unobtrusive.adapters, function () {
        if (this.name === "number") {
            // Get the method called by the adapter, and replace it with one 
            // that changes the message to the jQuery.validate default message
            // that can be globalized. If that string contains a {0} placeholder, 
            // it is replaced by the field name.
            var baseAdapt = this.adapt;
            this.adapt = function (options) {
                var fieldName = new RegExp("The field (.+) must be a number").exec(options.message)[1];
                options.message = $.validator.format($.validator.messages.number, fieldName);
                baseAdapt(options);
            };
        }
    });
} (jQuery));

Now, instead of the message defined by ASP.NET MVC, the default message provided by jQuery.validate is used. This is much better because this one _can_ be localized:

$.validate.messages.number="Eine Nummer für {0}, bitteschön";

Instead of inventing your own messages you should maybe use the jquery-glob plugin that comes with a variety of langauges.

Using this solution in your own project

  1. Copy the JavaScript code above into a new JavaScript file named jquery.validate.unobstrusive.fixMvcNumberMessage.js in the Scripts directory of your ASP.NET MVC project.
  2. Add a reference to that file directly beneath jquery.validate.unobstrusive. The references to scripts are normally held in the _Layout shared view.

Feel free to use the code in this article as it serves you.

About these ads
About

Christian is a software architect/developer. He lives in Germany, reads a lot, and likes cycling.

Tagged with: , ,
Posted in Coding
8 comments on “Fixing non-localizable validation messages with JavaScript
  1. Great post on a really annoying issue with MVC. I have spent days trying to figure out a solution. You have some good solutions for the problem. I cant seem to get your javascript snippet to work (the regexp.exec part throws an error).

    I managed to find another solution though which is a tad more simple.

    Regex dataannotation validation is run before the MVC number validation on the model. Which means you can just add a regexp validation to your property with whatever message you want (our localization is in a ressource file)

    For isntance:
    [RegularExpression(@"\d", ErrorMessage = "Eine Nummer für {0}, bitteschön")]
    public double Value
    {
    get
    {
    return _dto.Value;
    }
    set
    {
    if (value == _dto.Value)
    return;

    _dto.Value = value;
    }
    }

  2. Christian says:

    Hi Michael,

    Thanks for your reply!
    I tried that as well – the results however were kind of inconsistent. Both the attributes for number and regular expression validation are created on the <input> element, and which one gets picked up by jequery-validate “wins”. It looked like chances are in favor for the regular expression, but the number validation still popped up sometimes. Browsers we’ve been testing were Chrom, FF, IE8.

    – Christian

  3. muckelpeter says:

    Another, imho better approach:
    Create your own ClientNumberValidatorProvider by deriving from ClientDataTypeModelValidatorProvider and override GetValidators. Create a new ClientSideNumberValidator by deriving from ModelValidator override GetClientValidationRules.

    Implement sth. like this:

    public override IEnumerable GetClientValidationRules()
    {
    yield return new ModelClientValidationRule { ValidationType = “number”, ErrorMessage = string.Format(CultureInfo.CurrentCulture,
    ValidationMessages.MustBeNumber, Metadata.GetDisplayName())
    };
    }

    Register your new validator on startup:

    protected void Application_Start() {

    // Leave the rest of this method unchanged
    var existingProvider = ModelValidatorProviders.Providers .Single(x => x is ClientDataTypeModelValidatorProvider);
    ModelValidatorProviders.Providers.Remove(existingProvider); ModelValidatorProviders.Providers.Add(new ClientNumberValidatorProvider());

    }

    Done. Without doing nasty jQuery patching..

  4. RCDMK says:

    Thank you very much @Christian.

    I’m a brazilian developer and have searched and tryed almost every solution on the web.
    Since my primary language is pt-BR, all validations mus be set to use our particularities and this was the only think I couldn’t fix.

    This fix was the only one that worked as I expected without having to hack into the MVC core.
    You’re the boss.

  5. diego says:

    i get this error Uncaught TypeError: Cannot read property ‘adapters’ of undefined

  6. Alexander says:

    Thank you very much, Christian!
    Your approach works well for me.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: