Chrome and localising the date values in Typescript

I was recently was tasked with a seemingly simple task on one of the projects I was assigned to: Make dates in the application's front end "Region aware". Region aware dates are dates that conform to your computer's settings. For example:
If I had my region set to United States it would format the date like this: mm/dd/yyyy.
If I had my region set to United Kingdom it would format the date like this: dd/mm/yyyy.

On the face of it I thought how hard could something like this be. It turned out to be a lot more than I had thought. We had to set date formats based on user settings and not a specific format set by the application. This was because the application would not just be used in a single country. The application is built using .NET WebApi and Typescript/Knockout.This meant we had to handle date formatting in two places: Backend and the front end.

Formatting the date in the WebApi was not very hard as I could get the user locale settings from the Accept-Language request header:

This request header property is sent by default by all browsers and forms part of the HTTP standard. The value of the request header property can be passed directly into the CultureInfo class and set as a culture.
I wrote a very neat DateTimeExtension class in the WebApi which made date formatting in the WebApi a breeze:
`

public static class DateTimeExtension
{
    public static string ToLocaleDateString(this DateTime datetime)
    {
        List<string> userLanguages = new List<string>();
        if (HttpContext.Current != null)
        {
            userLanguages = HttpContext.Current.Request.UserLanguages.ToList();
        }
        userLanguages = userLanguages.Where(s => s != "en").ToList();
        CultureInfo ci;
        if (userLanguages.Any())
        {
            try
            {
                ci = new CultureInfo(userLanguages[0]);
            }
            catch (CultureNotFoundException)
            {
                ci = new CultureInfo("en-gb");
            }
        }
        else
        {
            ci = new CultureInfo("en-gb");
        }
        return datetime.ToString(ci.DateTimeFormat.ShortDatePattern);
    }
}`

I had to cater for the case that the language trying to be set was not installed on the server.I fell back to the Great Britain CultureInfo, dates should never be written DD/MM/YYYY.

To invoke my new extension method was a simple call on any datetime object:
period.StartDate.ToLocaleDateString()

Next task was doing the same thing in Typescript. There was a method called .toLocaleString() and other variations that took in a particular language. This seemed to solve my problem.
I modified the existing Date Binding handler that had been previously been created and manipulated it to call the "tolocaleString()' method. I had a few teething issues:

  1. I needed to get the particular language that the browser sent in the request header to ensure that the backend and the frontend were using the same language. Each browser implements this is in a different way:
    //get request header languages for the various browsers var nav: any = navigator; var languages = nav["languages"]// chrome; var ffLanguage = nav["language"]//firefox && Safari; var userLanguage = nav["userLanguage"] IE;
    All four major browsers have different properties, you can't do anything but laugh.

  2. Input fields do not want to accept dates as values unless they are set in the format: YYYY-MM-DD. This meant I could only do the region setting after setting the value.

It all seemed to be working well until I changed my region settings,it seems that the HTML input elements that were of type="date" were displaying a different format to the rest of the html elements. The input elements were right and the rest were not changing.

I then navigated to Chrome's settings to set the language there. It seemed it had set its own language there and was not changing when the device settings were changed. . It was using this setting to set the Accept Language Request header and format the rest of the HTML elements except the input fields.
I researched if there was a way to force the input element to show using a specific date format and it turned out that there was noway to specify the format of the input field and it followed the device setting.

It seems asinine to me, Chrome displays all the dates in the system using its own application set language however when it comes to input fields it uses the device region settings. You needed to have the Chrome's settings and your device region settings to be same. There is no way to make sure that all the dates are the same if you want to do it with region.

This left us with two options:

  1. Educate the users on how to sync the settings.
  2. When signing up the user needs to select the preferred region and formats.

The application is not a public facing one and the user base will not large one. We therefore opted for option 1. I still believe this should be fixed and is actually a bug.
It took me awhile to get the ticket through Quality Assurance due to the issues raised above, thanks Google, but we do now have region aware dates in the application.

I have included the code for the binding handler below:

    ko.bindingHandlers.tzDateValue = 
    {
init: (element: any, valueAccessor: () => any, allBindingsAccessor: () => any, viewModel: any, bindingContext: KnockoutBindingContext) => {
         if (valueAccessor()) 
         {
            var observable = valueAccessor();
          if (ko.isObservable(observable)) {
            var currentValueMoment = moment(ko.utils.unwrapObservable(observable));
            observable(currentValueMoment.format());
            }
         } ko.utils.registerEventHandler(element, "blur", (a) => {
        var observable = valueAccessor();
        if (!observable) return;

        var currentValue = ko.utils.unwrapObservable(observable);
        var $element = $(element);
        var elementVal: string = $element.val();

        if (elementVal === "" || currentValue === "") {
            observable("1900-01-01T00:00");
            $element.val("1900-01-01T00:00");
            return;
        }

        var slashCount = (elementVal.match(/\//g) || []).length; //for capturing like 13/02/2014
        if (!tz.browserInfo.supportsDateInput()) {
            if (elementVal.length < 10) //await a fully qualified date string before processing
                return;
            else if (slashCount > 0 && slashCount < 2)
                return; //we're busy filling in a date, give us time. moment will try and interpret this too early.
        }

        var elementMoment = moment(elementVal);
        var elementMomentFormatted = elementMoment.format("YYYY-MM-DD");
        if (elementMomentFormatted.toLowerCase() != "invalid date") {
            if (!tz.browserInfo.supportsDateInput() && elementMoment.year() < 1000) //don't process really small dates (less than 1000)
                return;

            var datePortion = elementMomentFormatted; //get the date portion from the date control and leave the time portion alone.
            observable(datePortion + "T00:00");

            if (!tz.browserInfo.supportsDateInput()) {
                tz.notify.success("Captured: " + $(element).val());
            }
        }
    });
},
update: (element: any, valueAccessor: () => any, allBindingsAccessor: () => any, viewModel: any, bindingContext: KnockoutBindingContext) => {
    if (element.validity != null && !element.validity.valid) return;
    var observable = valueAccessor();
    var valueUnwrapped: string = <string>ko.utils.unwrapObservable(valueAccessor());
    if (valueUnwrapped === "") {
        observable("1900-01-01T00:00");
        return;
    }
    var pattern;
    if (tz.browserInfo.supportsDateInput())
        pattern = "YYYY-MM-DD";
    else
        pattern = allBindingsAccessor().datePattern || "DD MMM YYYY"; //" YYYY-MM-DD";
    var momentDate: Moment = moment(valueUnwrapped);
    if (momentDate.year() < 1000) return;
    if (!momentDate.isValid() || momentDate.diff(tz.helper.defaultDate) < 0) //if date is invalid or less than 1 Jan 1900, make it 1 Jan 1900
        momentDate = tz.helper.defaultDate;

    var valueFormatted = momentDate.format(pattern);

    var blankValue = false;

    if (momentDate.diff(tz.helper.defaultDate) === 0) {
        blankValue = true;
        if (moment(observable()).diff(tz.helper.defaultDate, "days") !== 0) //only update the observable if necessary
            observable("1900-01-01T00:00");
    }//handle non input date fields
    if (element.type !== "date") {

        //get request header languages for the various browsers
        var nav: any = navigator;
        var languages = nav["languages"];
        var ffLanguage = nav["language"];
        var userLanguage = nav["userLanguage"];


        var language = languages ? languages[0] : (ffLanguage.language || userLanguage);

        //pass the specific language in 
        valueFormatted = momentDate.toDate().toLocaleDateString(language);
    }
    var value = blankValue ? "" : valueFormatted;

    if (element.nodeName.toLowerCase() == "input") {

        $(element).val(value);
    } else {
        element.innerText = value;
    }

  }
};

Richard Pilkington

I am a .NET Developer who is always seeking out new things and loves trying anything new. There has to be a quicker way tends to be the way I approach things

Cape Town, South Africa