f there's one topic in software development that has never gotten the respect it deserves, it's internationalization. How can this be when the Internet allows for the worldwide distribution of software to a population where only 10 percent of the people use English as their primary language? The issue was overlooked in the early days of the PC since most users were also part-time programmers and most programming languages are based on a subset of English. At the time, the majority of users could deal with English-only applications even when it wasn't their native language. Now that the PC has become so widely used internationally, users are becoming less tolerant of applications that don't communicate with them in their own language. With the release of the .NET Framework, Microsoft® has both simplified and enhanced the ability for developers to create global-ready applications. I'll look at ways to deal with international issues using .NET technologies and I'll use Visual Studio® .NET tools to build international Windows® Forms applications. You can download the sample applications from the the link at the top of this article. The code in this article is in C#; however, there is also a version in Visual Basic® .NET in the download.
In Win32®, a locale identifier (LCID) is used to identify and retrieve information about a locale. This LCID is a 32-bit unsigned integer that's divided into four parts. The first two parts identify the language and sublanguage while the last two specify unique sorting orders for text strings. Windows has a default LCID that is assigned when installing the system; each user inherits this default LCID, which propagates to all the threads that are created on the user's behalf. Users can change their default LCID using Regional Settings in the Control Panel, and an application can also change the LCID programmatically using API functions. LCIDs and the National Language Support (NLS) are contained in the .NET culture-related namespaces. The .NET Framework supports interoperability with NLS-based software by exposing an LCID property of the CultureInfo class, which I'll discuss later. The term "culture" in .NET refers to a user's language and location. This culture identifier takes the form of language code-country or region code. The actual codes used in these two positions are defined by two ISO standards: ISO 639, "Code for the representation of names and languages," and ISO 3166, "Codes for the representation of names and countries." (For more information, see http://www.iso.ch/iso/en/ISOOnline.frontpage.) Some of these codes are shown in Figure 1. A complete list is provided in the .NET Framework SDK documentation. Cultures that have only a language code are referred to as neutral or language-only, while cultures that also supply a region code are referred to as specific cultures. It is important to recognize this difference since aspects of internationalization use different culture types. For example, the process of resource selection is only concerned with the language culture (controlling which language of an error message to display) while other cultural-specific features like currency symbol selection and date formatting require a specific culture. In most cases, the culture for an application's user interface is set either implicitly using the Windows regional settings or explicitly through code. As you'll see when I discuss my Culture Explorer sample application, the current culture is specified on a per-thread basis. Each thread inherits the default Windows culture unless you change it by accessing the current thread through services provided by the System.Threading namespace.
The sample application I developed demonstrates ways to manipulate the current culture at run time. My objective was to provide an easy way to understand the difference between different culture settings. It also allows you to see what cultures are available in .NET and how they affect different types of information. The construction of the Culture Explorer application started with TreeView and ListView controls centered by the popular splitter control. To make things easier to follow, I set the Sorted property of the TreeView to true and the HideSelection property to false. In the ListView, I set the View to Details and the GridLines property to true. Then I added some items to the ListView that I can manipulate at run time. Figure 2 shows the application at design time. In addition to the items shown in the ListView, I also added an empty subitem to each of the items in the item collection where I can insert additional information about the selected culture.Figure 2 Culture Explorer App at Design Time Once the GUI portion was complete, I started writing code. First I added a using clause for each of the namespaces I used in my sample:
using System.Globalization;using System.Threading;
I used the Globalization namespace extensively since this is where most of the classes that deal with internationalization issues are kept. I only used the services it provides to change the culture at run time. You can let the culture be selected implicitly based on the underlying Windows settings. My first objective was to populate the TreeView control with all of the possible cultures. To do this, I used the code shown in Figure 3 for the Load event of the form to generate the output shown in Figure 4. This shows all of the neutral and specific cultures available in .NET and those that are installed in your copy of Windows; any non-available cultures would be greyed out.Figure 4 Culture Explorer Output The code in Figure 3 is reasonably straightforward, but I'll explain aspects of it. The first foreach statement simply adds all of the neutral (language-only) cultures as root-level nodes in the tree:
foreach (CultureInfo CultureX in CultureInfo.GetCultures (CultureTypes.NeutralCultures))
foreach (CultureInfo CultureX in
An instance of the CultureInfo class is used in all cases to represent an individual culture (neutral or specific) in .NET. I then used the static GetCultures method of the CultureInfo class to retrieve all of the neutral cultures filtered using the CultureTypes enumeration. In addition to Neutral Cultures, other choices include AllCultures, InstalledWin32Cultures, and SpecificCultures. With this information, I built the first level of the tree by adding each culture in turn. I constructed a human-readable string for displaying the culture and then added the culture itself to the tag property of the node. I can use the instance of the CultureInfo class that I stored here to actually change the current culture of the application at run time. The next foreach statement in Figure 3 iterates through all of the specific cultures and adds each to its parent neutral culture in the tree. I've added some code to grey out the specific cultures that are not installed on the current version of Windows. This is all well and good, but the next event handler makes the application a bit more useful. I then added the code for handling an individual culture—a very simple function (see Figure 5). The first step was to retrieve the instance of CultureInfo class stored in the node's Tag property:
selectedCulture = (CultureInfo)treeView1.SelectedNode.Tag;
Once I had that data, I needed to change the application's current culture so I could see the effects on the representative data in the ListView control. One thing I needed to look out for here was that I could only set the current culture of a thread to a specific culture because using a neutral culture threw an exception. Therefore, I made the appropriate check and then used the following method to change the current culture of the thread on which the application was running:
Thread.CurrentThread.CurrentCulture = selectedCulture;
Another instance of a CultureInfo class, associated with the current thread, was exposed as the CurrentUICulture property. While the CurrentCulture property affected the type of data I showed in this application, the CurrentUICulture property is only concerned with the language code and is used by the ResourceManager to select the appropriate resources to display. The remaining code in Figure 5 adds the appropriate data to each of the ListView items. If you run the code, you should see results similar to Figure 6.Figure 6 Cultures Shown in Culture Explorer You may notice that the "not installed" cultures are still selectable, but some items may not display properly if the language's characters are unavailable. I chose Saudi Arabia since it looks markedly different and the characters were available on my system. Some differences are obvious while others are a bit more subtle. For example, notice the characters used for the thousands separator and decimal point in the number data types.
Before I discuss what Visual Studio .NET specifically provides to help build global-ready applications, I am going to look at two other issues related to internationalization. The first is bidirectional (BIDI) languages that are not read left to right. For example, Arabic and Hebrew are read from right to left, except for numbers and foreign words that are presented in a left-to-right manner. Even more different are traditional forms of Chinese and Japanese that are read as columns from top to bottom and arranged from right to left. Although this style is seldom used in Asian computer applications, it is a good example of what you should be aware of when you are building global-ready applications. There is some support in both Windows and .NET for BIDI languages but a discussion of that support is beyond the scope of this article. The second special issue relates to dealing with currency values. As you probably noticed with my Culture Explorer application, the appropriate currency symbol was automatically supplied when I changed the current culture. For a personal finance application, this might be fine since the user will be the one supplying all the values. Imagine an application such as an auction site that receives currency-style data from the Internet. In that case, the seller may post a starting price in their home currency but the user may be in another country. The default behavior in an international desktop application would be to display this currency value with a symbol appropriate for the user's current culture. No currency conversion would be performed. As you can imagine, this may have some very undesirable results. I'll examine one way to solve this common problem. The Globalization namespace in the .NET Framework defines a NumberFormatInfo class that defines how specific types of numeric data are formatted. An instance of this class is exposed as a property of the CultureInfo class you saw earlier. With one feature of this object you can override the currency symbol for the current culture:
CultureInfo.NumberFormat.CurrenySymbol = "$";
This statement causes all future currency values displayed by the application to use "$" as the currency symbol regardless of the current culture.
Visual Studio .NET can help you build a global-ready forms-based desktop application. From a project management standpoint, building an international application is usually a two-step process. The first step is building the application that can be easily extended to support more than the default language. The second step consists of actually translating portions of the application's UI into one or more languages. Visual Studio .NET allows all the steps for building resource files which contain data in a variety of languages to be completed within the form designer, so I didn't need to write a single line of code. For my sample application, I created a new C# Windows-based application. Then I added a pair of buttons. The names of the components didn't actually matter in this case, so I just left the defaults (see Figure 7).Figure 7 Sample App I've labeled the two buttons "Hello" and "Goodbye" and these will need to be translated into whatever language the application supports. My goal was to make this demo application work for both American and German customers. I assumed that the developer had no idea what the translation would be. The developer should be concerned with making things as easy as possible for the translator by creating an application that was "localizable." Thus far what I've shown you concerns globalization or creating an application that will format certain data differently depending on the culture. Localization deals with creating an application that will display completely different data depending on the culture. Conveniently, there is a Boolean property named Localizable but I won't change it quite yet (see Figure 8).Figure 8 Form Properties To see what Visual Studio .NET is actually doing, go to the Solution Explorer window and make sure the "Show All Files" toggle button is pressed. Once you can see all of the files, expand Form1.cs to find Form1.resx. This is the resource file for the form, an XML-based file that is compiled into a binary resource file in my application. When I change the form's Localizable property to True, nothing happens. When I change the value of the form's Language property to German, an additional resource file is added to the form named Form1.de.resx, with "de" indicating Deutsch, or German (see Figure 9).Figure 9 New Resource File There are now two separate visual instances of the form. The one being edited in the designer depends on which language is selected in the form's Language property. For this particular example, I'll translate the text of the buttons in Visual Studio .NET. Later I will show you another tool that allows a translator to carry out the same task without having to use Visual Studio .NET. With German selected for the form's language, I can change the text of the buttons, but unfortunately "auf Wiedersehen" doesn't all fit on the button (see Figure 10). This is actually a very common problem when using string-based resources and substituting them at run time. Visual Studio .NET provides a very simple solution—manually resizing the button. Both the new text and the new size of the button exist only in the German version of the form. If you select Default for the form's Language property, you'll see the buttons revert to their original appearance. There is still only one copy of the code for the form.Figure 10 Size Problem Next I added the following code to the form's constructor before the call to InitializeComponent is executed:
System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("de-AT");
If you try to change the culture after InitializeComponent or somewhere else (for example, in the form's Load event) it looks like nothing happens. In fact, the culture will have changed. I used the entire namespace path in the code instead of including the using System.Threading and using System.Globalization statements as I did before. I also used CurrentUICulture instead of CurrentCulture. As a side note, CurrentCulture and CurrentUICulture are also available by using System.Globalization.CultureInfo.CurrentCulture. This is a read-only instance of the CultureInfo class and cannot be used to change the current culture. Finally, it's important to remember that the code pertaining to the current culture is only necessary for testing purposes since the current culture would be set automatically based on settings in Windows for a real-world application.
In my sample application, you saw two different versions of a form, each for a different culture. I modified the German version by translating the text and resizing the button. It is more common that a different person will be doing the translating and you may prefer not to have these individuals working on your UI inside of a Visual Studio .NET environment. It would be much better if they could perform their tasks without having access to the source code. You may also need to create several language-specific versions of a form to give each version to the appropriate translator so work can be done in parallel. This way, it would be very easy to bring the changes back into a project upon completion. Fortunately, a tool in the .NET SDK helps to accomplish these goals—the Windows Resource Localization Editor, which is located in the bin directory of the SDK as WinRes.exe. This application is basically a standalone version of the Visual Studio .NET form designer. Figure 11 shows WinRes after opening Form1.resx from the previous example.Figure 11 WinRes Displaying Form1.resx This tool allows you to build your application and send the .resx file to as many translators as necessary. Once the translators receive the .resx file, they can use WinRes (without having Visual Studio .NET installed) both to translate the text and modify the form's layout to accommodate the new text. Once the translator modifies a form for a particular language, it must be saved as a new .resx file and sent back to be inserted into the project.
Next I'll take a look at how these resources are packaged. With my calendar application open, you can expand the bin directory in the Solution Explorer (with "Show All Files" selected) and you will see an additional directory under the build directory (either Debug or Release). There is also a directory for each additional culture I've added for my form. In this case, there is just one directory named "de." In that directory is the file ProjectName.resources.dll. For every new culture that I add, there will be a subdirectory with the same name as the culture identifier and a file with the name ProjectName.resources.dll. This is the structure and naming convention that must be used when the application is deployed. These dlls are actually referred to as satellite assemblies and the runtime follows a defined sequence to locate and use the appropriate resources, a process known as Resource Fallback. In the previous example, the culture was set to de-AT (German-Austrian). The runtime looks for any resources provided for this culture in the Global Assembly Cache (GAC). If not found there, the runtime checks in the directory of the currently executing assembly for a directory with the name "de-AT." In my example, the runtime was not successful in either location. The next search is of the GAC for a resource assembly that matches the parent of the current culture. In my case, this would be the neutral culture "de." The runtime must also search for a directory under the executing assembly with a name matching that of the parent of the current culture (de). This time the runtime was successful and it used the resources that are contained in the satellite assembly under the "de" folder. If I run this application on a system where the current culture is one for which I have provided no resources—Chinese, for example—then the runtime would not succeed in finding an appropriate resource assembly even during this fourth step of the search. In this case, the runtime would use the default culture. The default culture is the only culture which is actually compiled into the main assembly—English (en), in my particular case. One major advantage to this approach of packaging and retrieving resources is the ability to add (or replace) resources for cultures after the application has been deployed. The new satellite assemblies need to be added in the correct subdirectory or installed into the GAC on the user's machine and no recompilation of any code is necessary.
The .NET Framework and Common Language Runtime make the construction of international applications a very easy endeavor. Using Visual Studio .NET and WinRes, it's possible to create Windows Forms applications which can present an adaptive user interface depending on the language settings of the user's machine. In addition, the model of resources contained in satellite assemblies and the fallback mechanism built into the runtime allow for developers to build international-ready applications that can become global applications without changing code or deploying a completely new version of the application.
More MSDN Magazine Blog entries >
Browse All MSDN Magazines
Subscribe to MSDN Flash newsletter
Receive the MSDN Flash e-mail newsletter every other week, with news and information personalized to your interests and areas of focus.