Localization of Dates in ASP.Net MVC

Introduction

It is a somewhat inconvenient truth that users of your web site are everywhere but they don’t all talk the same language or even do simple things like read dates in the same way. ASP.Net prov ides rich support for globalization. This post covers the localization of dates, the different treatment ASP.Net applies to dates received in a QueryString to those in posted fields and provides some techniques to mitigate any issues caused by this different treatment.

ASP.Net treats Dates in URL’s different to POST fields

This came as a surprise to me. When ASP.MVC binds values from POSTed fields, it applies the current culture, but when it binds values from the URL (querystring) is uses the Invariant Culture.

This can result in unpleasant effects, particularly if like me (programming in Australia) want dates in the format day-month-year, and not the invariant month-day-year.

Say you had a form with a input field for dates. If the user entered ‘14/5/2011’ and the form was submitted via GET instead of POST, MVC would complain that this was not a valid date. If you submit the form via POST, MVC would bind the date correctly.

I wrote to Phil Haack about this:

Hi Phil,I noticed this one today. I’m in Australia (en-AU), so it common to use dates of the form dd/mm/yyyy.When these fields are POSTed, the values are as expected, because the ValueProviderResult is being constructed with the CurrentCulture out of the Mvc.FormCollection.But when the string is submitted via GET, the NameValueProvider is creating a ValueProviderResult with the Invariant culture, which is different to the current culture, and it doesn’t believe that 29/4/2011 is a valid date (29 April 2011). Most upsetting !

He replied thus

Yeah. We made a conscious choice that values pulled from the URL would be culture invariant. The reason is that URLs must be uniform. So if I send you the URL, it shouldn’t change its meaning whether the person clicking it is in AU or in US.

I can’t say I’m convinced as to his reasoning. It kind of assumes that URL’s are static and only accessed by ‘clicking’. But a form submitted via GET is a totally different interaction to a mere click. This logic more or less requires unless the server current culture somewhat reflects the invariant culture (ie you’re in the US) you should POST your forms. Consider the following simple page:

@model TestDates.Models.HomeModel
<html>
<head>
    
    <link rel="stylesheet" type="text/css" href="@Url.Content(" content ~ site.css?)?>
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>


@using (Html.BeginForm("Index", "Home", FormMethod.Get))
{ 
    @Html.ValidationSummary(true)
    @Html.LabelFor(m =&gt; m.SubmissionDate);    
    @Html.TextBoxFor(m =&gt; m.SubmissionDate, new { type = "date" })
    @Html.ValidationMessageFor(m =&gt; m.SubmissionDate);
    
    <input value="submit" type="submit">
}


Assuming that using a cultre with a date format dd/MM/yyyy, if you fire up this page and enter a date of say ’14/5/2010′, you will get an error from the server:”The value ’14/5/2010′ is not valid for SubmissionDate”. Not ideal!! This is a bit of a restriction but there is a workaround. You need to hook into the form submit event, get the URL the form was going to send, including it’s serialized field information and manually find and replace date fields with their equivalent invariant values. Then manually submit the form and cancel the default event. This is done using the script below:

<script type="text/javascript">
    $('form').submit(function (e) {
        //serialize the form
        var data = $(this).serialize();
	//find the url that the form was going to submit itself to
        var url = $(this).attr('action');

	//now replace every instance of a date with the equivalent ISO-8601 value
        data = data.replace(/(\d{1,2})%2F(\d{1,2})%2F(\d{4})/g, "$3%2F$2%2F$1");

	//append the url and change the window location
        if (url.match(/\?/)) {
            window.location.href = url + '&amp;' + data;
        } else {
            window.location.href = url + '?' + data;
        }

	//prevent the default action
        e.preventDefault();
    });
</script>

If you run this and submit, then the date will be parsed correctly. However a problem exists – when the date is displayed to the user, it is in the format posted to the server, so although the user typed dd/MM/yyyy, MVC saw yyyy/MM/dd in the query string and this is the format returned to the user. To get around this, you need to ensure that the users local or preferred culture is automatically set from the user’s browser by setting the following web.config element:

 <system.web>
    <globalization responseEncoding="UTF-8" 
                       requestEncoding="UTF-8" 
                       culture="auto" 
                       uiCulture ="auto" />
	.....
 </system.web>

This will result in output to the browser of, for example, dd/MM/yyyy for en-AU, MM/dd/yyyy for en-US and YYYY/MM/dd for ja-JP.

The problem still remains in that I want the date to come back to the browser in an invariant manner, so I’m going to modify my submit script to take into the current culture and always submit the date in yyyy/mm/dd format – ISO-8601 format. But because the relative positions of date, months and years can change between cultures, I need to work out the positions before applying the respective replacement regex. To do this, I create a test date and format it to a string in the current UI Culture and test what the first element is. That tells me whether the date or month or year comes first. I can then render the appropriate regex accordingly.So instead of:


data = data.replace(/(\d{1,2})%2F(\d{1,2})%2F(\d{4})/g, "$3%2F$2%2F$1");

I need to use some Razor C# when rendering the script block as follows:


....//following on previous script
@{        
  //create a known date 
  string testDate = new DateTime(2000, 10, 30).ToString("d");

  //replace the known values for years, months and days 
  //with the replacement regex expressions
  string dateRegex = testDate.Replace("2000", @"(\d{4})")
     .Replace("10", @"(\d{1,2})").Replace("30", @"(\d{1,2})").Replace("/", "%2F");

  //see if the produced date starts with the date component
  if(testDate.StartsWith("30")){
    @:data = data.replace(/@dateRegex/g, "$3%2F$2%2F$1");                           
  }
  //see if it starts with the month component
  else if(testDate.StartsWith("10")){
    @:data = data.replace(/@dateRegex/g, "$3%2F$1%2F$2");
  }  
  //it starts with the year component so no work to be done
}

....//continuing on with the script

Obviously you can extend the logic to test for date culture formats such as yyyy/dd/MM or dd/yyyy/MM (if such cultures exist) but I hope you get the point.

Localization and jQuery datepicker

It is worth noting how to switch the jquery datepicker localization. It’s simply a matter of dynamically setting the localization js as follows:

<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <link href="../../Content/themes/base/jquery.ui.all.css" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
    <script src="../../Scripts/jquery-ui-1.8.11-min.js" type="text/javascript"></script>
    
    <!-- set the localization file based on the current culture -->
    <script src="../../Scripts/jquery.ui.datepicker-@(UICulture).js" type="text/javascript"></script>
</head>

Conclusion

This post has demonstrated the vagaries of localizing dates using ASP.Net MVC, and presented a few methods to provide a consistent date experience to the user, regardless of their culture.

Advertisements
This entry was posted in ASP.Net MVC, jQuery, Razor and tagged , , , . Bookmark the permalink.

One Response to Localization of Dates in ASP.Net MVC

  1. Thank you for taking the time to write this, it helped a lot when debugging this strange behavior.

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