Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Globalized Web Applications & ASP.NET

By Max Poliashenko and Chip Andrews

, April 01, 2002


Apr02: Globalized Web Applications & ASP.NET

Max is a software architect and can be contacted at [email protected]. Chip is a software security architect and can be reached at [email protected]. Both authors are members of the research team at Clarus Corp.


Localization is the process of adapting an application to the conventions of a particular locale. Globalization, on the other hand, is the process of enabling localization in the single code base of an application. However, globalization generally means more than just enabling string translation — true localization includes displaying numbers, currencies, dates, calendars, direction of the text layout, images, and icons in a format specific to a given locale.

Unfortunately, globalization often comes as an afterthought, requiring extra time and effort to localize to a particular end-user's language and conventions. Fortunately, .NET provides an extensible framework to achieve all levels of globalization from the outset. In the .NET world, a locale is called "culture" and is the primary means for determining how information is presented to end users. Once a user's culture is determined, .NET can properly render the user interface via features such as:

  • .NET assemblies, which contain culture information in their metadata. They can also use satellite assemblies including resources for different cultures.
  • System.Globalization namespace, which contains classes that encapsulate local formatting settings, local calendars, and other localization aspects.

  • .NET threads. The CurrentCulture property affects how date-time, number, and currency data are formatted to strings, while CurrentUICulture is used by the Windows UI to determine proper localized resources.

  • Unicode. ASP.NET internally uses Unicode, although it is possible to set a different encoding, which is used for HTTP request and response. Default encoding may be specified both at a web site level (in web.config file) and page level.

  • Default culture, which can be set for a web site declaratively in web.config file, or at the page level via special tags.

The ASP Way

Several globalization techniques exist with ASP, including server-side script injections into HTML as well as building HTML dynamically with the Response object; see Listings One(a) and (b), respectively. However, such code is difficult to debug, maintain, and extend (imagine adding or omitting a ">" somewhere).

Another ASP feature is that a server-side script is parsed and interpreted every time. Using in-line server-side code injections causes the parser to constantly switch between script and HTML contexts, thereby creating performance problems. Writing custom controls to alter behavior or enhance standard HTML controls is an ordeal and usually involves ActiveX or Java. Additionally, proper formatting of numbers, dates, times, and currencies requires switching locales on the server and employing custom metadata to keep track of additional information (the number of decimal places for a currency and position of the currency sign, for instance). All of these make the implementations either client heavy or nonreusable.

The ASP.NET Way

The ASP.NET framework treats web pages and their individual elements as classes. This allows for utilization of full object-oriented benefits including implementation inheritance, polymorphism, and better encapsulation. The ability to use object-oriented techniques is a real plus when building robust and transparent globalization frameworks.

Using the "code-behind" features and treating a web page as a class lets you automatically derive its behavior from other classes. This, along with a flexible DOM-like model (which exposes web page elements as objects on the server side), allows for complete separation of UI and content. If your web page requires someone other than a programmer to work on the interface, we recommend writing web pages in such a way that the page itself contains nothing but a static HTML-like layout of server-side elements and controls. The code to load page content, authenticate, authorize, control population, and handle events reside in a code-behind class. In this way, your web page can be passed to web designers who can move controls around the page and extend its presentation. The good news is that the code-behind doesn't even have to be recompiled as long as the designer has not deleted any controls or changed the IDs.

Server Controls

In the ASP.NET world, almost every HTML element and control can be represented by an ASP.NET server control. Some of the controls are simple wrappers around HTML elements (HTML server controls), some are sophisticated built-in controls (web form controls), and others are custom built for a particular application (user and custom controls). What is important is that all are classes and, therefore, can derive from other classes and vice versa. These controls know how to persist state between HTTP posts and how to render proper HTML and client-side script at response time. Server controls can also generate certain server-side events.

Custom controls are classes that derive from the System.Web.UI.WebControls.WebControl class or its descendants. The WebControl class defines a virtual function Render(), which is called by the ASP.NETframework when the control renders the HTML output. Custom controls can also be derived from other server controls to specialize their behavior. In this case, they may or may not override Render(), depending on the character of their specialization.

User controls (formerly known as "pagelets") are composite UI elements and usually include several web form or custom controls. They are equivalent to embedded web pages and are stored in their own .ascx files. Because they are completely separate files, their nested controls live in separate namespaces; there is never a name collision between controls in the host and in user control pages. User controls may also have code-behind classes, and server-side events (like Page_Load) fire for them independently from the host page events. Their output can be cached separately.

Because of these features, user controls are usually developed as standalone pages. You can then convert the standalone pages into user controls to encapsulate a chunk of business functionality that can be easily modularized and reused within a web application. They too can be used for globalization purposes, particularly when several controls must be localized as an orchestrated group — as in the case of complex calendars.

ASP.NET Globalization Techniques

Taking time upfront to plan use of ASP.NET's features can save you a lot of pain later. Here, we show how to build an application in such a way that, once completed, it could be globalized with relative ease and little additional effort.

Inheritance can make the globalization process more transparent. Again, all elements on a web page may be represented by server-side controls, which are .NET classes. As classes, their behavior can be passed down through inheritance. Thus, you can derive your own classes from TextBox, Label, DropDownList, and others in the System.Web.UI.Control namespace. These new classes can encapsulate translating corresponding UI elements and can use virtual methods that the creators of ASP.NET thoughtfully defined for the WebControl class, enabling the derived classes to intercept and override rendering HTML code. Listing Two(a) is an example of a GlobalizedLabel class. System.Web.UI.WebControl.Label is a web form control class that displays its Text property as read-only text. Render() is one of its virtual functions that can be overridden by the derived classes. It takes HtmlTextWriter as a parameter and writes into it whatever HTML and client-side script this control wants to render. Our implementation of Render() takes its Text property and translates it using the Translator.GetLocalizedString() method.

The GetLocalizedString() method can only work if the string passed to it can be pretranslated (unless you employ a real-time translator program). For this reason and because word order can be different in other languages, we strongly discourage string concatenations in globalized expressions as in Listing Two(b); instead, use a Format function as in Listing Two(c).

It should be noted that sometimes translation of a simple string may also depend on its context on a web page. In these cases, instead of keying off of the string alone, you could use a complex key to identify the text to be localized. For example, you could add an additional parameter to the GetLocalizedString() function, where you would pass the page path name and control ID (which you can obtain inside a globalized web control via this.Context.Request.Path and this.ID).

The formatting string now can be properly translated. With this, if we register its TagPrefix in our Test.aspx page, we can simply use GlobalizedLabel instead of an intrinsic label. Essentially, we can replace <LABEL> or <asp:Label> with <glb:GlobalizedLabel>; otherwise leaving the rest of the .aspx page and its code behind unchanged. It doesn't matter if the Text property is implicitly set in HTML script, as in Listing Two(d), or changed programmatically via lblGlobal.Text = "Test Label". The new label class will do a proper translation before rendering its content to the HTML writer. Because it was derived from the ASP Label class, it can be used in any way that a Label can be used, including manipulating Text properties and generating server-side click events.

Creating such globalized wrappers is sufficient to start with. You don't have to write unreadable code, the resource strings don't have to be created beforehand, and the globalization framework doesn't have to be implemented in advance.

Validation controls such as RequiredFieldValidator, RegularExpressionValidator, CompareValidator, and RangeValidator live, for the most part, an invisible life — attached to user-input controls, quietly validating data entered. They only appear when invalid input is detected, and sometimes they do it right on the client side via DHTML without posting back to the server. They can also work in conjunction with the ValidationSummary control that gathers all the validation messages and displays them conveniently in one place. It does this by querying and translating the ErrorMessage property of each validation control that signals invalid input. The ValidationSummary control contains the HeaderText property, which is also to be translated as in Listing Three.

The implementation of GlobalizedValidationSummary hides its base class HeaderText property rather than translating it in its Render() method. However, doing it at render time is more efficient because it ensures that each translated string is retrieved only once during the page load.

Figure 1 is a web form with validation control messages localized for Spanish. Notice how, in Listing Three, GlobalizedValidationSummary sums up all the ErrorMessage properties from each of the validation controls at the top of the page. Besides that, each validation control can display its own Text property next to the control that it validates, which in this case is used as a tip.

Listing Four presents the contents of the UserEdit.aspx file. This is its complete source; for illustrative purposes we did not use the code-behind file. Even though the content of UserEdit.aspx doesn't look globalized at all (in fact, English strings seem to be hardcoded everywhere), the globalized controls were easily rendered in Spanish.

The primary purpose of custom controls is to encapsulate control behavior that you find repeatedly being recreated in your business applications. There is nothing wrong in creating new control types that would be specific to your business needs that go beyond generic control functionality available in the Visual Studio Toolbox. Sometimes, a more complex situation arises when simply translating localized messages and calling the base.Render() function is not sufficient. Sometimes, a control must generate client-side code, which also must be localized. Consider Listing Five where the globalized confirmation button pops up a confirmation dialog on the client side: We use the Attribute.Add() method to generate the so-called "expando attribute" to enable wiring the OnClick event to the client-side JavaScript function. It is also processed on the server side as well.

Culture Determination

So how do our globalized controls determine the culture of users without requesting them being passed around from the web page? One way is to use HttpContext. Unlike in classic ASP where request context was restricted to the server-side script on the page and could not propagate down the "stack" of COM function calls, HttpContext (with User, Request, Response, and other children objects in ASP.NET) is available to all the functions on the calling stack. We recommend using HttpContext.Current.Request.Cookies to store and pass around user-culture information, as well as other profile attributes such as user roles.

Alternatively, we could have detected locale settings from the user's browser. When the GetLocalizedString() method is called several levels below, it would have no difficulty finding user-culture information stored in HTTP context and using it to retrieve the appropriate translation. Other possibilities include session state (now more robust in ASP.NET), passing hidden form variables, or passing parameters in the URL.

Localizing Numbers, Dates, and Currencies

String translation is important, but it isn't the only globalization task. Take, for instance, the task of converting numbers, currencies, dates, and times. Fortunately, the Common Language Runtime (CLR) framework was built with globalization issues in mind. All of its formatting functions take culture into consideration and have overloaded versions that take culture as a parameter.

Based on the specific culture, .NET formatting functions (such as ToString()) put correct decimal and millennium delimiters for numbers, and display dates and times in the proper format (like DD/MM/YY for the United Kingdom). For long date-time formats, days of the week and months would be translated into a language of the current culture.

Localizing monetary amounts represent several levels of challenges. Besides the fact that they are decimal numbers and suffer from the same indecisiveness about the use of commas, periods, and spaces as ordinary decimals do, you also have to worry about currency symbols, and whether to round up for zero, two, or three decimal places. Also, there is the issue of where to place a minus sign. Table 1 shows some representative permutations for various countries.

In the past, most applications dealt somehow with commas and periods, displayed three-letter currency symbols instead of currency signs, used some proprietary means to keep track of decimal places, and usually ignored positioning of minus signs. Fortunately with .NET, different representations become a breeze. Listing Six formats currency in a Dutch format, as in Table 1.

Unless there is a possibility that you may want to use different cultures on the same page, displaying multicurrency transactions is a hassle. Every formatting call must figure out what culture is to be used, to create appropriate CultureInfo, and to pass it to every function call. If culture information is not passed to a formatting function as a parameter, it uses Thread .CurrentCulture as a default. There are many ways to set it. One way is to set culture declaratively in a web.config file as in Listing Seven. In this case, the culture set in the web.config file would be a default for an entire web application and any nested virtual directories. Listing Eight shows how to set the language for a single page, you can also do it via its Culture property, as shown in Listing Nine. Finally, you can also set it from anywhere for a thread as in Listing Ten.

A warning: When specifying a culture name, be careful with local-neutral (such as "es") and extended cultures (like "es-ES-Is"). While they are defined in .NET documentation, they are not supported by the CultureInfo class.

Conclusion

The .NET platform and ASP.NET provide many features to support development and deployment of international applications. The object-oriented architecture of ASP.NET allows for easy extension with application frameworks to make globalization aspects of programming easier and more transparent. With such frameworks, the process of globalization may be reduced to simply gathering appropriate strings and translating them into supported languages. You won't have to write unreadable code, the resource strings won't have to be created beforehand, and the globalization framework itself need not be implemented in advance. Creating globalized wrappers would be sufficient for you to start using them in your code, and the implementation of globalized classes can be deferred until the proper time after which the entire web application UI will have appeared automatically localized.

DDJ

Listing One

(a)

<INPUT  type=text value= <%=GetString(unTranslatedString ) %>>

(b)
<pre>Response.Write("<SELECT>")
   For Each row In myRecordset
       Reponse.Write("<OPTION")
       If myRecordset!ID.value = oldSelection Then
           Response.Write(" SELECTED>")
       Else
           Response.Write(">")
       End If
       Response.Write(GetLocalizedString(myRecordset!MyField.value)&
               "</OPTION>")
   Next
   Response.Write("</SELECT>")

Back to Article

Listing Two

(a)

public class GlobalizedLabel : System.Web.UI.WebControls.Label
{
protected override void Render(HtmlTextWriter output) 
    {
        output.Write(Translator.GetLocalizedString(Text));   
    }
}

(b)
<pre>Translator.GetLocalizedString("The amount of ")) + price.ToString() + 
   Translator.GetLocalizedString(" will be charged to your account."));   

(c)
<pre>(new StringBuilder()).AppendFormat("The amount of {0:C} will be 
                                   charged to your account.", price);

(d)
<pre><%@ Page language="c#" %>
<%@ Register TagPrefix="glb" NameSpace="GlobalControls" 
                                         Assembly="GlobalComponents"%>
<HTML>
<BODY>
<glb:GlobalizedLabel id=lblGlobal runat=server>Test Label
</glb:GlobalizedLabel>
</BODY>
</HTML>

Back to Article

Listing Three

/// <summary>
/// Validation messages are translated
/// </summary>
public class GlobalizedRequiredFieldValidator : RequiredFieldValidator
{
   protected override void Render(HtmlTextWriter output) 
   {
    Text = Translator.GetLocalizedString(Text);
    ErrorMessage = Translator.GetLocalizedString(ErrorMessage);
    base.Render(output);
   }
}
 ... etc.
public class GlobalizedValidationSummary : ValidationSummary
{
   /// <summary>
   /// This header will be translated
   /// </summary>
   public new string HeaderText 
   {
    get{return base.HeaderText;}
set{base.HeaderText = Translator.GetLocalizedString(value);}
   }
}

Back to Article

Listing Four

<%@ Page language="c#" %>
<%@ Register TagPrefix="glb" NameSpace="AspNetDemo.Components" 
                                              Assembly="Components"%>
<HTML>
<HEAD>
    <meta content="Microsoft Visual Studio 7.0" name="GENERATOR">
    <meta content="C#" name="CODE_LANGUAGE">
</HEAD>
<body bgColor="#e4e4e0">
  <h3 align="center">User Information</h3>
  <form id="Form2" method="post" runat="server">
    <glb:GlobalizedValidationSummary id="vSummary" runat="server" 
    headertext="The following validation errors occurred:" />
    <hr>
    <table cellSpacing="0" cellPadding="0" width="100%" border="0">
      <tr>
        <td width="30%">Name</td>
        <td>
          <asp:textbox id="txName" runat="server"></asp:textbox>
          <glb:GlobalizedRequiredFieldValidator id="rfValidator1" 
                                          runat="server" display="dynamic" 
          controltovalidate="txName" ErrorMessage="Name is required." 
                                                         Text="required" />
        </td>
      </tr>
      <tr>
        <td>Password</td>
        <td>
          <asp:textbox id="txPassword" TextMode="Password" 
                            runat="server"></asp:textbox>
          <glb:GlobalizedRequiredFieldValidator id="rfValidator2" 
                            controltovalidate="txPassword" runat="server"
          ErrorMessage="Password is required." 
                            Text="minimum 5 characters" display="dynamic" />
       </td>
      </tr>
      <tr>
        <td>Confirm password</td>
        <td>
          <asp:textbox id="txPasswordConf" 
                                  TextMode="Password" runat="server" />
          <glb:GlobalizedCompareValidator id="cmpValidator1" 
                                  runat="server" display="Dynamic" 
          controltovalidate="txPasswordConf" 
                      ErrorMessage="Passwords don't match. Please, re-enter." 
          ControlToCompare="txPassword" operator="Equal" 
                      type="String" Text="doesn't match" />
        </td>
      </tr>
      <tr>
        <td>Email</td>
        <td>
          <asp:textbox id="txEmail" runat="server"></asp:textbox>
          <glb:GlobalizedRequiredFieldValidator id="rfValidator3" 
                                    runat="server" display="dynamic" 
          controltovalidate="txEmail" ErrorMessage="Email is required." 
                                     Text="required" />
          <glb:GlobalizedRegularExpressionValidator id="EmailRegValidator" 
          validationexpression="\w+@\w+\.\w+" runat="server"
          ErrorMessage="Email format is invalid.
                             " controltovalidate="txEmail" display="static">
          [email protected]</glb:GlobalizedRegularExpressionValidator>
        </td>
      </tr>
      <tr>
        <td>Age</td>
        <td>
          <asp:textbox id="txAge" runat="server"></asp:textbox>
          <glb:GlobalizedRangeValidator id="rngValidator1" 
                                  runat="server" controltovalidate="txAge" 
          MinimumValue="1" MaximumValue="199" Type="Integer" 
                                  ErrorMessage="Age is invalid." 
          display="static" Text="0 < age < 200" />
        </td>
      </tr>
      <tr><td> </td></tr>
      <tr>
        <td align="middle" colSpan="2">
          <glb:GlobalizedLinkButton id="btUpdate" 
                                  runat="server" Text="Update" />
           
          <glb:GlobalizedLinkButton id="btCancel" 
                                  runat="server" Text="Cancel">
          Cancel</glb:GlobalizedLinkButton>
        </td>
      </tr>
    </table>
  </form>
</body>
</HTML>

Back to Article

Listing Five

/// <summary>
/// Button that pops client-side dialog to confirm the action. It exposes 
/// ConfirmMessage property to set its dialog message with "Are you sure?" 
/// being a default. It relies on GlobalizedButton class to translate 
/// its Text and ToolTip properties. 
/// </summary>
public class GlobalizedConfirmButton : Button
{
private string m_ConfirmMsg = "Are you sure?";
    /// <summary>
    /// Message will be translated and displayed in confirmation dialog box. 
    /// </summary>
    public string ConfirmMessage 
    {
        set{m_ConfirmMsg = value;}
    }
    /// <summary>
    /// Renders HTML and client javascript for confirmation box
    /// </summary>
    /// <param name="output"></param>
    protected override void Render(HtmlTextWriter output) 
    {
if (Page.Request.Browser.JavaScript == true)
{
  output.WriteLine(@"<script id='clientConfirmation' 
language='javascript'><!-- 
            function RUSure() 
{if (confirm('" + Translator.GetLocalizedString(m_ConfirmMsg)
 + @"')) return true;else return false;}
            //--></script>");
  Attributes.Add("onclick","return RUSure();");
        }
        Text = Translator.GetLocalizedString(Text);
        base.Render(output);
}
}

Back to Article

Listing Six

decimal d = 1234.556M;
d.ToString("C",new System.Globalization.CultureInfo("nl-NL"));

Back to Article

Listing Seven

<configuration>
    <system.web>
        <globalization 
responseEncoding="utf-8" 
requestEncoding="utf-8" 
fileEncoding="utf-8" 
culture="en-CA" 
uiCulture="de-DE" 
/>  

    </system.web>
</configuration>

Back to Article

Listing Eight

<%@ Page language="c#" Culture="ru-RU" ResponseEncoding="utf-8" %>

Back to Article

Listing Nine

/// <summary>
/// Page sets its Culture according to the user profile  
/// to affect all the formatting on this page
/// </summary>
public class LocalizedPage: Page
{
   public LocalizedPage()
   {
       this.Culture = Utility.GetUserContextCulture();
   }
}

Back to Article

Listing Ten

System.Threading.Thread.CurrentThread.CurrentUICulture = 
new System.Globalization.CultureInfo("de-DE");




Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.