The first thing you need to decide when localizing a site is whether you want to localize only static content (menus, links, copyright notices, usage agreement, titles and descriptions for pages, tables, fields, buttons, and other controls) or whether you want to provide a translation for everything, including articles, poll questions, product listings, and so on. Let me state up front that adding support for complete localization would be very difficult at this stage of development, as it would require a complete rework of the database design, the DAL, the BLL, and the UI. It's something that should be planned very early, during the initial site design and the foundations development. Complete localization in a single web site is not a common requirement: You normally wouldn't translate every article on the site, forums, polls, and newsletters, but rather, only those that have a special appeal to one country or language-specific audience. You may also want to present information differently for different languages — changing something in the site's layout, for example. Because of this, most sites that want to be fully localized "simply" provide multiple copies of their pages under different subdomains or folders, one copy for each language. For example, there could be www.contoso.com/en and www.contoso.com/it or http://en.contoso.com/ and http://it.contoso.com/. Each copy of the site would target an independent database that only contains data for that specific language. If you take this approach, you'll only need to make static content localizable, and then install the site multiple times for multiple languages. Another advantage of this strategy is that with completely separate web sites you can have different people managing them independently, who would be able to create content that best suits the audience for that particular language. In this chapter we'll localize the site's static content, and we'll support the different locale settings for dates and numbers in different languages.
Our sample site will only be installed once, and the language for which the site is localized will be specified by each user at registration time, or later from the Edit Profile page. This setting is mapped to the Preferences.Culture profile property, which was described and implemented in Chapter 4. An alternative would be to detect the user's favorite language from her browser's settings, which is sent to the web server with the request's header. However, many nontechnical users don't understand how to set this, and it would be difficult to explain it on your site and answer support questions from people who don't understand this. Therefore, it's better to directly ask users which language they'd like to use, so they understand what it's for, and how to change it. The next section provides an overview of the new features introduced by ASP.NET 2.0 regarding localization of static content.
ASP.NET (and the .NET Framework in general) has always supported localization to some extent. Displaying and parsing dates and numbers according to a specific culture, for example, only requires you to create a System.Globalization.CultureInfo instance for that culture (e.g., "en-US" for American English, or "it-IT" for Italian of Italy), and use it as the value for the CurrentCulture property of the current executing thread (System.Threading.Thread.CurrentThread). For example, after executing the following statement, all dates and numbers displayed to the user would follow the Italian format by default:
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture("it-IT");
The preceding code would have been typically placed into the page's Init or Load event handlers, or even better in the Application_BeginRequest event handler accessible from the Global.asax file, so that it would execute for all pages of the site without replicating the code in each of them. Putting it into a custom base class from which all pages' code-behind class would inherit was another great solution.
Localizing static content by dynamically setting the properties of the various controls on a page (such as Text, ToolTip, etc.) with values translated to a specific language was much less easy in ASP.NET 1.x, though. You needed to create one or more resource files for each language you wanted to support (such as Messages.resx for the generic default culture, Messages.it-IT.resx for Italian, Messages.fr-FR.resx for French, and so on) and write key-value pairs into these files (they are XML files, but Visual Studio has an editor that allows editing them in a grid), where the value was some string translated into the language of the resource file. Then, from the code-behind class of every page that you wanted to localize, you had to manually write something like this:
using System.Resources; using System.Reflection; ResourceManager rm = new ResourceManager( "WebProjectName.Messages", Assembly.GetExecutingAssembly()); lblTitle.Text = rm.GetString("Title");
This would instantiate a ResourceManager object for the resources stored in a class called WebProjectName.Messages (created from the .resx file). Then, it would load the string resource with a key equal to "Title", and use it for the Text property of a label control. ResourceManager automatically loads the resource class from the current assembly, or from one of the satellite resource-only assemblies, according to the culture specified by the CurrentUICulture property of the current thread. If the resource for the current UI culture is not found, the ResourceManager will fallback to the resources for the default neutral culture.
Note |
Note that the property used by ResourceManager to load the correct satellite assemblies is CurrentUI Culture and not CurrentCulture, which is instead used to display and parse numbers and dates. The two properties are often set to the same culture, but not necessarily. |
Although the previous framework had technology for localizing sites, the solution outlined in the preceding section had a number of problems that made the process unwieldy and prone to error. The most significant issues were as follows:
You had to create the resource files manually, in a folder of your choice. However, the final name of the resource class would change because of the inclusion of the folder name, and many developers didn't realize this. This often resulted in errors whereby resources could not be found.
You had to specify the resource key as a string, and if you misspelled this string it resulted in errors whereby resources could not be found. If this were an enumeration it could avoid the possibility of misspelling it.
You had to invent your own naming convention to choose key names that would identify the same resource but for different pages, such as Page1_Title and Page2_Title. This is because there were only site-wide global resources, and not page-specific resources. You could also create different .resx files, one for each page and thus simulate page specific resources. This was merely a way to physically separate resources as they were still accessible by any other page.
Above all, you had to manually write the code to set the Text property (or any other localizable property) to the proper string loaded by means of a ResourceManager, as there wasn't a declarative way to do it from the .aspx markup file.
With ASP.NET 2.0 all this changes considerably, and even though under the cover things work pretty much the same, from the developer's viewpoint things are much easier and more intuitive now. Here's a list of improvements, which are described in detail in the following subsections:
Strongly typed global resources: Once you create a global resource file (like the ones you may have used under ASP.NET 1.x), it is dynamically compiled into a class, and you can immediately see and access the class listed under the Resources namespace. Each resource of the file is accessible as a property, and IntelliSense is provided by Visual Studio to make it easier to select the right one. No more mistyped resource names!
Page-level resources: In addition to global resource files, you also have page-specific resource files, so that you can place the resource strings only in the page that uses them. This enables you to have a resource called Title for every page, with different values, as they are stored in separate files. You no longer have to come up with a naming convention such as using the page name as the prefix for the resource keys.
New localization expressions: Similar to data binding expressions, these enable a developer to associate an expression to the properties to localize directly in the .aspx markup file, so you don't need any C# code. A special declarative syntax is also available to bind all localizable properties to resources in a single step. Programmatic localization is still possible, of course, and has been improved as mentioned before for the global resources.
Improved Visual Studio designer support: This enables you to graphically associate a localization expression to a resource string from a dialog box, without requiring the developer to write any code. There's also a command to automatically generate the neutral language page-level resource file for the current page, which you can copy, rename, or translate to another language.
Auto detection of the Accept-Language HTTP header: This is used to automatically set the page's UICulture and Culture properties, which correspond to the current thread's CurrentUICulture and CurrentCulture properties.
Custom providers : Should you want to store localized resources in a data store other than .resx files, such as a database, you can do that by writing your own custom provider. This enables you to build some sort of online UI for managing existing resources, and create new ones for additional languages, without the need to create and upload new resource files to the server.
Global resources are shared among all pages, controls, and classes, and are best suited to store localized data used in different places. When I say "data," I don't just mean strings, but also images, icons, sounds, and any other binary content. Although this was already possible in ASP.NET 1.x, VS.NET 2003 had worse graphical support for resource files. Now you access a resource file item (from the Add Item dialog box) under a folder named App_GlobalResources, which is a special folder, handled by the ASP.NET runtime and VS2005; and you can insert data into the grid-style editor represented in Figure 11-1.
If you click the arrow on the right side of the editor's Add Resource toolbar button, you will be able to create an image or icon, or insert any other file. Figure 11-2 shows the window after choosing Images from the drop-down menu of the first toolbar icon (where Strings was selected in Figure 11-1) and after adding a few image files.
After adding a few strings and a few images, you can go to a .cs file in the editor and type Resources: IntelliSense will pop up a drop-down list with the names of the resource files added to the project, i.e., Messages in the example shown in Figure 11-1. Then, when you type Resources.Messages, it will list the string and image resources added earlier; and if you look closely at Figure 11-3, you'll also note that image resources are returned with the proper type of System.Drawing.Bitmap.
This results in less manual typing, less probability of mistyping a resource or key name, and less casting.
Programmatic access of resources is necessary in some cases, particularly when you need to retrieve them from business or helper classes, and in this case you'll be happy to know that IntelliSense for dynamically compiled resources works also! However, when the resource will be used as the value for a property of a control on a page, there's an even easier approach: Just select the control in the page's graphical designer and click the ellipses button on the right side of the "(Expressions)" item in the Properties window. From the dialog box that pops up you can select the property you want to localize, select Resources as the expression type, and select then the resource class name and key, as shown in Figure 11-4. Note that you just select the resource you want from a pre-filled drop-down list, so you don't need to type that yourself.
After using this dialog box on an ASP control, if you go to the Source View, the declaration of the localized Label control will look like this:
The Text property is set to a new localization expression (also called dollar-expression, because of the leading $ character), which at runtime will return the resource string from the Greeting2 item of the Messages class. You can also write these expressions yourself if you prefer coding your pages directly in the Source View (as I do). In either case, these expressions in the .aspx code are much better than manually writing C# code in the code-behind file, as you had to do with ASP.NET 1.x.
You can create page-level resources by creating resource files just as you do for global resources, but placing them under a folder named App_LocalResources (as opposed to App_GlobalResources used for global resources) located at the same level of the page to localize. For example, if the page is in the root folder, then you'll create the App_LocalResources under the root folder, but if the page is under /Test, then you'll create a /Test/App_LocalResources folder. This means you can have multiple App_LocalResources folders, whereas you can only have one App_GlobalResources folder for the whole site. The name of the resource filename is also fundamental, as it must be named after the page or control file for which it contains the localized resources, plus the part with the culture name: For example, a culture-neutral resource file for Localization.aspx would be named Localization.aspx.resx, whereas it would be named Localization.aspx.it-IT.resx for the Italian resources. In Figure 11-5, you can see the organization of files in the Solution Explorer, and the resource file being edited in the grid editor.
You can still use the Expressions dialog shown in Figure 11-4 to bind a control's property to an expression pointing to a localized resource: When you point to a page-specific resource you just leave the ClassKey textbox empty. The code below shows the generated expression:
<asp:Label ID="lblCopyright" runat="server" Text="<%$ Resources:CopyrightMessage %>" / >
It differs from the expression shown in the previous section, as it doesn't include the class name; it just specifies the resource key. If you want to access local resources programmatically, you use the page's GetLocalResourceObject method, which takes the resource key name and returns an Object that you must cast to string or to the proper destination type (such as Bitmap if you stored an image):
string copyrightMsg = (string)this.GetLocalResourceObject("CopyrightMessage");
Even with localization expressions and local resources, localizing full pages will be a slow task if you need to create the expressions for dozens of controls, and things get worse if you need to localize multiple properties for the same control, such as Text, ToolTip, ImageUrl, NavigateUrl, and so on, which is often the case. To speed things up, Visual Studio offers the Tools ð Generate Local Resource command, which generates a local resource file for the current page, and creates entries for all localizable properties of all controls on the page, following the ControlName.PropertyName naming convention for the names of the resources. Resource items are also automatically set with the value extracted from the page's markup; if a property is not used in the control's declaration, a resource item for it is generated anyway and left empty.
Note |
Localizable properties are those that are decorated with a [Localizable(true)] attribute, which you can add to the properties of your custom controls. However, even when you add it to properties of user controls, resources for those properties will not be automatically generated by the Generate Local Resource command. You can create the local resources for those properties yourself, and write the localization expressions to make the association: Everything will work perfectly at runtime, so this is only a design-time limitation. In addition, you'll have to write the localization expressions manually, because the (Expressions) item is not available from the Properties window when a user control is selected. |
Figure 11-6 shows the resource editor for the Localization.aspx.resx local resource file after executing this Generate Local Resource command on the test page.
Besides the automatic generation of the resource file (or the addition of the resource items, if a resource file for that page was already present in the proper folder), what's even more interesting is that each control's declaration is modified as follows (note the code in bold):
<asp:Label ID="lblTitle" runat="server" Font-Size="X-Large" ForeColor="#C00000" meta:resourcekey="lblTitleResource1" Text="Localization Demo" Text="This page provides a nice demo of new ASP.NET 2.0 localization features" />
A meta:resourcekey attribute is added to the declaration and is set to the prefix used in the local resource file to identify all localized properties of that control, such as lblTitleResource1.Text and lblTitleResource1.ToolTip. These are called implicit localization expressions (expressions used earlier are considered explicit). At runtime, the framework parses all resources and applies them to the properties of the corresponding controls, making the mapping of the first part of the key with the value of meta:resourcename. This means that the control's declaration is decorated with just a single new attribute, but may make multiple properties localizable. Later, if you want to localize a property that you didn't take into account originally, you just need to go to the resource editor and add an item following the naming schema described above, or edit its value if it already exists.
Important |
Note that the controls retain their original property declarations after running the Generate Local Resource command. These declarations are no longer necessary, though, as the property's value will be replaced at runtime with the values saved in the resource file; therefore, you can completely remove the definition of the Text, ToolTip, and the other localized properties from the .aspx files to avoid confusion. |
This behavior works with the page's title as well, originally defined in the @Page directive, which is modified as follows:
<%@ Page Language="C#" meta:resourcekey="PageResource1" ...other attributes... %>
All content that you want to localize must be displayed by some sort of server-side control, such as the Label or Literal controls. You'll typically want to use a Literal over a Label if you don't need the appearance properties of a Label, either because you don't need to format the text or because the formatting is done through raw HTML tags present directly within the text, which is frequently the case for static text such as section and field titles, descriptions, copyright notices, and so on. An alternative to Literal is the new Localize control, listed in the last position under the Toolbox's Standard tab. If you declare it manually from the Source View, it's identical to a Literal. If, however, you work in the graphical designer, you'll notice that it doesn't have any properties listed in the Properties window beside the ID, not even a Text property. The way you fill it with text in the designer is to place the caret inside it and type the text directly. In Figure 11-7, you can see the test page with the description text under the title placed inside a Localize control: Note the caret symbol in the current insert position.
The declaration produced, after executing the Generate Local Resource command, is the following:
<asp:Localize ID="locDescription" runat="server" meta:resourcekey="locDescriptionResource1" Text="The page provides a demo... " />
As mentioned earlier, you can completely remove the Text property from the declaration, as it will be set from the localized resources at runtime.
Important |
When wrapping static content into a Localize or Literal control, it's advisable that you don't include HTML formatting tags in the control's Text, because that would go into the resource when the page is localized. If you were to pass that resource file to a nontechnical translator, she may not understand what those HTML tags are, and may modify them in some undesirable way. Because of this, if you have static content with HTML tags in the middle, then it may be wise to split it into multiple Localize controls, leaving the HTML formatting outside. |
Once you've modified your page with localization expressions for the various controls displaying static content, and you've created local or global resource files for the different languages you want to support, it's time to implement some way to enable users to change the page's language. One method is to read the Accept-Language HTTP header sent by the client to the server, which contains the array of cultures set in the browser's preferences, as shown in the dialog box in Figure 11-8.
In ASP.NET 1.x, you would set the current thread's CurrentCulture and CurrentUICulture properties to the first item of the UserLanguage array of the Request object, which would contain the first language in the list. You would execute this code in the Init or Load event of a page (typically a BasePage class, so that all others would inherit the same behavior), or from the Application's BeginRequest event, as shown below:
void Application_BeginRequest (Object sender, EventArgs e) { if (Request.UserLanguages.Length > 0) { CultureInfo culture = CultureInfo.CreateSpecificCulture( Request.UserLanguages[0]); Thread.CurrentThread.CurrentCulture = culture; Thread.CurrentThread.CurrentUICulture = culture; } }
In ASP.NET 2.0, however, you only need to set the culture and uiCulture attributes of the web.config file's <globalization> element to "auto", so that the user's favorite language will be retrieved and used automatically:
<configuration> <system.web> <globalization culture="auto" uiCulture="auto" /> ... </system.web> </configuration>
You can also specify these setting at the page-level, with the Culture and UICulture attributes of the @Page directive:
<%@ Page Culture="auto" UICulture="auto" ... %>
Figure 11-9 shows what the same page looks like when loaded for the American English or Italian language selected in the browser.
The "auto" setting only saves a few lines of code, but it's nice to have. In many situations, however, you'll prefer to set the culture by yourself anyway, because you'll need to extract the current culture from the user's profile, a session variable, or according to some other logic (I mentioned earlier that it's a good idea to let users specify their language of choice). If that's the case, the preceding code showing how to handle the application's BeginRequest is still valid, but you may actually prefer handling the application's PostAcquireRequestState event, so that the profile and session variables have been initialized already with the proper values. An even better solution is to override the page's new InitializeCulture method, to programmatically set the page's Culture and UICulture properties to the culture string (and not to a CultureInfo object as you do with the Thread properties). Here's an example:
protected override void InitializeCulture() { string culture = Helpers.GetCurrentCulture(); this.Culture = culture; this.UICulture = culture; }
Helpers.GetCurrentCulture is a custom function that would return something like "en-US" or "it-IT" after reading the desired current culture from somewhere. In the "Solution" section, we'll read the culture from the user's profile, and override this method in the custom BasePage class, so that all pages inherit this behavior without the need to replicate the code more than once.
Important |
The Generate Local Resource command automatically sets the Culture and UICulture attributes of the @Page directive to auto. If you use one of the application's events in the Global.asax file to programmatically set the current culture, you must remember to remove those attributes from the @Page directive after running the command on .aspx pages; otherwise, the automatic settings will override what you do by hand, because the page is parsed after the global.asax events you would typically use. (There is no such problem when generating localization resources for user controls, though, because the @Control directive doesn't have those page-level attributes, of course.) If you follow the approach of overriding the page's InitializeCulture event this isn't important, because this method is raised after the page is parsed, so your code will override the culture set by the framework, as desired. |