Introduction to Localization in ASP.NET 2.0

by Rick Strahl
www.west-wind.com/weblog


Last Update:
07/18/2007


ASP.NET 2.0 has greatly improved the localization features for Web applications compared to ASP.NET 1.x with many new features that simplify creating localized applications. In this article I discuss the new features from a technology point of view along with some insight into how it works under the covers.

kick it on DotNetKicks.com

Download the Code Samples for this article
Creating an ASP.NET Localization Resource Provider and Editor
Discuss this article


If you find this article useful, consider making a small donation to show your support  for this Web site and its content.

 

I started out writing this article as part of and introduction for another article on creating a custom resource provider and it quickly got too big for a single article. So this article now has been broken out to serve as an introduction of the localization features in ASP.NET 2.0. I've significantly expanded the focus of the original introduction so this article covers what I think are the key aspects of the new ASP.NET features. This article however is not meant to be an extensive treatise on all things localization with ASP.NET. Instead it focuses primarily on the technology available in ASP.NET not the all aspects of localization of which I don't even pretend to be an expert of. <s> The focus of this article is to provide an understanding of the base technology and provide the pre-requisites for the custom resource provider article.


If you’re new to Localization I hope this article can serve as a crash course in features and basic architecture that underlies the ASP.NET localization features.

Localization and ASP.NET 2.0

Localization is the process of making an application run in multiple locations. Locations are defined – at least in the context of .NET localization lingo – as a language and a country/region. So typically a locale is represented by a language and country code like en-US for English in the US or fr-CA for French in Canada. Each locale tends to have a specific set of formatting rules for things like numbers, dates, sorting rules and so on and a specific language that text needs to be translated to.  You can think of a locale of a specific set of formatting rules and a specific language. In addition specific regions may also have some less tangible culture specific features – like right to left text display, or specific meaning for images and icons. For example an icon in the western world might signify some very different meaning in the eastern world.

 

.NET provides very thorough localization support to create localized applications. That doesn’t mean it’s easy – localization is a very tedious and complex task and this article makes no attempt at covering the entire topic. But here are a few highlights of what .NET provides natively:

 

Unicode Support
Unicode support is crucial for representing the wide variety of characters of the worlds many languages. .NET supports Unicode much end to end from the Web page down to the database which means that application code has to do next to nothing to deal with character set translations. Client side code in the browser tends to run in an encoded Unicode format called UTF-8 which is an 8-bit representation of Unicode characters that encode characters higher than 255 with multiple characters. In ASP.NET Unicode and UTF-8 encoding is handled transparently (assuming you use UTF-8 encoding in web.config). Unicode support in .NET means that for the developer character encoding is largely transparent – there’s nothing to think about and it just works even with complex language combinations. The only time Unicode encoding/decoding issues come into play is when using binary streams in which case the Encoding.Utf8 or Encoding.Default classes can be used to encode/decode to and from Unicode.

 

Culture Mapping

.NET provides mapping of cultures via the CultureInfo class. This class and the Culture and UICulture properties of it determine the current culture context for the running code.

 

The current Culture determines how things are formatted which is important for number and date conversions. Numbers and dates format differently in different locales and the Culture determines these rules both for output (ie. ToString(), string.Format() etc.)  or for input parsing (ie. .parse()). The current Culture determines the default formatting that is applied, although you can override the default behavior by providing a custom IFormatProvider for most conversions. Other things effected are sort orders, various symbols like the currency symbol and separator characters for dates and numbers etc.

 

The key feature of .NET is that it can switch cultures on the fly by assigning a new Culture or UICulture. The values are tied to a specific .NET thread so an ASP.NET application can have multiple simultaneous requests running with each thread – or user connection – displaying data using a different Culture/UICulture. These classes are easy to use and provide very rich information about each locale they represent.

 

Resource based Localization

Resources are localizable pieces of information; strings and images typically, although anything can really be stored as a resource. Resources are stored in a resource store of some sort and .NET by default uses .Resx resources which are stored in XML files and compiled into binary code that is embedded in .NET assemblies. Resources are stored using resource ids which identify the resource and let an application query for a resource. A ResourceManager (or ResourceProvider in ASP.NET) manages and serves the resources based on the resource id and currently active locale. In ASP.NET this comes back to HttpContext.GetLocalResoureObject/GetGlobalResourceObject() which takes a resource ID and an optional Locale ID as a parameter.

Locale Identification and Mapping

Locales are indentified by a Locale Id. There are two common ways that locales are tagged using either string based identifier like en-US (language-Region) or as a numeric LCID value which is 1033 for the same en-US locale. Most commonly you’ll reference locales through the locale string. All of the configuration settings in web.config also use the string Locale id to identify a locale.

 

In .NET cultures are assigned through the CultureInfo class. To create a new culture you can simply do:

CultureInfo ci = new CultureInfo("fr-CA");

string sep = ci.DateTimeFormat.DateSeparator;

 

At this point a wealth of information about the culture is available.

 

Cultures are thread specific and you can manually or implicitly override a culture by creating a new CultureInfo class and assigning it. You can find the current culture in a couple of ways:

Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-CA");

Thread.CurrentThread.CurrentUICulture = new CultureInfo("fr-CA");

 

CultureInfo.CurrentCulture = new CultureInfo("en-US");

CultureInfo.CurrentUICulture = new CultureInfo("en-US");

 

Both of the above actually point at the same object so either one of them works.

 

Once the Culture or UICulture are set .NET will use that locale for all display of numbers and dates, for sorting, parsing etc. as well as try to apply resources that are configured for the specified locale.

 

You can override the default behavior for many of the conversion and formatting functions explicitly by providing a custom IFormatProvider to the conversion or parsing functions. For example to display a Date explicitly in German date format you might use:

 

DateTime.Now.ToString(new CultureInfo("de-de").DateTimeFormat);



Assigning Culture in ASP.NET

The code above already shows how you can apply a different culture and UI culture to the current thread. For ASP.NET the key for this to work properly is to ensure that any custom culture assignments occur very early in the request cycle so that formatting and resource localization can be applied to ALL content. Keep in mind that some content might load as early as the HttpHandler (think Page class) initializes so any culture assignments need to occur before that.

 

You can assign cultures in a number of ways – you can do it manually or let ASP.NET do it for you. You can do it as part of the ASP.NET pipeline or you can do it as part of an HttpHandler or an ASP.NET Page that is executing.

 

Let’s start with the manual approach which I personally think is the best way to handle this. If you want to have your entire application switch locale based on the inbound user’s default browser language you can use a little code in the BeginRequest handler for the Application object. In global.asax you can add a reference to code like that shown in Figure 1:

Listing 1: Switching locales dynamically based on browser language

/// <summary>

/// Sets a user's Locale based on the browser's Locale setting. If no setting

/// is provided the default Locale is used.

/// </summary>

public static void SetUserLocale(string CurrencySymbol,bool SetUiCulture)

{

    HttpRequest Request = HttpContext.Current.Request;

    if (Request.UserLanguages == null)

        return;

 

    string Lang = Request.UserLanguages[0];

    if (Lang != null)

    {

        // *** Problems with Turkish Locale and upper/lower case

        // *** DataRow/DataTable indexes

        if (Lang.StartsWith("tr"))

            return;

 

        if (Lang.Length < 3)

            Lang = Lang + "-" + Lang.ToUpper();

        try

        {

            System.Globalization.CultureInfo Culture =
                             
new System.Globalization.CultureInfo(Lang);

            System.Threading.Thread.CurrentThread.CurrentCulture = Culture;

 

            if (!string.IsNullOrEmpty(CurrencySymbol))

                          Thread.CurrentThread.CurrentCulture.NumberFormat.CurrencySymbol =

                  CurrencySymbol;

 

            if (SetUiCulture)

                System.Threading.Thread.CurrentThread.CurrentUICulture = Culture;

        }

        catch

        { ;}

    }

}

 

This function lets you easily and generically assign the locale to whatever the user has set as his default browser language. Notice there are a couple of options namely to allow overriding of the currency symbol and specifying whether the UICulture is to be set as well. The former is actually quite common – imagine that you have an application in the US that is localized to other languages and formats, but displays its totals as US currency values. If you don’t override the currency setting a European customer will see a Euro symbol with the order amount which is not going to make him happy as his price will be outrageously high.

 

To hook this up in global.asax you now simply can do this:

 

void Application_BeginRequest(object sender, EventArgs e)

{

    wwWebUtils.SetUserLocale("$",false);

}

 

And from here on out every request that comes in will automatically attempt to apply formatting for the browser locale. For now I have resource localization disabled by passing false. We’ll come back to that.

 

You can also use the above function on an ASP.NET page if your localization focus is more narrow. When using ASP.NET pages realize that any changes to culture settings should be applied in the special InitializeCulture method which you need to override.

 

protected override void InitializeCulture()

{

    base.InitializeCulture();
   
wwWebUtils.SetUserLocale("$",false);
}

 

InitializeCulture fires very early in the page request cycle before any property assignments are made to any control properties. This is the only place where culture and especially UICulture changes should be made on a form or localization is not going to be applied properly when the page loads.

 

ASP.NET can also automate this process for you both in page code and globally for the Web application. On the global level you can specify the following in web.config:

 

<globalization culture="auto:en-us" uiCulture="auto:en-US" />

 

Which will effectively switch the locale for every request automatically. The same can be applied on the page level with:

 

<%@ Page Language="C#" AutoEventWireup="true"  

         CodeFile="Cultures.aspx.cs"

         Inherits="Cultures"

         Culture="auto:en-us" UICulture="auto:en-us" %>

 

In which case the culture is applied on the page just before InitializeCulture is called.

 

While it may seem easier to use these automatic mechanisms because they are declarative understand that they are not as flexible. If you need to make special property assignments like the CurrencySymbol you can’t do this declaratively. Further you cannot override any values on the existing CultureInfo object once it’s been assigned by ASP.NET because it is read only. The only way to override any value on the auto culture assigned is to completely override it with a new CutureInfo object. So if you need to make any changes to the culture you are better off just creating it yourself and skip the declarative setting. Only use the declarative setting if you only need the default behavior which in my experience is very uncommon.

Locale setting Example

To demonstrate how locale assignment works I’ve provided a small sample page Cultures.aspx that lets you experiment with a variety of ways to switch cultures. It shows default content, auto-switched content and manually selected locale settings. Of these the latter is probably the most commonly used approach in real world applications. Although Auto culture switching may seem cool it’s not really practical since few applications localize for more than a few languages.

 

Figure 1 show the Cultures.aspx form in two different modes. You can see a non-adjusted default view and a view that picks up the browser’s default language and adjusts the Culture and UICulture (if available) accordingly. Finally there’s also a deterministic drop down that lets you choose one of a few languages. You can see the results in Figure 1 when I switch my browser language to Swiss German (de-CH):

 

Figure 1 – This sample page lets you see how the Culture and UICulture affect a page by toggling between between culture switching and leaving the default server culture in effect.

 

When changing a language deterministically by making a selection from the language drop down list at the bootm, it’s tricky getting the language choice to display immediately. This is because the Localization for the page has already been applied by the time the selection event occurs. Typically when the language is explicitly changed you need to save the language choice (in Session, Profile or a database) and then redirect to the same or another page, rather than posting back to the same page to display the new language choice correctly. It’s possible to make this work but it’s ugly in that you basically need to Post back to the same page, switch the language, display a message then post back again to actually show the updated language. The sample does it via a modal pop up that forces a page refresh to get the new language choice and also keep the POST values alive. Check out the demo to see how this was done, but as I said – it’s ugly!

 

BTW, I localized the form’s UI for German since I can kind of remember my native language <s>. French on the other hand I can’t do so no translation for that so that’s not a bug just my shortcoming. You can still see the French Culture mapping though for formatting.

Localization Settings and Caching

If you are using Caching with your Web site and you apply custom cultures it’s important to understand the effect localization has on caching. Even if you do no more than change the culture formatting, different users from different locales will generate different output. If a localized page gets cached without taking culture into account you will invariably end up serving incorrect cached data to a user of a different locale.

 

There are ways around this of course by specifically including locale information along with any other cache information. The easiest way for ASP.NET Pages to deal with OutputCache issues on localized pages is to use directive like this:

 

<%@ OutputCache Duration="60" VaryByParam="none"
               
VaryByCustom="Culture" %>

 

You can then create custom cache handler by overriding GetVaryByCustomString in the HttpApplication object (in global.asax typically):

 

public override string GetVaryByCustomString(HttpContext Context,
                                            
string Custom)

{

    if (Custom == "Culture")

        return Context.Request.UserLanguages[0];

 

    return base.GetVaryByCustomString(Context, Custom);

}

 

This will cause ASP.NET to check each page for the active browser language and apply caching only based for the appropriate locale. But note that this also has some serious implications especially if you use Culture="auto": This code will result in cached pages for every conceivable locale and can severely affect your cache size as many localized versions of a single page end up getting cached. Think very carefully about caching and the machine resource requirements for your application’s localized scenarios.

 

It’s actually quite rare for applications to implement more than a few langauges so a better choice might be to localize only by a few specified languages or language regions as shown in the code in Figure 2, which accepts only a few languages.

 

Listing 2: Custom Caching for a set of supported languages           

public override string GetVaryByCustomString(HttpContext Context,
                                            
string Custom)

{

    if (Custom == "Culture")

    {

        // *** Figure out the supported language from the

        // *** UserLanguage[]                

        string Lang = GetSupportedLanguage("en,fr,de","en");

        return Lang;

    }

 

    return base.GetVaryByCustomString(Context, Custom);

}
private string GetSupportedLanguage(string SupportedLanguages,
                                   
string DefaultLanguage)

{

    SupportedLanguages += ",";

    foreach (string Lang in HttpContext.Current.Request.UserLanguages)

    {              

        string[] LangKeys = Lang.Split('-');

        if (LangKeys.Length > 0)

        {

            string SelectedLanguage = LangKeys[0].ToLower();

            

            if (SupportedLanguages.Contains(SelectedLanguage + ","))

                return SelectedLanguage.ToLower();             

        }

 

    }

   

    // *** Return English as the

    return DefaultLanguage.ToLower();

}

 

 

This reduces the number of languages that actually will be cached significantly. Nobody wants to localize all 174 of Window’s supported locales <s>.

 

GetSupportedLocale() is also useful for the raw locale assignment like SetUserLocale. There are several overloads of this method that exist that allow assigning a specific locale and specifying one of a list of supported locales.

Resource based Localization in ASP.NET 2.0

Up to now I’ve talked about formatting of data and specifically number and date formatting and conversion as well as how ASP.NET and .NET figure out which locale to use.

 

While this is important the more prominent and complicated issue is resource localization. Resource localization is the process of creating resources for various locales and applying them effectively against static content in your application. This means localization of static text most commonly - control property values that display as text, any text messages for common operations (think warnings, error messages etc.) as well little things like words that describe ‘Today’,’Yesterday’ etc that you might use in the user interface or in popup messages. In a fully localized application every static message should be localized which is easier said than done. It’s a tough job and I can’t even do it justice here, so I won’t try to  go into all the issues involved. Rather I’ll show the mechanisms that ASP.NET provides to facilitate this process beyond raw manual resource assignment.

No Static Text!

The first thing to remember about Resource Localization in ASP.NET is that you need to ensure that as much as your UI as possible is localizable. This means ensuring that you don’t directly type static markup text into your pages and controls. In other words, make sure you use Label controls or the Localize control to hold static page content rather than directly entering text into an ASP.NET page.

 

You don’t want write markup like this:

 

Name: <asp:TextBox ID="txtResourceName" runat="server"></asp:TextBox>

 

as “Name:” will never be localizable. Instead you should use a Label control:

 

<asp:Label ID="lblName" runat="server" Text="Name:"></asp:Label>

<asp:TextBox ID="txtResourceName" runat="server"></asp:TextBox>

 

For larger text you can use the new ASP.NET Localize Control which is useful for larger blocks of text:
 

<div style="width:300px">

    <asp:Localize ID="lblWarrantyStatement" runat="server">

    Under no circumstances shall the provider of this software

    be held responsible for any damages <b>directly or indirectly</b>

    incurred by the use of this software.

    </asp:Localize>

</div>

 

Unlike the Label control, the Localize control lets you edit its text content inside of the visual editor in Visual Studio so it behaves more like a ‘static markup text’. The Label and Localize controls are dynamically accessible and ASP.NET can either automatically localize it or you can use raw code to assign a localized value to the label. I’ll come back to Resource assignment in a minute. First let’s look and see how resources are found.

Automatic Resource Compilation

 

ASP.NET provides a new mechanism for resource localization based on the concept of Local and Global resources. Global resources are akin to traditional resources where the resources you define in a global resource file are accessible from anywhere in the application. Local Resources in turn are tied to a particular form, control or Master Page and basically provide a way to link specific UI elements with localization resources. In combination both  types provide a good variety of options of how to represent your resources and get them assigned to static resources.

 

Both global and local resources are implemented using a very particular folder naming scheme that allows ASP.NET to dynamically find and compile your .Resx resources as part of the pre-compilation for the Web site. This means that resources can be deployed easily with your application and ASP.NET will handle compilation and placement of the resource assemblies.

 

The scheme works with an App_GlobalResources folder at the root of the Web site and an App_LocalResources folder for each directory of the site. Figure 1 shows what the folder layout looks like in Visual Studio.

 

Figure 1 – Resource Layout in an ASP.NET project

 

As you can see in Figure 1 global resources are defined at the root level of the Web site. Typically you’ll have only one or maybe a few resource files in the App_GlobalResources folder because resources are global and you don’t need to separate them out logically. Local resources exist at every directory level assuming you are actually localizing and each form or control or Master page has its own set of resources. Both global and local resources use .Resx files which are XML files that you can edit through the resource basic editor provided by Visual Studio.

 

Resources are named with a .Resx extension. The Invariant culture uses just the .Resx extension while any specific cultures use .en.Resx or .en-us.Resx for example for each of the supported locales. Figure 2 shows two resource files opened side by side in a Visual Studio Resource Editor side by side.

 

Figure 2 - .Resx files can be edited in Visual Studio, but VS.NET provides no mechanism for synching multiple resource files and their resource keys.

 

Note that I use Window Split View to achieve the side by side effect for this screen shot.  Visual Studio however provides no direct support for synching resources across different language files, so the process of keeping resources in sync is entirely up to you. I’ll come back to this issue when I’ll discuss the data driven resource provider in my separate resource provider article.

 

ASP.NET implicitly knows about these special folders and automatically compiles the .Resx resources. Resources are compiled into dynamically generated  assemblies (something like

App_GlobalResources.og0mkopq.dll and App_LocalResources.root.lpo-nquk.dll) that are then accessible to your ASP.NET application. ASP.NET then makes these resources available via its default resource implementation and HttpContext.Current.GetGlobalResourceObject & GetLocalResourceObject.

 

This process is nice because it frees you from manually having to compile resource files or even configuring resource files correctly in a project. Instead ASP.NET handles all the details at runtime for you. All you need to do is deploy the .Resx files to the server and ASP.NET will do the rest.

 

If you precompile your ASP.NET 2.0 projects with the ASP.NET Precompiler or ASP.NET’s Web Deployment Projects the compiled resource assemblies are created during the pre-compilation step rather than at runtime, but the effect is essentially the same. In this scenario you don’t need to copy the .Resx files – only the compiled reosource assemblies that are created in the BIN directory as part of the precompilation output directory.

Resource Fallback

Once the resources are in place you can access them via the HttpContext or the this (or me) Pointer in the context of  a Page using the  GetGlobalResourceObject() or GetLocalResourceObject() methods. Depending on the active UICulture in your page ASP.NET will retrieve resources from the appropriate resource store based on the ID provided.

 

When the resource lookup occurs ASP.NET checks for the active UICulture and returns the resource that matches the culture specified. The process to find the appropriate resource if an exact match is not found uses a mechanism called Resource Fallback. If an exact locale match is found the resource is pulled from the appropriate resource store and the resource is returned. But what if there’s no exact match? In that case Resource Fallback kicks in and .NET goes through a couple of steps to find and return a resource. The full sequence for resource lookups is:

 

1.    Specific locale lookup (en-US)

2.    Generic Language lookup (en)

3.    Invariant

 

So if I come into the site as a Swiss German with de-CH as my browser locale, ASP.NET looks first for the specified resource key in the de-CH resources (loaded from Resources.de-CH.Resx). If that fails it looks for just German (de from resources.de.Resx). Finally if there’s no matching resource there, the Invariant resources are used. If no match is found in the invariant location null is returned by GetGlobalResourceObject(). Depending on which resource embedding mechanism is used you can see either an empty value displayed or a page error as a result of missing resources.

 

The Invariant resource is the ‘default’ resource which is typically set to the primary language and the ultimate fallback. So for non-default locales there are up to 3 lookups that occur – once for the specific locale, one for the generic language, and once for the invariant locale.

 

For the ‘default’ or most used locale you can also force .NET to go straight to the neutral – ie. Invariant resources by specifying the assembly level NeutralResourcesLanguages attribute:

[assembly: NeutralResourcesLanguageAttribute("en-US")]

 

You can place this attribute anywhere, but most commonly you’ll put it into Properties.cs along with all other assembly level attributes. This attribute tells .NET to go directly to the Invariant/default resources if the specified culture is encountered. If most of your traffic comes from the specific locale you can save quite a few lookups and performance is slightly improved.

Resource Usage by .NET

Although resources can be stored in a variety of formats like .Resx files or a database, .NET caches all resources in memory. The first time a resource is accessed a ResourceReader reads the resources into memory as a ResourceSet which is basically a Dictionary that exposes the ResourceSet as a simple Enumerator. This means that resource lookup from the actual source – the .Resx file so far - only occurs once for each set of resources. After that all lookups are done in memory and is very quick although there is some memory overhead as these resources are loaded as in memory data structures (Hashtables typically). The memory usage can be substantial if many languages and many localizable resources are used but even with load testing the actual performance difference is almost negligible.

 

Keep in mind that the ResourceProvider loads resources once per application and the resources cannot be unloaded through easily code. They will only reload when the Web application restarts. ASP.NET’s folder monitoring does keep track of the App_LocalResources and App_GlobalResources folder and if any of the .Resx files there is changed it automatically causes the application to recompile and restart. Note this behavior depends on whether you app is pre-compiled or doing full source compilation on the server. Precompiled application MUST ship resources to the server already compiled because the resource expressions are checked at compile time and the compiler will fail if expressions are missing. The precompiler creates the appropriate App_GlobalResource*.dll and App_LocalResource*.dll files that must be deployed with the application – precompiled applications don’t work with dynamic resources.

Accessing Localization Resources

Resources wouldn’t do you much good if you couldn’t readily access them in your applications. As mentioned before you can use either code or a couple of Expression builders to embed localized Resource content into your Web applications both from local and global resources.

Global Resources

Global resources can be thought of traditional resources which are used globally throughout the Web application. Typically you use global resources for error messages and other display messages and common words that you might display as part of your application.

 

Because the resources are ‘global’ they can be accessed from anywhere in the context of the Web Application. The core functionality is provided by the HttpContext object and to retrieve a global resource value you can use a static method on HttpContext:

string Today = HttpContext.GetGlobalResourceObject("Resources",

                                                   "Today") as string;

 

This works from anywhere in a Web application including HttpModules and custom HttpHandlers. There’s also a Page specific version that can be used from within an ASP.NET page (or an TemplateControl based control):

 

string Today = this.GetGlobalResourceObject("Resources", "Today") as string;

Both of these commands look for Resources.Resx (or another locale specific version thereof .en-us.Resx, .en.Resx) in App_GlobalResources and find the resource key called Today inside of it.


GetGlobalResourceObject talks directly to the ASP.NET ResourceProvider configured and so doesn’t explicitly use a ResourceManager as you would use in say a WinForm or Library project.

 

Strongly Typed Resources – Better not to use them!

ASP.NET also generates strongly typed classes for each global resource file

so you can also access the above resources as:

 

string Today = Resources.resources.Today;

 

Although it may seem very tempting to use resources this way I’d advise you NOT to use this feature because this automatically generated resource class doesn’t use the ASP.NET ResourceProvider. Instead the code above talks directly to the .Resx ResourceManager. This means if you plug in a different provider your resources will no longer work correctly. In addition, because a separate Resource Manager is used to access resources it creates another set of resources in memory that’s separate from the ResourceProvider which is a waste of machine resources.

 

When I discuss my custom ResourceProvider I’ll provide another tool to generate strongly typed global resources that properly use the ASP.NET ResourceProvider.

Local Resources

As the name suggests local resources are local to a given source file – a Page, User Control or Master Page for example. If you look back on Figure 1 you’ll notice that Local Resource files match one or more resource files to each Page, User Control MasterPage etc. that requires localization and have a name like MyPage.aspx.Resx or MyPage.aspx.de-CH.Resx. Each page has it’s own set of local resources that are associated with the underlying ASPX page or control.

 

You can access local resources like this:

 

string Status = HttpContext.GetLocalResourceObject("~/Default.aspx",

                                                   "lblStatus") as string;

 

Here you need to specify a Web relative virtual path to the page, control or master and the resource id. The path is needed for ASP.NET to figure out which local resource to access.

 

As with global resources there’s a shortcut that can be accessed directly from the page or control in question by using:

 

string Status = this.GetLocalResourceObject("lblStatus_Text") as string;

 

Since we are on the current page or control the virtual path can be omitted in this call and the control simply passes its own VirtualPath to the HttpContext method. This is less verbose and so typically the preferred way to retrieve a resource manually.

Resource Markup Expressions

If all you could do is to call the above methods explicitly we really wouldn’t be much ahead of where we were with ASP.NET 1.x. But ASP.NET 2.0 also includes a couple of mechanisms for dynamically injecting resource expressions directly to control properties. There are two ways that this can be done using both Explicit and Implicit Resource Expressions

 

Explicit Resource Expressions

Explicit Resource Expressions allow you to embed expressions into any property definition for a control a page. So for example, rather than having to manually assign a resource value in code you can use a resource expression to assign the value declaratively in markup. To write the GetLocalResourceObject() call above and assign it to the lblStatus label control you can instead write:

 

<asp:Label ID="lblName" runat="server"

           Text="<%$ Resources: lblName_Text %>">

</asp:Label>

 

This assigns a local resource of lblName_Text to the Text property. You can use a similar expression for global resources:

 

<asp:Label ID="lblName" runat="server"

           Text="<%$ Resources: MyResources,lblName_Text %>">

</asp:Label>

 

where MyResources is the name of the .Resx file in App_GlobalResources that holds the global resource.

 

Explicit resources must be assigned to properties as a whole. In other words you can’t combine these resources with any sort of code logic; it’s all or nothing.

 

These explicit expressions can also be viewed and edited via the property editor, but it’s a bit cumbersome to get to those expressions. Figure 3 shows the Text property with an icon adjacent to it that indicates that a resource expression is responsible for the value. The value you see (Your Name:) is in fact pulled from the resource file even when viewed in the designer. The actual resource expression can be edited with the expression editor. Although it’s nice that the designer lets you edit these expressions, it’s actually easier to edit the expressions directly in markup.

 

Figure 3 – Explicit Resource Expressions show up in the Property Editor by marking properties with a blue text icon and an expression editor.

 

Explicit Resources are based on an ASP.NET compiler feature called an ExpressionBuilder. ExpressionBuilders are special classes that can handle <%$ %> markup expressions and turn the markup into generated code at page generation time when ASP.NET builds the control tree for a page/control. These Explicit Resource Expressions are converted into calls to this.GetLocalResourceObject() or this.GetGlobalResourceObject()at page generation time and are injected into the control initialization code. For example:

 

__ctrl.Text = Convert.ToString(

    base.GetLocalResourceObject("btnRefresh.Text"),

    CultureInfo.CurrentCulture);

 

It’s important to understand that these resource expressions are created very early in the page cycle; basically during the control initialization. This is why any changes to localization settings need to happen in the Page.InitializeCulture method which fires before these methods dynamically generated expressions are evaluated and assigning localized default values to the control properties.

 

Think of these resource expressions basically overriding the initial default value of a control property. Rather than assigning a static value, the localized resource value is pulled from the ResourceProvider and used instead.

 

Furthermore ASP.NET checks the resource expression at compile time. If the resource referenced doesn’t exist the compiler throws an error. This is not a runtime error but a compile time error so any missing resource in the site will keep the site from running! So it’s very important to ensure all of your resources can be found at least in the Invariant culture.

Implicit Resource Expressions

Implicit Resources use the same premise of code generation at compile time, but they make it even easier to assign resources to control properties – well, implicitly. Rather than linking resources one by one to each property as Explicit Resources do, Implicit Resources use a specific naming scheme to link all resources associated with a specific control to a set of matching resources with just a single tag in the control markup.

 

<asp:Label ID="lblName" runat="server" meta:ResourceKey="lblName">

</asp:Label>

 

This tag directs ASP.NET to look in the Local Resource file for this page, and find any ResourceKey that starts with "lblName." and assigns any of the properties that match. So if you have resource keys that look like this:

 

·         lblName.Text

·         lblName.ToolTip

 

both the Text and ToolTip property will be assigned from the resources in the resource file. Note that it’s not the control name that determines the key name, but whatever the resource key you specify in the meta:ResourceKey tag. The reason for this is that you may have multiple controls with the same name on the page with the same name used in a lower level container or template control for example.

 

Visual Studio 2005 includes a nice feature called ‘Generate Local Resources’ which can automatically generate resource keys for all localizable properties for all controls on the page. It’s on the Tools menu and it can be used only when the page to localize is in design mode. When you select it ASP.NET runs through all the controls on a page and creates meta:resource keys for any properties that are localizable and applies the default property values to each. You in effect get a local resource file that includes all localizable properties with their default vaues already assigned.

 

In case you’re wondering how this is done – ASP.NET 2.0 introduced a [Localizable] attribute that is assigned to any control property that can be localized. If you’re a control developer and you have properties that should be localized be sure to add this attribute to your controls so this Visual Studio tool can find it. I’ll also use this feature in my runtime resource provider to allow live resource editing in the follow up article.

 

The resource keys generated have names that are a bit verbose. For example, for the blName control above it becomes:

 

meta:ResourceKey="lblNameResource1"

 

If duplicate controls are found the tool will increment the control index number. I can see the need for the index at least on multiple items but I don’t really see how the  ‘Resource’ postfix helping any. But that’s just VS.NET being anal and making sure there’s not accidental overlap with another preexisting resource. Again, this is another feature I fix in my resource provide which generates names just as control names plus indexes if there are no duplicate names in the page/control.

 

One thing to consider is that The Generate Local Resources will generate resources for ALL localizable properties on the page, which means you’ll probably end up with a lot of resource keys that don’t have any values assigned to them. For example, on a label both the label text and ToolTip are localizable so for any control both of these resource keys are generated even though you may never actually localize a ToolTip.

 

As with Explict Resource Expressions the Implicit Resources cause ASP.NET to generate code in the Control Tree generation. In fact the generated code is exactly identical to the code I showed for the explicit expressions except that Implicit resources may cause several property values to be assigned from a single Implicit Resource key.

 

Unlike Explicit Expressions though a missing Implicit Resource Expression does not cause a compilation error, nor a runtime error. This is because implicit resource expressions are – well implicit and you can set properties explicitly. For example:

 

<asp:Label ID="lblName" runat="server" meta:ResourceKey="lblInvalid">

Name:

</asp:Label>

 

If the lblInvalid resource key doesn’t exist you’ll see Name: instead. If you haven’t specified a default value for a control though the value will simply be blank which can be quite disconcerting.

 

Implicit resources can be a huge timesaver in getting localizable content into your resource store. The Generate Local Resources tool can be run multiple times so as you make changes to your page it will keep creating new resources for any new controls you add and (in most cases) keep the resource names intact. If you delete controls though it doesn’t remove resource keys. The tool also doesn’t automatically create resource keys for anything but the default Invariant culture (ie. the plain .Resx file) which means as new controls are added it gets pretty hard to keep track of which resources have been localized properly especially if you’re dealing with multiple languages at the same time.

 

Implicit expressions also show up in the property sheet as shown in Figure 4. The property sheet shows all properties that are marked as Localizable (Text and ToolTip in this case), but it doesn’t show the actual values from the resource file.

 

Figure 4 – Implicit Resource Keys shown in the designer. They show up but you can’t really edit them in any way. It’s just an indicator to let you know which properties are localizable by Implicit Resource keys.

 

The property sheet support is useful only to give you a visual indication that a meta:ResourceKey is active (the red icon) and which properties are marked for localization. There’s no indication as to whether the values exist in the resource file however.

Explicit Implicit? What’s best?

With these localization features in place you can manage most UI based localization tasks quite easily by using markup code without having to dig into writing code. In general I think it’s much easier to use markup functionality for UI localization and the new features in ASP.NET make this job much easier.

 

Whether you use explicit or implicit resources largely depends on your preferences on how you’d like to work. Explicit resource leave finding each of the properties you need to localize up to you and you have to do the work of hooking up each of the resources. Implicit resources are really nice because it makes short work of at least creating all the base resources that you need to localize. The downside is that it creates a lot of resource that will never be actually localized because it generates resource ids for everything

 

I prefer the implicit approach just because it helps tremendously in the resource editing process to see what’s localizable. When I describe the custom resource provider and the ASP.NET resource editor in the follow up article you’ll see why this is very useful.

 

As it stands ASP.NET does a great job getting the base resources created, but it doesn’t help one bit in keeping other locales in sync. So as you add new controls and resource ids, using only VS.NET it becomes really hard to keep track of which resources have change and need to be updated in other locales.

Resources and Client Script

One issue that is becoming more important all the time is how to represent ASP.NET resources to client script in pages. If you’re writing AJAX type applications or even if you’re doing things like custom validations one of the things that you may need to do is pop up custom messages on the client side. But how do you get access to the server side resources?

 

The only way that’s supported natively in ASP.NET involves using ASP.NET script expressions in ASPX pages. Say if you have some client script and you want to expose a resource you can do something like this:

 

function Backup()

{

    if ( !confirm('<%= this.GetGlobalResourceObject( ("Resources","BackupNotification") %>' ) )

        return;

       

    Callback.Backup(Backup_Callback,OnPageError);       

}

 

As ugly as this looks it works reasonably well. Well in most cases. The above actually has some fairly serious shortcomings that deal with encoding of JavaScript strings. If in the code above a JavaScript string contains a single quote or any other escaped character like a line break the expression above would actually fail.

 

To fix this a little more work is needed. You basically need to encode the string embedded into the page so that it’s properly JavaScript encoded. Figure 3 shows a couple of helper functions that can be used to create a string like "Hello \'Rick\'\r\n! Rock on!" including the quotes. The idea is to wrap the call to GetGlobalResourceObject() and JSON encode the string. Figure 3 demonstrate the two functions:

 

Listing 3 – Helper methods to embed Resources into JavaScript

/// <summary>

/// Returns a resource string. Shortcut for HttpContext.GetGlobalResourceObject.

/// </summary>

/// <param name="ClassKey"></param>

/// <param name="ResourceKey"></param>

/// <returns></returns>

public static string GRes(string ClassKey, string ResourceKey)

{

    string Value = HttpContext.GetGlobalResourceObject(ClassKey, ResourceKey) as string;

    if (string.IsNullOrEmpty(Value))

        return ResourceKey;

 

    return Value;

}

/// <summary>

/// Returns a resource string. Shortcut for HttpContext.GetGlobalResourceObject.

///

/// Defaults to "Resources" as the ResourceSet (ie. Resources.xx.Resx)

/// </summary>

/// <param name="ResourceKey"></param>

/// <returns></returns>

public static string GRes(string ResourceKey)

{

    return GRes("Resources",ResourceKey);

}

 

/// <summary>

/// Returns a JavaScript Encoded string from a Global Resource

/// </summary>

/// <param name="ClassKey"></param>

/// <param name="ResourceKey"></param>

/// <returns></returns>

public static string GResJs(string ClassKey, string ResourceKey)

{

    string Value = GRes(ClassKey, ResourceKey) as string;

    return EncodeJsString(Value);

}

 

/// <summary>

/// Returns a JavaScript Encoded string from a Global Resource

/// Defaults to the "Resources" resource set.

/// </summary>

/// <param name="ResourceKey"></param>

/// <returns></returns>

public static string GResJs(string ResourceKey)

{

    return GResJs("Resources", ResourceKey);

}

 

/// <summary>

/// Encodes a string to be represented as a string literal. The format

/// is essentially a JSON string.

///

/// The string returned includes outer quotes

/// Example Output: "Hello \"Rick\"!\r\nRock on"

/// </summary>

/// <param name="s"></param>

/// <returns></returns>

public static string EncodeJsString(string s)

{

    StringBuilder sb = new StringBuilder();

    sb.Append("\"");

    foreach (char c in s)

    {

        switch (c)

        {

            case '\"':

                sb.Append("\\\"");

                break;

            case '\\':

                sb.Append("\\\\");

                break;

            case '\b':

                sb.Append("\\b");

                break;

            case '\f':

                sb.Append("\\f");

                break;

            case '\n':

                sb.Append("\\n");

                break;

            case '\r':

                sb.Append("\\r");

                break;

            case '\t':

                sb.Append("\\t");

                break;

            default:

                int i = (int)c;

                if (i < 32 || i > 127)

                {

                    sb.AppendFormat("\\u{0:X04}", i);

                }

                else

                {

                    sb.Append(c);

                }

                break;

        }

    }

    sb.Append("\"");

 

    return sb.ToString();

}

 

This looks like a bit of code but there are a number of overrides to make the expression as short as possible. With GResJs you can now do:

 

function Backup()

{

    if ( !confirm(<%= wwWebUtils.GResJs("BackupNotification") %>) )

        return;

       

    Callback.Backup(Backup_Callback,OnPageError);       

}

 

which is a guaranteed to produce a valid string value and makes the resource embedding code a little easier to write. You can find these methods in wwWebUtils.cs in the Westwind.Globalization project or you can create them in your project or even in the page.

 

This approach is not pretty but it’s easy and it works.

 

There are a few other ways that this has been handled in various JavaScript frameworks. Most of these approaches generate separate JavaScript files with the resources embedded into a resource object with one file generated for each locale. Since this isn’t native to ASP.NET the problem with this approach is that you have to write your own JavaScript and make sure you keep the .js resources in sync with the ASP.NET resources. I’ve experimented around with this approach generating the resources from my database Resource Provider, but actually found it easier to use the embedded ASP.NET expressions. Althogh it looks kinda ugly, functionally wise it works just fine and you end up with one set resources and fewer files served from the site. The bigger problem is if you want to use external .js files that need access to resources. Since these files don’t run through ASP.NET you can’t easily get script expressions into them. One way around this is to use ASPX files to serve the .js file but this gets pretty weird quickly. There are really no clean solution to this problem at the moment short of the resource class generation mentioned above.

Localize away

I hope this overview of localization features in ASP.NET has given you an idea of how the localization process works in ASP.NET. It’s actually pretty straight forward to get started with localization, although I probably won’t be the last person to tell you that localization is a time consuming and tedious job that’s best left to non-developer types who know about the nuances of the  localization trade.

 

Then again we often find ourselves working on these types of tasks anyway and for this particular situation my follow up article will be interesting. The article discusses a custom data drive resource provider implementation and a rich ASP.NET editor that can be used to edit localization resources.

 

I hope you’ll check it out…

 

Discuss this article:

http://shrinkster.com/qza 

Resources:

 

Code Download

www.west-wind.com/tools/wwDbResourceProvider/wwDbResourceProvider.zip

 

Online Sample

http://www.west-wind.com/wwdbResourceProviderSample/

 

Creating an ASP.NET Localization Resource Provider and Editor

Rick Strahl

www.west-wind.com/presentations/wwDbResourceProvider/

 

.NET Internationalization Book
Guy Smith Ferrier

http://shrinkster.com/qyr

 

ASP.NET 2.0 Localization: A Fresh Approach to Localizing Web Applications

Michelle Leroux Bustamante

http://msdn2.microsoft.com/en-us/library/ms379546(VS.80).aspx