Wednesday, March 19, 2008

Localisation in ASP.NET: How to write multilingual applications

I been thinking of learning how to do this for a while now, number of reasons for that, my first language is not English, is Spanish, Uruguayan Spanish to be more precise, those that hear me from day to day can witness my struggles with English from time to time. Sometimes you wake up and words just flow, some other times, well.. that's not quite the case.

I always wondered how applications be developed that can then be easily extended to support other languages. Especially in this global village we live in today, in our Pacific region for example there are so many languages that is just mind blowing, from Oceania, South-East Asia, to Japan, the Koreas and even to the other side of the Pacific Ocean there is South and Central America.

Also by pure coincidance a couple of months we were asked to provide an English language version to a  charity website we have been maintaining at home for a number of years now called uruguayosunidos (I'm not responsible for a terrible spelling mistake on the homepage).  Problem is this site is just a plain html site where unfortunately we cannot use localisation. :-(

Enter ASP.NET 2.0 and resource files

One common approach in ancient times was to provide two versions of the same web, one for each language for example, one for Spanish another for English. This can clearly turn into a nightmarish scenario when maintaining the app, imagine!. The good news is that when creating an application with ASP.NET 2.0 or later you can use resource files to contain language specific content, eg one resx file can contain your French content, another can contain your English specific text etc.

ASP.NET supports two types of resources, local and global resources, local resources are scoped to a particular page while a global resource is scoped to the entire application. The equation is simple, if you want content specifically for a particular page, use local resources, now if you want the text that say what your 'Save' button should display through your whole app, then this should be in your global resource file.

To discover how this all works together I first put some labels and textboxes into an aspx page and got right into it. To get started, I just want to display a simple form where a user can enter her first name, last name and a button to submit the entries.

localisation_1

<table>
   <tr>
      <td><asp:Label ID="lblFirstName" runat="server" Text="Name"  /></td>
      <td><asp:TextBox ID="txtFirstName" runat="server"/></td>
      <td><asp:Label ID="lblLastName" runat="server" Text="Last Name" /></td>
      <td><asp:TextBox ID="txtLastName" runat="server"></td>
   </tr>
   <tr class="ButtonRow">
      <td colspan="4"><asp:Button ID="btnSave" runat="server" CssClass="Button" Text="Save" /></td>
   </tr>    
</table>

How to localise Page using Explicit Localisation Expressions:

So far nothing new. What I wanted to do was render those controls text's either in English, Spanish or Chinese according to the user's preferred language.

For that to happen first of all what I had to do was 'localise' the page, first I had to modify the controls to use resources's expressions. This expression is going to get the content from the resource files according to the user's language of choice. For example:

<asp:Label ID="lblFirstName" runat="server" Text='<%$ Resources:lblFirstName %>' />

The value of the Text property is now an "Explicit" resource expression, it is called Explicit because the property is set explicitly to a given value in the resource file for the preferred language.

Also you have to set the Page directive to include UICulture to auto, so when a user requests the page, a file matching her settings will be loaded automatically.

<%@ Page Language="C#" UICulture="auto"...

Associate a resource file to a Page

I created three resource files for this page and I then had to adde them to a special folder called App_LocalResources. You have to create this folder within the same folder as the page you want to localise.

Right-click where you want to add the folder and then Add ASP.NET folder->App_LocalResources,  once this is done, I was ready to add the resource files to the folder. To do this right click on the App_LocalResources --> Add New Item and selecting Resource File as shown below.

localisation_3

Important: To associate a resource file to a particular page, the naming convention is very very important.

It has to be in the format of page_Name.[Culture_Name].resx

See how above I have my Default.aspx page, I want to associate a resource file to deal with users who's language of choice is Spanish-Uruguayan (es-UY)

localisation_2

I added three files, a default resource file that is used in case none of the other resource files match the user's preference/settings (it is strongly recommended to always have a default resource file) and then the files for spanish and chinese simplified. Also note that in this example I have my Default.aspx page and that at the same level I added the App_LocalResources folder.

You can also add a 'neutral' culture resource file and name it in our case Default.aspx.en.resx, in this case the language is set to English without specifying the specific culture name as say AU for Australian, this is used if the user's preferred language is english but not say Australian english.

Resource Files:

Visual Studio has an editor for the resource files which enables you to enter name and value pairs as shown below..

localisation_4

Note that I have associated for my Uruguayan Spanish page the name of the properties to their corresponding values that I will then retrieve from my aspx page.

Resource files are nothing more than XML files that are then compiled by the framework into assemblies in the background! Open them in Notepad and see!

localisation_5

Once I 'localise' all the controls in the page, my aspx looked  like this.

<table>
    <tr>
       <td><asp:Label ID="lblFirstName" runat="server" Text='<%$ Resources:lblFirstName %>' /></td>
       <td><asp:TextBox ID="txtFirstName_Ex" runat="server"/></td>
       <td><asp:Label ID="lblLastName_Ex" runat="server" Text='<%$ Resources:lblLastName %>' /></td>
       <td><asp:TextBox ID="txtLastName_Ex" runat="server"/></td>
    </tr>
    <tr class="ButtonRow">
       <td colspan="4"><asp:Button ID="btnSave" runat="server" CssClass="Button" Text='<%$ Resources:btnSave %>' /></td>
    </tr>    
</table>

Now is up to the user's settings to see what gets loaded. I'm going to setup my preferred language in IE7 to Spanish and see what happens..

To do this: Tools->Internet Options -> Languages button at the bottom. I then added Spanish Uruguay and Moved it to the top to set it as my preferred language as shown below.

localisation_6

Now is all ready to go and loading the page should display my labels and button in Spanish.

localisation_7

And yeah! it worked, look how I associated the lblFirstName property in the Resource file:

 <asp:Label ID="lblFirstName" runat="server" Text='<%$ Resources:lblFirstName %>' />

localisation_8 

If I then set Chinese to my preferred language ( provided I have the Chinese culture resource file as shown below)

localisation_9

Thanks Jerry for the characters! I hope it means what we intend to ;-)

Then loading the page shows the correct content.

localisation_10

Using Implicit Localisation Expressions:

An alternative to explicit expressions is that you use implicit localisation which is very handy if you want to localise controls throughout your page with just one resource key.  The resource expression looks as follows:

<table>
   <tr>
     <td><asp:Label ID="lblFirstName" runat="server" Text="Name" meta:resourceKey="lblFirstName" /></td>
     <td><asp:TextBox ID="txtFirstName" runat="server"></asp:TextBox></td>
     <td><asp:Label ID="lblLastName" runat="server" Text="Last Name" meta:resourceKey="lblLastName" /></td>
     <td><asp:TextBox ID="txtLastName" runat="server"></asp:TextBox></td>
   </tr>
   <tr class="ButtonRow">
      <td colspan="4"><asp:Button ID="btnSave" runat="server" CssClass="Button" Text="Save" meta:resourceKey="btnSave" /></td>
   </tr>    
</table>

The labels and button include the meta:resourceKey property that represents a key in the local resource file. The entries are set to the Text property of the labels and button. I noted that you have to define all the meta resourceKey for each localisation resource file you have.

localisation_11

The advantages of using implicit localisation is that you can use the content for multiple controls, say you have 4+ 'Save' buttons through your page, all you need to do is associate a resource key to the controls. Also it is easier to localise existing apps, all that needs to be done is add meta:resourceKey="Property_Name" attributes  to your existing controls.

Note that is also possible to load resources programmatically by using GetLocalResourceObject(String name):Object and that you can also used it to format text, you can store values in the resource file such as "Welcome {0}!" and then at run time format the string as necessary. 

Global Resources:

While local resources are cool, their limitation is that they are relevant at a page level scope rather than at an application scope. A global resource can then be used through the app for you to setup the localised text. By convention, any resource that you want to be available to all your pages, controls etc should be in a global resource file.

To add global resource files you need to first add another special folder to your solution, this time the folder must be at the root of your application. To add it right-click on your application node --> Add ASP.NET folder --> App_GlobalResources.

Then you just add resource files as I did it for local page level, following the same naming convention as shown before for the language and culture.

Resource_File_Name.[Culture_Name].resx

localisation_12

I called this file Resource.es-UY.resx

Now the interesting thing comes when you want to retrieve the values from a global resource file. The sytnax is slightly different and it is as follows:

   <h2><asp:Literal ID="litWelcome" runat="server" Text='<%$ Resources:Resource,Welcome %>' /></h2>

With global resources you have to specify the name of the resource file and then the property you want to get to.

The result is:

localisation_13

It is also possible to retrieve values from global resources programmatically using GetGlobalResourceObject(string, string) method that takes two parameters, name of the global resource page and name of the property key you want to retrieve.

Because the .net framework converts global resources into compiled classes, you can use strongly typed expressions to retrieve values from the resource files. When you create a resource file, a new class is added to the Resource namespace for you to use! How good is that!!? The snippet  shown below with Visual Studio intellisense doing the damage for the above example. Wicked!

localisation_14

Last but not least there is the Localize control. It is used when you want to localise bigger pieces of text in your apps/pages. It works the same way as described above using meta:resourceKey at page level or using the global resource as in the last example. I can't see a super good use for it since it seems very similar to the Literal and Label controls. Jury still out.

<asp:Localize ID="elLocalisator" runat="server" Text='<%$ Resources:Resource,GoodMorning %>' />

That all!

This was a very interesting post to write, learned some tricks, there's still a lot to cover for example, how to format currency and DateTime according to culture, setting cultures programmatically or in web.config or page level. Maybe is there is interest we can do it later.

Hope it helps someone out there!

el#

Resources:

1 comment:

Frank said...

Is it possible to use implicit localisation with a Global Resource file. For example, if I have a global resource file storing all website control names is it also possible to specify extra control properties?

eg
Say in my global resx I have:
&ltdata name="Hello" xml:space="preserve"&gt&ltvalue&gtCiao&lt/value&gt&lt/data&gt

And my label uses it:
lblGreeting.Text = (string)GetGlobalResourceObject("resources.strings", "Hello");

Is there the possiblity to have extra key value pairs that can be applied to the text box?
Say if I also wanted to make the text bold?

kind regards,
Frank