Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best way to handle validation with different culture

Tags:

I am trying to build a multilingual MVC application. I have a form in my application and I have field to enter a cost. I am able to create a record using the spanish culture.

But on trying to update the record I am getting jquery validation false. and I am getting a default error message as:

The field must be numeric.

In my view model I have set the following attributes.

[LocalizedDisplayName("Label_Cost")]
[RegularExpression("^[^<>,<|>]+$", ErrorMessage = null, ErrorMessageResourceName = "Error_Message_Html_Tags_Prevented", ErrorMessageResourceType = typeof(Resources))]
[Range(0, 9999.99, ErrorMessage = null, ErrorMessageResourceName = "Error_Message_Cost_Not_Valid", ErrorMessageResourceType = typeof(Resources))]
public decimal? Cost { get; set; }

I have set in my Gobal.asax file with following

protected void Application_AcquireRequestState(object sender, EventArgs e)
{
    try
    {
        HttpCookie cookie = HttpContext.Current.Request.Cookies.Get("CurrentCulture");
        string culutureCode = cookie != null && !string.IsNullOrEmpty(cookie.Value) ? cookie.Value : "en";
        CultureInfo ci = new CultureInfo(culutureCode);
        System.Threading.Thread.CurrentThread.CurrentUICulture = ci;
        System.Threading.Thread.CurrentThread.CurrentCulture =
        CultureInfo.CreateSpecificCulture(ci.Name);
    }
    catch(Exception ex)
    {
        // Code
    }
}

and the above method works as expected at server side in changing the culture . But the client side validation breaks on non-english cultures as javascript recognizes only decimal literals. I'd like to know the best way to extend the mvc client side validation with culture specific validation.

EDIT

With reference to Mike's url I have made following changes in Js bundle. Js bundle is as follows

public static void RegisterBundles(BundleCollection bundles)
{
   BundleTable.EnableOptimizations = true;

  bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));

bundles.Add(new ScriptBundle("~/bundles/globalisation").Include(
               "~/Scripts/globalize.js",
               "~/Scripts/globalize/currency.js",
                "~/Scripts/globalize/date.js",
                "~/Scripts/globalize/message.js",
                "~/Scripts/globalize/number.js",
                "~/Scripts/globalize/plural.js",
                "~/Scripts/globalize/relative-time.js"));

  bundles.Add(new ScriptBundle("~/bundles/globalisationEN").Include(
               "~/Scripts/GlobalisationCulture/globalize.culture.en-AU.js"));

            bundles.Add(new ScriptBundle("~/bundles/globalisationES").Include(
               "~/Scripts/GlobalisationCulture/globalize.culture.es-AR.js"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryuiEN").Include(
                        "~/Scripts/jquery-ui-1.10.3.js"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryuiES").Include(
                        "~/Scripts/jquery-ui-1.10.3.js"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                "~/Scripts/jquery.validate.js",
                "~/Scripts/jquery.validate.unobtrusive.js",
                "~/Scripts/jquery.unobtrusive-ajax.js",
                "~/Scripts/jquery.validate.globalize.js"));
}

In the layout page I have implemented as follows

HttpCookie cookie = HttpContext.Current.Request.Cookies.Get("CurrentCulture");
        string culutureCode = cookie != null && !string.IsNullOrEmpty(cookie.Value) ? cookie.Value : "en";
        if (culutureCode.Equals("en-AU", StringComparison.OrdinalIgnoreCase))
        {
            culutureCode = "EN";
        }
        else if (culutureCode.Equals("es-AR", StringComparison.OrdinalIgnoreCase))
        {
            culutureCode = "ES";
        }
        else
        {
            culutureCode = "EN";
        }
@Scripts.Render("~/bundles/jquery",
                    "~/bundles/globalisation",
                    string.Format("~/bundles/globalisation{0}", culutureCode),
                    "~/bundles/jqueryval",
                    string.Format("~/bundles/jqueryui{0}", culutureCode))
like image 673
Jayaraj.K Avatar asked Sep 15 '15 12:09

Jayaraj.K


2 Answers

There are 2 jQuery Globalize plugins.

The old version is v0.0.1 contains one script globalize.js and it has a subfolder cultures where you can find all the script cultures such as:

  • globalize.culture.en-AU.js
  • globalize.culture.es-AR.js

those scripts allow you to add as many cultures as you want so it would be perfectly fine to have your bundle built this way:

bundles.Add(new ScriptBundle("~/bundles/globalisation").Include(
    "~/Scripts/globalize.js",
    "~/Scripts/cultures/globalize.culture.en-AU.js",
    "~/Scripts/cultures/globalize.culture.es-AR.js"
));

Globalize will have a collection of localization scripts which you can set simply using:

Globalize.culture('en-AU');

or

Globalize.culture('es-AR');

It can use some sort of proximity to figure out which is the closest culture you want to use. If you have loaded in your bundle globalize.culture.es-AR.js you can set Globalize.culture('es'); and Globalize would be able to figure out that you want to use the 'es-AR' culture; of course if you have added globalize.culture.es.js the loader would chose this last one.

The new version of jQuery Globalize (stable) is v1.0.0 and it works in a completely different way.

It still have the main script file called globalize.js but you have to add a lot more scripts to make it work.

Someone has built a tool which tells you exactly what script you need, depending on what type of module (number, dates, currencies) you want to use.

If you're opting to use v1.0.0 you will see that the tool will suggest to include the base scripts (numbers only):

  • cldr.js
  • cldr/event.js
  • cldr/supplemental.js
  • globalize.js
  • globalize/number.js

plus some CLDR JSON scripts:

  • cldr/supplemental/likelySubtags.json
  • cldr/main/{locale}/numbers.json
  • cldr/supplemental/numberingSystems.json

You can find these files in the core package and the numbers package.
If you want to validate dates this is the package. More info here.

These are all json file and you cannot bundle them. You can loaded them in run-time doing something like this:

Application.loadCulture = function (culture) {

    $.when(
      $.get(Application.CldrFetch + '/' + culture + '/' + encodeURIComponent("likelySubtags.json")),
      $.get(Application.CldrFetch + '/' + culture + '/' + "numberingSystems.json"),
      $.get(Application.CldrFetch + '/' + culture + '/' + "plurals.json"),
      $.get(Application.CldrFetch + '/' + culture + '/' + "ordinals.json"),
      $.get(Application.CldrFetch + '/' + culture + '/' + "currencyData.json"),
      $.get(Application.CldrFetch + '/' + culture + '/' + "timeData.json"),
      $.get(Application.CldrFetch + '/' + culture + '/' + "weekData.json"),
      $.get(Application.CldrFetch + '/' + culture + '/' + "ca-gregorian.json"),
      $.get(Application.CldrFetch + '/' + culture + '/' + "timeZoneNames.json"),
      $.get(Application.CldrFetch + '/' + culture + '/' + "numbers.json"),
      $.get(Application.CldrFetch + '/' + culture + '/' + "currencies.json")
    )
    .then(function () {
        // Normalize $.get results, we only need the JSON, not the request statuses.
        return [].slice.apply(arguments, [0]).map(function (result) {
            return result[0];
        });

    }).then(Globalize.load).then(function () {
        Globalize.locale(culture);
    });
};

Anyway; let's say you want to stick to the old v0.0.1 which is still the best.
Your bundle will have the globalize script and the culture ones:

bundles.Add(new ScriptBundle("~/bundles/globalisation").Include(
    "~/Scripts/globalize.js",
    "~/Scripts/cultures/globalize.culture.en-AU.js",
    "~/Scripts/cultures/globalize.culture.es-AR.js"
));

jQuery validation offers some other additional extension which you might want to consider:

  • additional-methods.js
  • localization/messages_es_AR.js (error messages for the culture)

I have seen that you're setting your culture in the Application_AcquireRequestState. Someone suggest it's better to do it in Application_BeginRequest as it is processed earlier in the pipe:

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        HttpCookie cookie = HttpContext.Current.Request.Cookies.Get("CurrentCulture");
        string cultureCode = cookie != null && !string.IsNullOrEmpty(cookie.Value) ? cookie.Value : "en";
        CultureInfo ci = new CultureInfo(cultureCode);
        System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureCode);
        System.Threading.Thread.CurrentThread.CurrentUICulture = System.Threading.Thread.CurrentThread.CurrentCulture;
    }

It seems that you're using this jQuery plugin for the validation. What I normally would do is, as soon as I load the script, configure the culture and set the custom validation:

    Globalize.culture(this.culture);

    $.validator.methods.number = function (value, element) {
        return this.optional(element) || jQuery.isNumeric(Globalize.parseFloat(value));
    };

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

    jQuery.extend(jQuery.validator.methods, {
        range: function (value, element, param) {
            var val = Globalize.parseFloat(value);
            return this.optional(element) || (val >= param[0] && val <= param[1]);
        }
    });

One thing that you're missing is a model binder for decimals:

using System;
using System.Web.Mvc;
using System.Globalization;

public class DecimalModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        ModelState modelState = new ModelState { Value = valueResult };
        object actualValue = null;
        try
        {
            //Check if this is a nullable decimal and a null or empty string has been passed
            var isNullableAndNull = (bindingContext.ModelMetadata.IsNullableValueType && string.IsNullOrEmpty(valueResult.AttemptedValue));

            //If not nullable and null then we should try and parse the decimal
            if (!isNullableAndNull)
            {
                actualValue = decimal.Parse(valueResult.AttemptedValue, NumberStyles.Any, CultureInfo.CurrentCulture);
            }
        }
        catch (FormatException e)
        {
            modelState.Errors.Add(e);
        }

        bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
        return actualValue;
    }
}

which can be set in your Global.asax Application_Start:

ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());

This is pretty much everything you need.

There's only an annoying problem with this approach.
Let's say you're using the culture en-AU and you input in your numeric field a value: 10,4. This number is perfectly valid in es-AR but it should be non-valid for the en-AU culture.

jQuery Globalize will consider it valid anyway as it would traslate it to 104 here:

$.validator.methods.number = function (value, element) {
    return this.optional(element) || jQuery.isNumeric(Globalize.parseFloat(value));
};

Globalize.parseFloat('10,4') for the culture en-AU would transform that number to 104.

Same thing would happen if you do the same for Globalize.parseFloat('10.4') for the culture es-AR; it would become, again, 104.

You can check this behaviour running this fiddle.

Both , and . are valid symbols as they would be used as decimal separator and thousands separator.

There are a few issues open on this topic on github and I guess it would be hard to fix since they are now working on the new version, where the same problem persists, by the way.

You're going to face the same problem on the server-side with our decimal model binder:

decimal.Parse('10,4', NumberStyles.Any, CultureInfo.CurrentCulture);

where the CultureInfo.CurrentCulture is 'en-AU' would, again, produce the same result: 104.

It can place a breakpoint there and see how it converts the value.

I guess probably this would be easier to fix, maybe using some regular expressions.

If you want to play with the solution with jQuery Validator v.0.1.1 or jQuery Validator v.1.0.0 I've created two repositories here and here.

like image 116
LeftyX Avatar answered Oct 26 '22 23:10

LeftyX


You have added bundles in RegisterBundles but didn't use them in layout page. You also added redundant jqueryui file in RegisterBundles. Update your RegisterBundles method like this:

public static void RegisterBundles(BundleCollection bundles)
 {
   BundleTable.EnableOptimizations = true;
   bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                    "~/Scripts/jquery-{version}.js"));
   bundles.Add(new ScriptBundle("~/bundles/globalisation").Include(
            "~/Scripts/globalize.js",                
            "~/Scripts/globalize/currency.js",
            "~/Scripts/globalize/date.js",
            "~/Scripts/globalize/message.js",
            "~/Scripts/globalize/number.js",
            "~/Scripts/globalize/plural.js",
            "~/Scripts/globalize/relative-time.js"));
   bundles.Add(new ScriptBundle("~/bundles/globalisationEN").Include(
           "~/Scripts/GlobalisationCulture/globalize.culture.en-AU.js"));
   bundles.Add(new ScriptBundle("~/bundles/globalisationES").Include(
           "~/Scripts/GlobalisationCulture/globalize.culture.es-AR.js"));
   bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include(
                    "~/Scripts/jquery-ui-1.10.3.js"));      

   bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
            "~/Scripts/jquery.validate.js",
            "~/Scripts/jquery.validate.unobtrusive.js",
            "~/Scripts/jquery.unobtrusive-ajax.js",
            "~/Scripts/jquery.validate.globalize.js"));
  }

and then update layout page like this :

@section Scripts 
{
    @Scripts.Render("~/bundles/jquery",
                "~/bundles/globalisation",
                "~/bundles/globalisationEN",
                "~/bundles/globalisationES",
                "~/bundles/jqueryval",
                "~/bundles/jqueryui"))

   <script type="text/javascript">
    $.validator.methods.number = function (value, element) {
        return this.optional(element) ||
            !isNaN(Globalize.parseFloat(value));
    }
    $(document).ready(function () {
        Globalize.culture('es-AR'); //set spanish culture
    });

   </script>
}

Hope this will help :)

like image 23
Mahbubur Rahman Avatar answered Oct 26 '22 23:10

Mahbubur Rahman