How to: Display Localized Date and Time Information to Web Users

Because a Web page can be displayed anywhere in the world, operations that parse and format date and time values should not rely on a default format (which most often is the format of the Web server's local culture) when interacting with the user. Instead, Web forms that handle date and time strings input by the user should parse the strings using the user's preferred culture. Similarly, date and time data should be displayed to the user in a format that conforms to the user's culture. This topic shows how to do this.

To parse date and time strings input by the user

  1. Determine whether the string array returned by the HttpRequest.UserLanguages property is populated. If it is not, continue to step 6.

  2. If the string array returned by the UserLanguages property is populated, retrieve its first element. The first element indicates the user's default or preferred language and region.

  3. Instantiate a CultureInfo object that represents the user's preferred culture by calling the CultureInfo.CultureInfo(String, Boolean) constructor.

  4. Call either the TryParse or the Parse method of the DateTime or DateTimeOffset type to try the conversion. Use an overload of the TryParse or the Parse method with a provider parameter, and pass it either of the following:

  5. If the conversion fails, repeat steps 2 through 4 for each remaining element in the string array returned by the UserLanguages property.

  6. If the conversion still fails or if the string array returned by the UserLanguages property is empty, parse the string by using the invariant culture, which is returned by the CultureInfo.InvariantCulture property.

To parse the local date and time of the user's request

  1. Add a HiddenField control to a Web form.

  2. Create a JavaScript function that handles the onClick event of a Submit button by writing the current date and time and the local time zone's offset from Coordinated Universal Time (UTC) to the Value property. Use a delimiter (such as a semicolon) to separate the two components of the string.

  3. Use the Web form's PreRender event to inject the function into the HTML output stream by passing the text of the script to the ClientScriptManager.RegisterClientScriptBlock(Type, String, String, Boolean) method.

  4. Connect the event handler to the Submit button's onClick event by providing the name of the JavaScript function to the OnClientClick attribute of the Submit button.

  5. Create a handler for the Submit button's Click event.

  6. In the event handler, determine whether the string array returned by the HttpRequest.UserLanguages property is populated. If it is not, continue to step 14.

  7. If the string array returned by the UserLanguages property is populated, retrieve its first element. The first element indicates the user's default or preferred language and region.

  8. Instantiate a CultureInfo object that represents the user's preferred culture by calling the CultureInfo.CultureInfo(String, Boolean) constructor.

  9. Pass the string assigned to the Value property to the Split method to store the string representation of the user's local date and time and the string representation of the user's local time zone offset in separate array elements.

  10. Call either the DateTime.Parse or DateTime.TryParse(String, IFormatProvider, DateTimeStyles, DateTime%) method to convert the date and time of the user's request to a DateTime value. Use an overload of the method with a provider parameter, and pass it either of the following:

  11. If the parse operation in step 10 fails, go to step 13. Otherwise, call the UInt32.Parse(String) method to convert the string representation of the user's time zone offset to an integer.

  12. Instantiate a DateTimeOffset that represents the user's local time by calling the DateTimeOffset.DateTimeOffset(DateTime, TimeSpan) constructor.

  13. If the conversion in step 10 fails, repeat steps 7 through 12 for each remaining element in the string array returned by the UserLanguages property.

  14. If the conversion still fails or if the string array returned by the UserLanguages property is empty, parse the string by using the invariant culture, which is returned by the CultureInfo.InvariantCulture property. Then repeat steps 7 through 12.

The result is a DateTimeOffset object that represents the local time of the user of your Web page. You can then determine the equivalent UTC by calling the ToUniversalTime method. You can also determine the equivalent date and time on your Web server by calling the TimeZoneInfo.ConvertTime(DateTimeOffset, TimeZoneInfo) method and passing a value of TimeZoneInfo.Local as the time zone to convert the time to.

Example

The following example contains both the HTML source and the code for an ASP.NET Web form that asks the user to input a date and time value. A client-side script also writes information on the local date and time of the user's request and the offset of the user's time zone from UTC to a hidden field. This information is then parsed by the server, which returns a Web page that displays the user's input. It also displays the date and time of the user's request using the user's local time, the time on the server, and UTC.

<%@ Page Language="VB" %>
<%@ Import  Namespace="System.Globalization" %> 
<%@ Assembly Name="System.Core" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    Protected Sub OKButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles OKButton.Click
        Dim locale As String = "" 
        Dim styles As DateTimeStyles = DateTimeStyles.AllowInnerWhite Or DateTimeStyles.AllowLeadingWhite Or _
                                       DateTimeStyles.AllowTrailingWhite
        Dim inputDate, localDate As Date 
        Dim localDateOffset As DateTimeOffset
        Dim integerOffset As Integer 
        Dim result As Boolean 

        ' Exit if input is absent. 
        If String.IsNullOrEmpty(Me.DateString.Text) Then Exit Sub 

        ' Hide form elements. 
        Me.DateForm.Visible = False 

        ' Create array of CultureInfo objects 
        Dim cultures(Request.UserLanguages.Length) As CultureInfo
        For ctr As Integer = Request.UserLanguages.GetLowerBound(0) To Request.UserLanguages.GetUpperBound(0)
            locale = Request.UserLanguages(ctr)
            If Not String.IsNullOrEmpty(locale) Then 
                ' Remove quality specifier, if present. 
                If locale.Contains(";") Then _
                   locale = Left(locale, InStr(locale, ";") - 1)
                Try
                   cultures(ctr) = New CultureInfo(Request.UserLanguages(ctr), False)
                Catch 
                End Try 
            Else
                cultures(ctr) = CultureInfo.CurrentCulture
            End If 
        Next
        cultures(Request.UserLanguages.Length) = CultureInfo.InvariantCulture
        ' Parse input using each culture. 
        For Each culture As CultureInfo In cultures
            result = Date.TryParse(Me.DateString.Text, culture.DateTimeFormat, styles, inputDate)
            If result Then Exit For 
        Next 
        ' Display result to user. 
        If result Then
            Response.Write("<P />")
            Response.Write("The date you input was " + Server.HtmlEncode(CStr(Me.DateString.Text)) + "<BR />")
        Else 
            ' Unhide form. 
            Me.DateForm.Visible = True
            Response.Write("<P />")
            Response.Write("Unable to recognize " + Server.HtmlEncode(Me.DateString.Text) + ".<BR />")
        End If 

        ' Get date and time information from hidden field. 
        Dim dates() As String = Request.Form.Item("DateInfo").Split(";")

        ' Parse local date using each culture. 
        For Each culture As CultureInfo In cultures
            result = Date.TryParse(dates(0), culture.DateTimeFormat, styles, localDate)
            If result Then Exit For 
        Next 
        ' Parse offset 
        result = Integer.TryParse(dates(1), integerOffset)
        ' Instantiate DateTimeOffset object representing user's local time 
        If result Then 
            Try
                localDateOffset = New DateTimeOffset(localDate, New TimeSpan(0, -integerOffset, 0))
            Catch ex As Exception
                result = False 
            End Try 
        End If 
        ' Display result to user. 
        If result Then
            Response.Write("<P />")
            Response.Write("Your local date and time is " + localDateOffset.ToString() + ".<BR />")
            Response.Write("The date and time on the server is " & _
                           TimeZoneInfo.ConvertTime(localDateOffset, _
                                                    TimeZoneInfo.Local).ToString() & ".<BR />")
            Response.Write("Coordinated Universal Time is " + localDateOffset.ToUniversalTime.ToString() + ".<BR />")
        Else
            Response.Write("<P />")
            Response.Write("Unable to recognize " + Server.HtmlEncode(dates(0)) & ".<BR />")
        End If 
    End Sub 

    Protected Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.PreRender
        Dim script As String = "function AddDateInformation() { " & vbCrLf & _
                  "var today = new Date();" & vbCrLf & _
                  "document.DateForm.DateInfo.value = today.toLocaleString() + " & Chr(34) & Chr(59) & Chr(34) & " + today.getTimezoneOffset();" & vbCrLf & _
                  " }" 
        ' Register client script 
        Dim scriptMgr As ClientScriptManager = Page.ClientScript
        scriptMgr.RegisterClientScriptBlock(Me.GetType(), "SubmitOnClick", script, True)
    End Sub
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Parsing a Date and Time Value</title>
</head>
<body>
    <form id="DateForm" runat="server">
    <div>
    <center>
       <asp:Label ID="Label1" runat="server" Text="Enter a Date and Time:" Width="248px"></asp:Label>
       <asp:TextBox ID="DateString" runat="server" Width="176px"></asp:TextBox><br />
    </center>       
    <br />
    <center>
    <asp:Button ID="OKButton" runat="server" Text="Button"  
            OnClientClick="AddDateInformation()" onclick="OKButton_Click" />
    <asp:HiddenField ID="DateInfo"  Value=""  runat="server" />
    </center>
    <br />
    </div>
    </form>
</body>
</html>
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Globalization" %>
<%@ Assembly Name="System.Core" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void OKButton_Click(object sender, EventArgs e)
    {
        string locale = "";
        DateTimeStyles styles = DateTimeStyles.AllowInnerWhite | DateTimeStyles.AllowLeadingWhite |
                                       DateTimeStyles.AllowTrailingWhite;
        DateTime inputDate;
        DateTime localDate = DateTime.Now;
        DateTimeOffset localDateOffset = DateTimeOffset.Now;
        int integerOffset;
        bool result = false;

        // Exit if input is absent. 
        if (string.IsNullOrEmpty(this.DateString.Text)) return;

        // Hide form elements. 
        this.DateForm.Visible = false;

        // Create array of CultureInfo objects
        CultureInfo[] cultures = new CultureInfo[Request.UserLanguages.Length + 1];
        for (int ctr = Request.UserLanguages.GetLowerBound(0); ctr <= Request.UserLanguages.GetUpperBound(0);
             ctr+)
        {
            locale = Request.UserLanguages[ctr];
            if (! string.IsNullOrEmpty(locale))
            {

                // Remove quality specifier, if present. 
                if (locale.Contains(";"))
                   locale = locale.Substring(locale.IndexOf(';') -1);
                try
                {
                    cultures[ctr] = new CultureInfo(Request.UserLanguages[ctr], false);
                }
                catch (Exception) { }
            }
            else
            {
                cultures[ctr] = CultureInfo.CurrentCulture;
            }
        }
        cultures[Request.UserLanguages.Length] = CultureInfo.InvariantCulture;
        // Parse input using each culture. 
        foreach (CultureInfo culture in cultures)
        {
            result = DateTime.TryParse(this.DateString.Text, culture.DateTimeFormat, styles, out inputDate);
            if (result) break;
        }
        // Display result to user. 
        if (result)
        {
            Response.Write("<P />");
            Response.Write("The date you input was " + Server.HtmlEncode(this.DateString.Text) + "<BR />");
        }
        else
        {
            // Unhide form. 
            this.DateForm.Visible = true;
            Response.Write("<P />");
            Response.Write("Unable to recognize " + Server.HtmlEncode(this.DateString.Text) + ".<BR />");
        }

        // Get date and time information from hidden field. 
        string[] dates= Request.Form["DateInfo"].Split(';');

        // Parse local date using each culture. 
        foreach (CultureInfo culture in cultures)
        {
            result = DateTime.TryParse(dates[0], culture.DateTimeFormat, styles, out localDate);
            if (result) break;
        }
        // Parse offset 
        result = int.TryParse(dates[1], out integerOffset);
        // Instantiate DateTimeOffset object representing user's local time 
        if (result) 
        {
            try
            {
                localDateOffset = new DateTimeOffset(localDate, new TimeSpan(0, -integerOffset, 0));
            }
            catch (Exception)
            {
                result = false;
            }
        }
        // Display result to user. 
        if (result)
        {
            Response.Write("<P />");
            Response.Write("Your local date and time is " + localDateOffset.ToString() + ".<BR />");
            Response.Write("The date and time on the server is " + 
                           TimeZoneInfo.ConvertTime(localDateOffset, 
                                                    TimeZoneInfo.Local).ToString() + ".<BR />");
            Response.Write("Coordinated Universal Time is " + localDateOffset.ToUniversalTime().ToString() + ".<BR />");
        }
        else
        {
            Response.Write("<P />");
            Response.Write("Unable to recognize " + Server.HtmlEncode(dates[0]) + ".<BR />");
        }
    }

    protected void Page_PreRender(object sender, System.EventArgs e)
    {
        string script = "function AddDateInformation() { \n" +
                  "var today = new Date();\n" +
                  "document.DateForm.DateInfo.value = today.toLocaleString() + \";\" + today.getTimezoneOffset();\n" +
                  " }";
        // Register client script
        ClientScriptManager scriptMgr = Page.ClientScript;
        scriptMgr.RegisterClientScriptBlock(this.GetType(), "SubmitOnClick", script, true);
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Parsing a Date and Time Value</title>
</head>
<body>
    <form id="DateForm" runat="server">
    <div>
    <center>
       <asp:Label ID="Label1" runat="server" Text="Enter a Date and Time:" Width="248px"></asp:Label>
       <asp:TextBox ID="DateString" runat="server" Width="176px"></asp:TextBox><br />
    </center>       
    <br />
    <center>
    <asp:Button ID="OKButton" runat="server" Text="Button"  
            OnClientClick="AddDateInformation()" onclick="OKButton_Click" />
    <asp:HiddenField ID="DateInfo"  Value=""  runat="server" />
    </center>
    <br />
    </div>
    </form>
</body>
</html>

The client-side script calls the JavaScript toLocaleString method. This produces a string that follows the formatting conventions of the user's locale, which is more likely to be successfully parsed on the server.

The HttpRequest.UserLanguages property is populated from the culture names that are contained in Accept-Language headers included in an HTTP request. However, not all browsers include Accept-Language headers in their requests, and users can also suppress the headers completely. This makes it important to have a fallback culture when parsing user input. Typically the fallback culture is the invariant culture returned by CultureInfo.InvariantCulture. Users can also provide Internet Explorer with culture names that they input in a text box, which creates the possibility that the culture names may not be valid. This makes it important to use exception handling when instantiating a CultureInfo object.

When retrieved from an HTTP request submitted by Internet Explorer, the HttpRequest.UserLanguages array is populated in order of user preference. The first element in the array contains the name of the user's primary culture/region. If the array contains any additional items, Internet Explorer arbitrarily assigns them a quality specifier, which is delimited from the culture name by a semicolon. For example, an entry for the fr-FR culture might take the form fr-FR;q=0.7.

The example calls the CultureInfo constructor with its useUserOverride parameter set to false to create a new CultureInfo object. This ensures that, if the culture name is the default culture name on the server, the new CultureInfo object created by the class constructor contains a culture's default settings and does not reflect any settings overridden by using the server's Regional and Language Options application. The values from any overridden settings on the server are unlikely to exist on the user's system or to be reflected in the user's input.

Because this example parses two string representations of a date and time (one input by the user, the other stored to the hidden field), it defines the possible CultureInfo objects that may be required in advance. It creates an array of CultureInfo objects that is one greater than the number of elements returned by the HttpRequest.UserLanguages property. It then instantiates a CultureInfo object for each language/region string, and also instantiates a CultureInfo object that represents CultureInfo.InvariantCulture.

Your code can call either the Parse or the TryParse method to convert the user's string representation of a date and time to a DateTime value. Repeated calls to a parse method may be required for a single parsing operation. As a result, the TryParse method is better because it returns false if a parse operation fails. In contrast, handling the repeated exceptions that may be thrown by the Parse method can be a very expensive proposition in a Web application.

Compiling the Code

To compile the code, create an ASP.NET Web page without a code-behind. Then copy the example into the Web page so that it replaces all the existing code. The ASP.NET Web page should contain the following controls:

  • A Label control, which is not referenced in code. Set its Text property to "Enter a Number:".

  • A TextBox control named DateString.

  • A Button control named OKButton. Set its Text property to "OK".

  • A HiddenField control named DateInfo.

Security

To prevent a user from injecting script into the HTML stream, user input should never be directly echoed back in the server response. Instead, it should be encoded by using the HttpServerUtility.HtmlEncode method.

See Also

Concepts

Formatting How-to Topics

Date and Time Format Strings

Parsing Date and Time Strings