Monday 23 January 2012

Globalization and Localization in .NET

Controlling Page-Level Culture Settings
ASP.NET 2.0 makes it easy to change the culture settings on a page-by-page basis. You simply add the UICulture and the Culture attributes to the Page directive within an .aspx page like so.
<%@ Page UICulture="fr" Culture="fr-BE" %>
Early in the page lifecycle, an .aspx page running with these attribute settings will initialize the current thread's CurrentUICulture property and CurrentCulture property with the appropriate CultureInfo objects. If you add the attributes shown above to an .aspx file during your testing, then add a built-in ASP.NET Web control such as the Calendar control to the page, you'll see immediately that things are working properly, as Figure 1 illustrates. The Calendar control on the left has been rendered on a page with a culture setting of en-US while the Calendar control on the right has been rendered on a page with a culture setting of fr-BE.

Figure 1 Localized Calendar Controls 
In most production Web sites, however, it doesn't make sense to hardcode specific culture settings into your pages like this. Instead, users who speak and read different languages should see different localized renderings of the same page. ASP.NET can automatically initialize culture settings for you on a per-request basis if you assign a value of "auto" to both the UICulture and the Culture attributes.
<%@ Page UICulture="auto" Culture="auto" %>
ASP.NET initializes the settings by examining HTTP headers sent by the browsers. You can test different localized versions of your pages in Internet Explorer® by changing the language preference settings in the Internet Options dialog.
Typically, you'll want all of the pages in your site to conform to the same culture settings. You can assign a site-wide value of "auto" for the UICulture and Culture attributes so you don't have to touch every page individually. Just add the following element into the web.config file located at the root of a site:
<globalization uiCulture="auto" culture="auto" />
You can also specify a default culture (in addition to the auto setting) that ASP.NET will use if it can't find an HTTP header to determine the user's preferred culture:
<globalization uiCulture="auto:en" culture="auto:en-US" />

Tracking Language Preferences Using Profiles
While the auto setting certainly makes things easy, it isn't always convenient for the user. Suppose, for example, a user prefers to read technical Web sites in English and business-related Web sites in French. He'd find it really annoying to change browser settings as he moved back and forth between sites. This user would really appreciate a Web site that let him select a language preference.
To see how to build a UI and a supporting implementation that makes it easy for users to switch back and forth between different languages download this month's code sample, an ASP.NET 2.0 Web site named LitwareWebApp. Its UI is shown in Figure 2.

Figure 2a English Version 

Figure 2b Localized Version 
The LitwareWebApp site tracks user language preferences using the new profile feature introduced in ASP.NET 2.0. You can add a string-based profile property named LanguagePreference that supports anonymous identification by adding the following elements to the site's web.config file:
<configuration>
    <system.web>
        <anonymousIdentification enabled="true"/>
        <profile>
            <properties>
                <add name="LanguagePreference" type="string" 
                    defaultValue="Auto" allowAnonymous="true" />
            </properties>
        </profile>
    </system.web>
</configuration>
The LitwareWebApp site is designed with a standard layout in a Master Page containing a RadioButtonList control named lstLanguage. Note that while this control displays friendly language names such as U.S. English and Belgian French, it also tracks the real culture names such as en-US and fr-BE using the SelectedValue property. When a user changes the language preference, the SelectedIndexChanged event of the lstLanguage control fires and executes the following code to update the LanguagePreference profile property:
Profile.LanguagePreference = lstLanguage.SelectedValue
Response.Redirect(Me.Request.Url.AbsolutePath)
The call to Response.Redirect forces a new round-trip from the browser to the Web server, which causes the lifecycle of the page to start again after the profile property has been set up with the desired language preference.
The next thing you have to deal with is programmatically adjusting the culture setting at the appropriate time in the page lifecycle. The proper way to do this in ASP.NET 2.0 is to override a method of the Page class named InitializeCulture. The page lifecycle has been designed to always call InitializeCulture before the page itself or any of its child controls have done any work with localized resources.
The design of the sample site requires that you add an overridden implementation of InitializeCulture to every page in the site. Unfortunately, you can't override the InitializeCulture method at the level of a Master Page because the MasterPage class does not inherit from the Page class. Furthermore, it would be tedious to override the InitializeCulture method separately for every page in a Web site. This would lead to many redundant implementations that would pose a serious maintenance issue.
A more efficient site-wide approach to initializing culture settings is to create a common Page-derived base class and then have all your .aspx page files inherit from that, and that's what I did in the LitwareWebApp sample site. My Page-derived base class named LitwarePage (see Figure 3) was defined in a source file named LitwarePage, which was added to the App_Code directory so that it is automatically compiled by ASP.NET and made available to other code in the current Web site.


Imports System.Globalization
Imports System.Threading

Public Class LitwarePage : Inherits Page

    Protected Overrides Sub InitializeCulture()

        ‘*** make sure to call base class implementation
        MyBase.InitializeCulture()

        ‘*** pull language preference from profile
        Dim LanguagePreference As String = _
            CType(Context.Profile, ProfileCommon).LanguagePreference

        ‘*** set the cultures
        If LanguagePreference IsNot Nothing Then
            Me.UICulture = LanguagePreference
            Me.Culture = LanguagePreference
        End If

    End Sub
End Class

Once you've created a Page-derived base class, you can update your .aspx page definitions to derive from it instead of from the standard Page class. For example, you can modify the partial class in default.aspx.vb to look like this.
Partial Class _Default : Inherits LitwarePage
    '*** page class definition goes here
End Class
At this point, I have a Web site capable of tracking user language preferences and initializing culture settings on a per-request basis. Now I have to localize the string literals for an ASP.NET 2.0 Web site using resource files so I can accommodate users who speak and read different languages.

Resource Files in ASP.NET 2.0
Since, by default, Visual Studio® 2005 doesn't use projects to manage ASP.NET 2.0 Web sites, there's no project-level resource file as there is for a Windows Forms application or a class library DLL. Instead, you must explicitly create resource files and add them to your Web site. Furthermore, you must use special folders that were introduced with ASP.NET 2.0: resource files containing global resources should be added to the App_GlobalResources folder, and local resources specific to a single file should be added to the App_LocalResources folder. Global resources are those that can be used on a site-wide basis from within pages as well as from within other files such as a site map. ASP.NET files types that support local resources include pages (.aspx files), Master Pages (.master files) and user controls (.ascx files).
Another difference with ASP.NET 2.0 is that you don't have to compile resource files ahead of time, as you do when developing an internationalized Windows Forms application. Instead, the ASP.NET runtime compiles global and local resource files into DLLs in a just-in-time fashion just as it does with .aspx files. This is a powerful feature because a company can add localization support for a new language simply by XCOPYing .resx files to a production Web server.
Let's work through an example of creating and using a global resource file in an ASP.NET 2.0 Web site with Visual Studio 2005. You can create a new global resource file by choosing the Add New Item command and then selecting Resource File.
When you click the Add button to create a new global resource file, Visual Studio 2005 prompts you with a dialog box recommending that you place the new resource file in the App_GlobalResources directory. Click Yes. If you put it elsewhere, ASP.NET will not automatically compile the resource file into a DLL.
Working with resource files in ASP.NET is similar to working with them in a Windows Forms application. You begin by creating a resource file with string literals localized for the default culture. In our sample Web site, there's a global resource file for this purpose named Litware.resx as shown in Figure 4. Once you've added all the named strings for the default culture, you can copy the resource file and rename it to something such as Litware.fr.resx to provide strings localized in French. You can also copy that French resource file and rename it to Litware.fr-BE.resx to maintain strings that have been regionally localized for Belgian French.

Figure 4 Localized Resources 
Adding and maintaining named strings within a resource file is easy because Visual Studio 2005 provides the convenient resource editor shown in Figure 5. Remember that resource files do not limit you to localizing strings. You can add in other types of resources such as image files, cascading style sheets and client-side JavaScript files.

Figure 5 The Visual Studio 2005 Resouce Editor 
Now let's create pages that retrieve named strings from a global resource file. It's very easy to do. Just as when you're developing an internationalized Windows Forms application, there's no need to program directly against the ResourceManager class supplied by the .NET Framework. That's because ASP.NET and Visual Studio 2005 build a strongly typed resource class behind the scenes for each global resource file and make it available through IntelliSense®.
The named strings you add to a global resource file are accessible through a strongly typed class nested within a top-level namespace named Resources. It takes a single line of code to assign a localized string to a control's property value:
lblApplicationName.Text = Resources.Litware.ApplicationName
In addition to programmatic access, ASP.NET 2.0 also introduces declarative syntax you can use to bind a named string to a property of a page or control. The syntax involves using the dollar sign ($) followed by the Resources namespace, the name of the resource file and the name of the string:
<%$ Resources:Litware, ApplicationName %>
For example, if you want to bind the string named ApplicationName to the Text property of a label within an .aspx page, you can write the tag to look like this:
<asp:Label ID="lblApplicationName" runat="server" 
    Text="<%$ Resources:Litware, ApplicationName %>" /> 
Visual Studio 2005 also provides a convenient tool named the Expression Builder, shown in Figure 6. This utility can help you generate the required syntax for binding named strings in a resource file to the properties of controls and pages. Once you've added one or more global resource files with named strings, you can access the Expression Builder by putting an .aspx page into design view and accessing the Expressions property through the Property sheet.

Figure 6 Expression Builder 
Note that declarative resource binding expressions are not limited to .aspx files, .ascx files and .master files. They can also be used to localize string literals from a site map defined in a Web.sitemap file. Figure 7 shows the XML from the site map used in the LitwareWebApp Web site to localize the link titles shown in the site's navigation menu.


<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" 
        enableLocalization="true" >
    <siteMapNode url="default.aspx" 
        title="$resources: Litware, HomePageLink"  >
        <siteMapNode url="AddCustomer.aspx" 
            title="$resources: Litware, AddCustomerPageLink"  />
        <siteMapNode url="Controls.aspx" 
             title="$resources: Litware, ControlsPageLink"  />
        <siteMapNode url="About.aspx" 
             title="$resources: Litware, AboutPageLink"  />
    </siteMapNode>
</siteMap>

Working with Local Resources
A local resource file contains resources for a single file-based item within a site such as a page, a Master Page or a User Control. Each local resource file must be properly named and added to the App_LocalResources folder to be compiled by ASP.NET.
A local resource file should be named in accordance with the file-based item to which it is supplying its resources. For example, the local resource file containing default culture resources for the AddCustomer.aspx page should be named AddCustomer.aspx.resx. The local resource file containing French resources should be named AddCustomer.aspx.fr.resx.
Once you've added named strings to a local resource file, you can access them from within a page or a user control in three different ways. First, you can access them programmatically. Second, you can bind to them declaratively using explicit syntax. Third, you can bind to them declaratively using implicit syntax. Let's examine each of these approaches.
Imagine you've created local resource files to localize all the control captions shown on the page AddCustomer.aspx. To localize the caption shown on the page's submit button, you can create a localized string named btnSubmit.Text. Once you've added this named string to the local resource file, you can access it through code by calling the GetLocalResourceObject method and converting the return value to a string:
'*** code inside AddCustomer.aspx.vb
btnSubmit.Text = _
    Me.GetLocalResourceObject("btnSubmit.Text").ToString()
This code isn't as nice as the code shown earlier to access a named string from with a global resource using a strongly typed class. Local resource files do not have associated strongly typed classes, so you won't benefit from IntelliSense and you must explicitly convert the Object-based return value when calling GetLocalResourceObject.
If you want to use an explicit declarative binding syntax, it works almost identically as with global resources. The only difference is that you omit the name of the resource file when you're working with local resources:
<asp:Button ID="btnSubmit" runat="server" 
    Text="<%$ Resources:btnSubmit.Text %>" />
Implicit declarative binding syntax is the most powerful option. You begin by adding a special attribute named meta:resourcekey to a control tag or to an ASP.NET directive such as Page, Master or Control. For example, if you want to use implicit declarative binding syntax with a Button control in an .aspx file, you would write its tag to look like this:
<asp:Button ID="btnSubmit" runat="server" 
    meta:resourcekey="btnSubmit" />
Once you've added the meta:resourcekey attribute, the only other thing to worry about is making sure there are strings in the local resource file with the proper name. In my example, ASP.NET will automatically load the localized string named btnSubmit.Text and assign it to the Text property of the control named btnSubmit.
The key point is that implicit binding is based on creating strings that have names matching targets defined by the meta:resourcekey attribute plus the name of a property. Since the meta:resourcekey in this example is targeted at btnSubmit, you can bind to several other property values in addition to Text simply by adding more named strings into local resource files as shown in Figure 8.

Figure 8 Adding Named Strings 
Note that Visual Studio 2005 provides a convenient command called Generate Local Resource in the Tools menu when a Page, User Control, or Master Page is open in design view editor. This command automates the creation of a local resource file for the default culture. It also adds meta:resourcekey attributes throughout the page and creates corresponding string values in the local resource file to act as targets for meta:resourcekey attribute entries.
Finally, note that there's a new ASP.NET 2.0 component called the Localize control, which lets you localize any element on an .aspx page. It provides a design time feature not offered by its base class, the Literal control; in particular, the Localize control provides design time editing of static content so you can see a default value while working in page design mode.

Embedding Resources in a DLL Project
I'm going to depart from the topics of internationalization and localization for a moment to discuss a new ASP.NET technique for using embedded resources in a class library DLL. This technique allows you to embed resources such as images files, cascading style sheet files, and JavaScript files inside a DLL and expose them through a URL on the hosting Web server.
Note that this technique requires the use of a class library DLL targeting ASP.NET 2.0 Web sites. The new functionality was added by the ASP.NET team specifically to give server-side control authors a better way to distribute the resource files that accompany their custom controls and Web Parts. Instead of having to distribute resource files along with a DLL and ensure they are copied into an accessible path on the hosting Web server, resource files can be distributed inside the DLL itself and exposed through URLs generated by ASP.NET at run time.
The LitwareWebApp Web site contains a class library DLL project named LitwareWebComponents that demonstrates this technique. Inside this project, an image file named LitwareSlogan.png has been embedded as a resource. You can embed a resource into an assembly by changing the file's Build Action to Embedded Resource, as shown in Figure 9.

Figure 9 Embed a Resource 
To provide Web-based access to an embedded resource file inside a DLL, you must add an assembly-level attribute named WebResource. When you add the WebResource attribute, you must include the qualified name of the resource file along with its MIME type. Within a Visual Basic® class library DLL project, the qualified resource file name includes the project name.
'*** inside AssemblyInfo.vb
Imports System.Web.UI
<Assembly: WebResource( _
    "LitwareWebComponents.LitwareSlogan.png", "image/png")>
The WebResource attribute is what allows to you to provide the ASP.NET runtime with the metadata it needs to retrieve the resource file from the DLL using a URL that can be generated at run time. To generate the URL to the resource file from code within a server-side control you call a method named GetWebResourceUrl as shown in Figure 10.


Public Class LitwareCustomControl
    Inherits WebControl

    Protected imgSlogan As Image

    Protected Overrides Sub CreateChildControls()
        ‘*** determine Url to Web resource 
        Dim ResourceName, ResourceUrl As String
        ResourceName = "LitwareWebComponents.LitwareSlogan.png"
        ResourceUrl = Me.Page.ClientScript.GetWebResourceUrl( _
            Me.GetType(), ResourceName)

        ‘*** Point Image control to Web Resource Url
        imgSlogan = New Image
        imgSlogan.ImageUrl = ResourceUrl
        Controls.Add(imgSlogan)
    End Sub

End Class

Here's what happens behind the scenes to make this technique work. A call to GetWebResourceUrl generates a URL pointing to a built-in HTTP handler named WebResource.axd. The dynamically generated URL also includes a query string identifying the name of the target DLL and the embedded resource file. The ASP.NET runtime responds to requests for WebResource.axd by loading a custom HttpHandler class named the AssemblyResourceLoader.
When the AssemblyResourceLoader class is called upon to load a resource file from a DLL, it reads the metadata supplied by the WebResource attribute. The AssemblyResourceLoader class has been implemented to extract the request resource file from the image of the DLL and stream it back to the caller. The AssemblyResourceLoader class even supplies a caching algorithm which can reuse the same resource file across multiple requests after it has been loaded into memory on the front-end Web server host.

Displaying Localized Images
Though using embedded resource files and the WebResource attribute is powerful, there are some notable limitations. First, you can only use this technique inside a DLL project that's targeted for ASP.NET 2.0 Web sites. You can't use it directly inside an ASP.NET 2.0 Web site. Second, this technique doesn't really support any form of localization. You'll have to resort to a different approach if your Web site has resource files such as graphic images and cascading style sheets that have been localized.
The LitwareWebApp Web site displays a graphic image named LitwareSlogan.png. The site has been designed to display a different version of the image depending whether the current user prefers English or French. While ASP.NET 2.0 doesn't directly support localizing image files, it doesn't require too much custom code to achieve the desired effect.
You can start by adding the localized versions of an image file to localized versions of a global resource file. For example, the English version of LitwareSlogan.png has been added to the global resource file named Litware.resx while the French version of LitwareSlogan.fr.png has been added to Litware.fr.resx. The resources in both resource files have been given the same name of LitwareSlogan.
When you have localized versions of the image file in different localized versions of a global resource file, you can load them conditionally based on the user's language preference using the custom handler file named LitwareSlogan.ashx shown in Figure 11.


<%@ WebHandler Language="VB" Class="LitwareSlogan" %>

Imports System
Imports System.Web
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Threading
Imports System.Globalization

Public Class LitwareSlogan : Implements IHttpHandler
    
    Public Sub ProcessRequest(ByVal context As HttpContext) _
               Implements IHttpHandler.ProcessRequest
        ‘*** switch to correct MIME type
        context.Response.ContentType = "image/png"

        ‘*** change UICulture before loading image
        Dim LanguagePreference As String = CType(context.Profile, _
            ProfileCommon).LanguagePreference
        If LanguagePreference IsNot Nothing AndAlso _
           LanguagePreference <> "Auto" Then
            ‘*** change culture UI setting fro current thread
            Thread.CurrentThread.CurrentUICulture = _
                New CultureInfo(LanguagePreference)
        End If

        ‘*** load in image from resource file and send to browser
        Resources.Litware.LitwareSlogan.Save( _
            context.Response.OutputStream, ImageFormat.Png)
    End Sub
 
    Public ReadOnly Property IsReusable() As Boolean _
            Implements IHttpHandler.IsReusable
        Get
            Return False
        End Get
    End Property

End Class

The custom handler class defined in LitwareSlogan.ashx initializes the current thread's CurrentUICulture setting before retrieving the image file from the global resource file using logic similar to what you saw earlier in the custom InitializeCulture method. You might notice that to load the proper resource file, you must initialize the current thread's CurrentUICulture property, but it is not necessary to initialize CurrentCulture property.
Once this custom handler has properly initialized the CurrentUICulture setting, it can then access the image file through the strongly typed resource class for Litware.resx. Then it is simply a matter of writing the bits of the image file to the HTTP response stream. The very last step to display the localized image is to assign the LitwareSlogan.ashx URL to the ImageUrl property of an Image control on any page within the site.

Summing Up
ASP.NET 2.0 makes internationalizing Web sites and resources easier. It's trivial to initialize the culture settings for pages within a Web site by examining HTTP headers sent by the browser. Plus it's easy to design a more elaborate scheme that allows users to personalize their experience by configuring their desired language preferences.

No comments:

Post a Comment

Total Pageviews