Thursday, September 6, 2012

Globalize and jQuery Validation

Update 27/08/2013

To make it easier for people to use the approach detailed in this post I have created a repository for jquery.validate.globalize.js on GitHub here.

This is also available as a nuget package here.

To see a good demo take a look here.

Background

I've written before about a great little library called Globalize which makes locale specific number / date formatting simple within JavaScript. And I've just stumbled upon an old post written by Scott Hanselman about the business of Globalisation / Internationalisation / Localisation within ASP.NET. It's a great post and I recommend reading it (I'm using many of the approaches he discusses).

jQuery Global is dead... Long live Globalize!

However, there's one tweak I would make to Scotts suggestions and that's to use Globalize in place of the jQuery Global plugin. The jQuery Global plugin has now effectively been reborn as Globalize (with no dependancy on jQuery). As far as I can tell jQuery Global is now disappearing from the web - certainly the link in Scotts post is dead now at least. I've ripped off been inspired by the "Globalized jQuery Unobtrusive Validation" section of Scotts article and made jquery.validate.globalize.js.

And for what it's worth jquery.validate.globalize.js applies equally to standard jQuery Validation as well as to jQuery Unobtrusive Validation. I say that as the above JavaScript is effectively a monkey patch to the number / date / range / min / max methods of jQuery.validate.js which forces these methods to use Globalize's parsing support instead.

Here's the JavaScript:

The above script does 2 things. Firstly it monkey patches jquery.validate.js to make use of Globalize.js number and date parsing in place of the defaults. Secondly it initialises Globalize to relevant current culture driven by the html lang property. So if the html tag looked like this:

<html lang="de-DE">
...
</html>

Then Globalize would be initialised with the "de-DE" culture assuming that culture was available and had been served up to the client. (By the way, the Globalize initialisation logic has only been placed in the code above to demonstrate that Globalize needs to be initialised to the culture. It's more likely that this initialisation step would sit elsewhere in a "proper" app.)

Wait, where's html lang getting set?

In Scott's article he created a MetaAcceptLanguage helper to generate a META tag like this: <meta name="accept-language" content="en-GB" /> which he used to drive Globalizes specified culture.

Rather than generating a meta tag I've chosen to use the lang attribute of the html tag to specify the culture. I've chosen to do this as it's more in line with the W3C spec. But it should be noted this is just a different way of achieving exactly the same end.

So how's it getting set? Well, it's no great shakes; in my _Layout.cshtml file my html tag looks like this:

<html lang="@System.Globalization.CultureInfo.CurrentUICulture.Name">

And in my web.config I have following setting set:

<configuration>
  <system.web>
    <globalization culture="auto" uiCulture="auto" />
    <!--- Other stuff.... -->
  </system.web>
</configuration>

With both of these set this means I get <html lang="de-DE"> or <html lang="en-GB"> etc. depending on a users culture.

Serving up the right Globalize culture files

In order that I send the correct Globalize culture to the client I've come up with this static class which provides the user with the relevant culture URL (falling back to the en-GB culture if it can't find one based your culture):

Putting it all together

To make use of all of this together you'll need to have the html lang attribute set as described earlier and some scripts output in your layout page like this:

<script src="@Url.Content("~/Scripts/jquery.js")" type="text/javascript"></script>
<script src="@Url.Content(GlobalizeUrls.Globalize)" type="text/javascript"></script>
<script src="@Url.Content(GlobalizeUrls.GlobalizeCulture)" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/scripts/jquery.validate.globalize.js")" type="text/javascript"></script>

@* Only serve the following script if you need it: *@
<script src="@Url.Content("~/scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>

Which will render something like this:

<script src="/Scripts/jquery.js" type="text/javascript"></script>
<script src="/Scripts/globalize.js" type="text/javascript"></script>
<script src="/scripts/globalize/globalize.culture.en-GB.js" type="text/javascript"></script>
<script src="/Scripts/jquery.validate.js" type="text/javascript"></script>
<script src="/Scripts/jquery.validate.globalize.js" type="text/javascript"></script>
<script src="/Scripts/jquery.validate.unobtrusive.js" type="text/javascript"></script>

This will load up jQuery, Globalize, your Globalize culture, jQuery Validate, jQuery Validates unobtrusive extensions (which you don't need if you're not using them) and the jQuery Validate Globalize script which will set up culture aware validation.

Finally and just to re-iterate, it's highly worthwhile to give Scott Hanselman's original article a look. Most all the ideas in here were taken wholesale from him!

28 comments :

  1. Really Nice! straight 2the point!!!
    and Yesssss You can make this work!
    cheers

    ReplyDelete
    Replies
    1. and yes, i forgot to thank you!!!:O
      so thank you!

      Delete
    2. My pleasure Boris - you've made me chuckle!

      Delete
  2. Thank you. Amazing job. Very useful.

    ReplyDelete
  3. Wowww!!!!! Looking for 3 days to solve this issue... You were the only one who made it working in few simple steps! Thanks!

    ReplyDelete
  4. Hi John,
    useful article and thanks for your efforts

    Unfortunately I have an issue with the NuGet package. It is forcing me to upgrade to the latest jQuery, but I need to maintain for IE7, :(, therefore I am stuck with jQuery 1.10. I assume it is forcing the upgrade because the dependencies do not specify a minimum version. Would it be possible for you to alter this so I can make use of the NuGet package? In the mean time I've just imported it by hand.

    I have an additional query. In the source supplied on GitHub you've not added the html lang reading code. Am I correct in assuming this was intentional to not force people down the same route as this article and we need to add this code ourselves (or some other means of achieving the same thing, like Scott's meta tag)? I just want to check I am not missing something else where Globalize figures this one out for itself (which doesn't work for me if there is).

    Cheers,
    Adam

    ReplyDelete
    Replies
    1. Hi Adam,

      Thanks for the feedback - I'll look into the jQuery version issue you mention as soon as I get a moment. (As it happens I planned to make a minor tweak to the library anyway.)

      You're quite correct that the code for determining culture is separate from the library itself. This is deliberate as not everyone is using the same server side technologies and this allows for people to with different back ends to all use this.

      Assuming you are using a .NET backend then you might want to take a look at this link:

      http://jqueryvalidationunobtrusivenative.azurewebsites.net/AdvancedDemo/Globalize

      This demonstrates how this might be used with jQuery Validation Unobtrusive Native (a project of mine). However, the "Determine which culture to use" tab should give you what you need in terms of determining the culture server side and initializing client side with that culture. It uses a different approach to Scotts but to the same end. You can take this and use it however feels best for you.

      Hope that helps - any questions then let me know.

      Delete
    2. Thanks. I'll keep an eye on the NuGet package then :-).

      I figured this was the case and wired up as per this article. I'll have a glance over the suggested page to see if there is anything else of use there.

      Thank you for your assistance.

      Delete
    3. No problem Adam - I updated the nuget package on Saturday - you should be good to go!

      Delete
  5. That seems to have done the trick. Thanks for the quick turn around.

    ReplyDelete
  6. John,

    I've got this all integrated and wired up and it appears to be working well, however I have one small issue that I'm stuck on...

    If I enter an invalid date for a DateTime property and tab out of the text box, I instantly get a validation error, however it is always in english and reads: The field DateField must be a date.

    All other fields will display the validation message in the appropriate language, any ideas?

    Thanks!

    ReplyDelete
    Replies
    1. Hi Mike,

      Its hard to tell without seeing the code but I'd speculate that the date message hasn't been customised. You'll need to set the invalid date message. I think this can achieved by adding a data-msg-date attribute which contains your custom message.

      Delete
  7. Hi John,

    This is very nice script, thank you for that. I would like to ask you for a problem that my solution came with: why is the browser culture always taken instead of the Thread.CurrentThread.CurrentCulture? Will that be always the case, because I am not happy using that culture all the time for validations.

    Thank you a lot.

    ReplyDelete
    Replies
    1. Hi Kristina,

      Glad you like it!

      To your question: this is probably a question for Microsoft rather than me :-) If you hook in the below code then this is the behaviour:

      <globalization culture="auto" uiCulture="auto" />

      I don't know for sure but perhaps it's possible to hook into this mechanism and tweak it a little for your own purposes. Best of luck!

      Delete
    2. Thank you John for the prompt answer.

      I am confused a little bit. Can you please tell me if the lang attribute of the html will be always ignored because of the language set in the browser?

      Delete
    3. Hi Kristina,

      The lang attribute is set from the System.Globalization.CultureInfo.CurrentUICulture.Name. The CurrentUICulture itself is driven by whether you have set the globalization web config setting to auto (or indeed to another fixed locale - eg "de-DE"). If it hasn't been set then I believe that the CurrentUICulture is driven by the machine hosting the code.

      Does that answer your question?

      Delete
    4. Kristina - As it is written the lang attribute will always be ignored because jquery.validate.globalize.js just overrides the jquery.validate.js methods like this:

      $.validator.methods.date = function (value, element) {
      var val = Globalize.parseDate(value);
      return this.optional(element) || (val instanceof Date);
      };

      whereas Globalize.parseDate is actually defined like this:

      Globalize.parseDate = function (value, formats, culture) {}

      Since jquery.validate.js is not passing in a culture parameter Globalize will just use the default culture (which will be the UICulture of your browser) even if you have supplied a culture override via the GlobalizeUrls.GlobalizeClientCulture in the article above. If like me you wanted a site selected culture whereby user can view your site in a language other than browser locale then as far as this plugin is concerned you will have to override the override in this plugin like so:

      $.validator.methods.min = function (value, element, param) {
      var val = Globalize.parseFloat(value, null, Globalize.cultureSelector);
      return originalMethods.min.call(this, val, element, param);
      };

      where Globalize.cultureSelector could be set in document.OnReady.





      Delete
  8. Thanks John for this helpful blog :)

    Why not initialise culture in layout page as below:

    $(document).ready(function () {

    // Set Globalize to the current culture driven by the html lang property

    if (currentCulture) {
    Globalize.culture("@Thread.CurrentThread.CurrentUICulture.Name");
    }

    });

    ReplyDelete
    Replies
    1. Hah thats what i just posted on the gitHub page.

      Delete
  9. Great post, helped me get 99% of they way.
    Was not quite working for me as I was MVC bundling, and the order that you bundle does matter otherwise script error occurs $.validator undefined.

    My fix was to make sure I bundle the jquery.validate.globalize.js AFTER jquery.validate.js

    E.g.

    bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
    "~/Scripts/jquery.validate.js",
    "~/Scripts/jquery.validate.unobtrusive.js",
    "~/Scripts/jquery.unobtrusive*",
    "~/Scripts/MvcFoolproofJQueryValidation.*",
    "~/Scripts/mvcfoolproof.unobtrusive*",
    "~/Scripts/jquery.validate.globalize.js"));

    Thanks.

    ReplyDelete
  10. Hi John,,

    I am trying to implement jQuery validation in my MVC application. Its working perfectly fine.

    Number validation by default is working as follows:

    1500 is valid number
    1,500 is valid number
    1,5 is invalid number
    -1 is valid number
    (1) is invalid number
    A is invalid number
    1, is invalid number

    Note: Default culture is en-US


    I implemented Globalization by including javascript files in this sequence
    globalize.jsscript>
    globalize.culture.@(System.Threading.Thread.CurrentThread.CurrentCulture).js
    jquery.validate.js
    jquery.validate.globalize.js
    jquery.validate.unobtrusive.js


    but now the issue is, for en-US culture it behaving as follows

    1500 is valid number
    1,500 is valid number
    1,5 is valid number
    -1 is valid number
    (1) is valid number
    A is invalid number
    1,5 is valid


    Please if u can put some light into it.

    ReplyDelete
  11. I have a problem with different culture.

    When I change CurrentCulture to “fa-IR” that decimal separator for this culture is (“/”).When I put for example 55/02 for decimal field got a validation Error “The field must be a number”

    ReplyDelete
  12. Thanks for sharing this. I really appreciated it and I've learned a lot.

    ReplyDelete
    Replies
    1. Happy to help Sophia. Just so you're aware - Globalize is currently in the process of being re-written (the alpha is now available on GitHub). This code applies to the prior version of Globalize (0.1 I think) which, despite the version number, is actually pretty solid and has handled all of my own use cases.

      All the best,
      John

      Delete
  13. Nice article John. It was very helpful for me.

    Just like to emphasize the importance of add the code below if you download jquery-globalize package from Nuget. I spend few hours to realize that. lol

    $(document).ready(function () {

    // Set Globalize to the current culture driven by the html lang property
    var currentCulture = $("html").prop("lang");
    if (currentCulture) {
    Globalize.culture(currentCulture);
    }
    });

    ReplyDelete