On This Page
Overview and Description
MUI in Win32
Resource Handling in the .NET Framework
Multilingual content in Web Pages
Resource Handling in Console Applications
Before Windows 2000, a user who needed to access the same operating system in different languages was forced to either have several machines on his or her desk--each with a different language version--or to have one machine with some complicated process of allowing the user to start different language versions of an operating system on this same machine. In addition, global corporations needed to support several different language versions of an operating system across their regional offices. At the request of our customers to find a more cost-effective way of handling these two situations, Microsoft added MUI functionality to a particular version of Windows 2000 that was enhanced in the release of Windows XP. This functionality allows the UI language of the operating system to be changed, according to user preference, to one of many supported languages.
Availability: This MUI support was released as the Windows 2000 MultiLanguage Version and as the Windows XP Multilingual User Interface Pack. Although it is not available in consumer versions of Windows--Windows 95, Windows 98, Windows Millennium Edition (Me), and Windows XP Home Edition--it is available for the Windows 2000 Professional and the Windows 2000 Server family, and is often implemented in a Terminal Services environment. It is also available for English versions of Windows XP Professional, Windows XP Embedded, Windows .NET Server family, and the Windows XP Tablet PC Edition. Most of the MUI languages supported correspond to official localized versions of Windows 2000 and Windows XP.
Advantages of MUI: MUI allows large corporations to roll out, support, and maintain the same worldwide image with a single installation. Other benefits of MUI are that users of different languages can share the same workstation, and roaming users can take their localized UI from one workstation to another. For instance, one user might choose to see system menus, dialog boxes, and other text in Japanese, while another user logging onto the same system might prefer to see the corresponding text in Danish. (There are two ways to set a UI language. Either the user can select it [Figure 1], or an administrator can set it through Group Policy for organizational units. Changing the UI language requires the user to log off and log back on to the operating system in order for changes to take effect.)
Figure 1: The Language tab within Regional And Language Options property sheet, with a drop-down menu only available when MUI is installed
A Windows system with MUI set to a particular UI language will largely look and behave like the localized version, with some exceptions. Since MUI runs on top of the English version of Windows, from a feature and architectural point of view, localized versions of Windows are the same as the MUI version on English Windows XP. However, on localized versions:
The UI resources are fully localized.
Windows Setup information--such as the system locale, user locale, and keyboard layout--is customized for the specific language or country. With the MUI Pack, however, this is a policy setting. By default, installing MUI does not change locale settings of the English system where it is installed.
Additional country-specific printer drivers are added in the East Asian versions only.
There is support for upgrades from localized versions of Windows 95, Windows 98, Windows Me, or Windows 2000 to localized versions of Windows XP. MUI can be installed only on English versions of Windows; as long as cross-language upgrades are not supported, you cannot upgrade a localized operating system to an English version of Windows with MUI.
There is no difference between the actual translations on a particular localized version of Windows XP, for example, and those on an MUI version. This is because the same resources are used to create both the localized version and the MUI version. As a result, the MUI version is almost fully localized, apart from INF files, UI strings that are stored in the registry, components such as HyperTerminal that are not Unicode-based, and 16-bit applications in code page-based format. In fact, the percentage of localization coverage with MUI in Windows 2000 varies between 90 and 95 percent depending on the language.
In Windows 2000 MultiLanguage Version, there are many UI strings that appear in English, which are particularly noticeable with the Start menu. This is because the Start menu is populated directly by using the file names of folders and link files created at setup time. These names appear in English even if you are running an MUI system with a Japanese UI, since English was the original installation language.
In Windows XP, however, the practice of using English UI strings has been changed, resulting in a much greater degree of localization for the system user (with a localization coverage of about 97 percent). Much of the additional localization coverage in Windows XP is achieved through "MUI-enabling" Windows XP system modules and applications. MUI-enabling essentially entails:
Transferring UI strings from the registry to Windows resource files.
Preventing the use of hard-coded file paths when loading resource files, including Help files, so that an alternate resource path can be used to load resource files.
Providing special code in each component to install and load the UI resource if a nontraditional Win32 resource is used-such as a resource based on Extensible Markup Language (XML) or on HTML.
Top of page
The number of possible ways to implement an MUI solution in applications is endless, but each possibility is a variation of the same basic idea: creating a language neutral functional binary and one satellite resource DLL per target language.
Assuming that you have already separated your resources from your core functional language binary, creating an MUI solution is as easy as following these basic steps:
Detect the UI language of the operating system.
Enumerate all languages for which you have a localized satellite DLL.
Load the appropriate satellite DLL that matches the UI of the operating system.
Before examining the technical implementation of MUI in Win32 applications, it is important to underscore that the only safe assumption for a fallback language is the UI language of the operating system. If you do not offer a localized solution for that language, then instead of defaulting to a given language (such as English), you will need to enable the user to select a language. For example, if the UI of the operating system is set to French, and yet you don't have localized files for French, you might be tempted to default to English resource files. This might be an accepted solution in Canada, where French and English coexist. But if a French user lives in Belgium, for example, odds are that his or her second language is Dutch rather than English.
Detecting the system's UI language is handled differently across various versions of Windows. In Windows Me, Windows 2000, and the Windows XP family of products, the new GetUserDefaultUILanguage API returns the language ID for the current UI language of the operating system. (In Windows MUI versions, it is possible to have different users with different UI languages selected.) You can enumerate all available UI languages on the system by calling the EnumUILanguages API.
In Windows 95, Windows 98, and Windows 98 Second Edition, the UI language is stored in the registry at HKCU\Control Panel\Desktop\ResourceLocale. This key will return the language ID (LANGID) of the UI in hexadecimal (for example, 00000409 for English).
In Windows NT 3.5x, and Windows NT 4, because of the absence of a relevant API and consistent registry entries, the safest way to check the language of the operating system is to look at the version stamp of Ntdll.dll. The language of this DLL is the same as the language of the UI. The only exceptions to this approach are with Arabic, Hebrew, or Thai versions of Windows NT 4, where version stamping can help you detect an operating system enabled to support these languages. Thus the steps for checking the UI language of the operating system are:
The following code sample demonstrates how detection of the system's UI language would work on Windows NT 3.5x and Windows NT 4 platforms:; }
All these techniques for detecting the system's UI language return a language ID composed of a primary language and a sublanguage. Two exceptions to this rule are Traditional Chinese (predominant in Taiwan) and Simplified Chinese (predominant in mainland China), which share the same primary language and yet have different localized versions. The same is true for Brazilian Portuguese and European Portuguese. Now that you have identified the default UI language of the operating system, you need to load the satellite corresponding to that language. This process can be simplified by using generic naming conventions for your resource DLLs. If you only have one or two resource files, the suggested approach is to name them with the language ID, such as Myres409.dll for the 0x409 English (United States) satellite DLL. If your application has several resource files, a more practical solution is to create subdirectories named with the corresponding language IDs and place all satellite DLLs for the same language together.
Figure 2: Global.exe (a language neutral binary) and all its language dependent satellite DLLs
With this approach, by extracting the LANGID part of the file name and then using it to call GetLocaleInfo, you can find out the native name of each language resource file. The following code sample detects all language resource files currently available, makes sure that the languages are supported, and displays the list to a user:
int nIndex = 0; WIN32_FIND_DATAW wfd; HANDLE hFindFile; // The naming convention for resource DLLs is as follows: GRes[LANGID].dll. // Find all available resource DLLs in the current directory // but enumerate gres*.* files. hFindFile = FindFirstFile(TEXT("gres*.*"), &wfd); do{ LANGID wFileLang; TCHAR szLangName[32]; // Skip first four letters ("GRes")of file name and convert // the rest to a LANGID. wFileLang = (LANGID) _tcstoul(wfd.cFileName+4, NULL, 16); // Since more languages might be offered than the user has // support for, only list available and supported languages. if (IsValidLocale(wFileLang, LCID_INSTALLED)) { // Get the native language name. GetLocaleInfo(MAKELCID(wFileLang, SORT_DEFAULT), _ LOCALE_SNATIVELANGNAME, szLangName, 32) ; // Add the new language to the list of UI languages. SendDlgItemMessage(hDlg, IDC_LANGUAGES, CB_INSERTSTRING, _ nIndex, (LPARAM) szLangName); nIndex++ ; } } // Look for the following resource DLL: while (FindNextFile(hFindFile, &wfd) );
In the previous code sample, adding new languages is transparent to the executable, since all you need to do is to put a new language resource DLL in the directory. The application enumerates the new file, finds its native language name, and adds this name to the list of supported languages.
The Microsoft .NET Framework offers a whole new approach to the way resource files are created and loaded. Through the support offered by the CLR, you can provide an MUI solution in your .NET applications much more easily. Moreover, with the .NET Framework you can implement MUI in a way that is practically transparent. The CurrentUICulture property is a vital part of resource handling.
In the new naming conventions of the .NET Framework, the CurrentUICulture property of the CultureInfo class (from the System.Globalization namespace) is a per-thread setting. You can retrieve CurrentUICulture by querying the Thread.CurrentUICulture property, and you can change CurrentUICulture by setting Thread.CurrentUICulture. Thread.CurrentUICulture is used by the ResourceManager class to look up culture-specific resources at run time.
The CultureInfo class specifies a unique name for each culture based on the Request for Comments (RFC) 1766 standard. This standard uses the format <languagecode2>-<country/regioncode2>, where <languagecode2> is a lowercase two-letter culture code associated with a language, derived from International Organization for Standardization (ISO) 639-1, and where <country/regioncode2> is an uppercase two-letter subculture code derived from ISO 3166. The invariant culture-which is not associated with any particular language, country, or region-is the root of all cultures. A neutral culture is associated with a language and can be used for resources. (The neutral culture is the equivalent of the primary language ID in the Win32 programming paradigm.) For instance, "fr" is a neutral culture indicating that the language you are dealing with is French, but the neutral culture makes no provision about the actual location (country or region) in which French is being used (France, Belgium, Canada, and so on).
A specific culture is associated with both a language and a region and provides formatting-specific information for that particular location. So, for example, "fr-FR" indicates that you are dealing with the French language and with regional settings that are appropriate for France.
Figure 3 summarizes this hierarchy, using German and English as examples. Within the neutral culture of German, for instance, are German in Austria, Switzerland, Germany, Liechtenstein, and Luxembourg. The neutral cultures, German and English, are both part of the invariant culture.
Figure 3: Hierarchy of invariant, neutral, and specific cultures
The CurrentUICulture property is set by the framework when the application starts so that it matches the user's default UI language. Alternatively, you can set CurrentUICulture explicitly in your application's code. The following code example sets the CurrentUICulture property to the neutral culture "de" for German.
Thread.CurrentThread.CurrentUICulture = new CultureInfo("de");
You can also set the CurrentUICulture property to a specific culture. The following code example sets the CurrentUICulture property to the specific culture "de-DE" for German in Germany:
Thread.CurrentThread.CurrentUICulture = new CultureInfo("de-DE");
As you have seen, the CurrentUICulture property can help you handle resources in a culture-specific manner. The following sections will show how resources are created and loaded.
As in the Win32 programming model, in the .NET Framework your resources should be separated from the rest of your code. Resource files in .NET are created within .resx files; these files consist of XML-based entries that specify objects and strings within. A .resx file contains a standard set of header information, which describes the format of the resource entries and specifies the versioning information for the XML that is used to parse the data. Following the header information, each entry is described as a name/value pair..
The resource generation process is straightforward and includes three steps (although the first and second steps are optional):
Figure 4: The .NET resource generation process
The ResourceManager class provides convenient access to culturally appropriate resources at run time. This class manages multiple resources from a common source that has a particular root name, like the hierarchy shown earlier in Figure 4. ResourceManager objects provide fallback resource lookup to region-independent and neutral cultures when specific localized resources are not provided. Resource-loading requests are based on the culture that is associated with the current thread. This culture can be set through the Thread.CurrentThread.CurrentUICulture property.
The ResourceManager class provides two methods, GetString and GetObject, which enable an application to load either a string resource or any object (that can be serialized) from an assembly. Both the GetString and the GetObject methods support two types of overloads:
If the resource is not localized for the culture that is requested (either explicitly through the second overload or implicitly, by using CurrentUICulture), the lookup will fall back using the culture's Parent property, stopping after looking in the default resources.
When you package your application's resources, you must name them using the resource-naming conventions that the CLR expects. The run time identifies a resource by its culture signature, or name, as discussed in "CurrentUICulture" earlier in this chapter. The .NET Framework uses a hub and spoke model to package and deploy resources. This model requires that you place resources in specific locations, so that they can be easily located and used. If you do not compile and name resources as expected, or if you do not place them in the correct locations, the CLR will not be able to locate them.
If your application includes resources for specific cultures (such as "de-DE"), place each specific culture in its own directory. Do not place specific cultures in subdirectories of their respective neutral culture's directory. (For example, do not put the "de-DE" resources in the "de" folder.) If you do so, the application will not be able to find the localized resource.
The hub and spoke model for packaging and deploying resources uses a fallback process to locate appropriate resources. If an application user requests a ResourceSet that is unavailable, the CLR searches the hierarchy of cultures looking for an appropriate fallback resource that most closely matches the user's request, and raises an exception only as a last resort. At each level of the hierarchy, if an appropriate resource is found, the run time uses it. If the resource is not found, the search continues at the next level.
For instance, if an application is localized in French using the "fr-FR" culture for its resources satellite assembly, and if the application is looking for a string that cannot be found in "fr-FR," it will then fall back to the French culture (if available) and try to find the string in the "fr" satellite assembly. If the string is not available at that level, it will then fall back to the default resources in the main assembly. Finally, if the string resource is not found at that level, an exception will be raised. Figure 5 illustrates the fallback process.
Figure 5: The .NET resource fallback mechanism
Offering Web sites that provide content in the user's preferred language follows the same principles as providing multilingual content for Win32 applications:
Internet Explorer offers its own MUI solution (called "pluggable UI," which also uses satellite DLLs), where the browser UI language on English Windows 98, for example, can be set to Swedish. On Windows 2000 and Windows XP--which also offer their own MUI solution for the operating system UI--if the browser's MUI files are available, it tries to keep its UI language synchronized with the UI language of the operating system to avoid confusion.
Dynamic generation of language-specific Web interfaces from XML data is not hard-at least, it is not any harder than using XML to store UI data for a single language. The traditional model is to create an XML file containing all necessary data, bind the XML data to an HTML object on the page or apply an XSL transformation-and that's it! The client gets a nicely formatted page and your data, which is isolated from the code and UI, is easy to maintain. A multilingual scenario does not require much change to this traditional model. You will basically need to replace one single-language XML file with a set of language-specific files, which are loaded according to the settings of the client. An important step in building a UI according to the user's preferences is getting those preferences in the first place. There are several methods you can use:
The advantage of the last two methods is that you can use them at a session's initialization step-for example, in the Session_OnStart procedure of Global.asa. However, these two solutions might be less flexible than the first method, in which you query values in a client-side script. In effect, the last two methods are equivalent to making an educated guess about the user's preferred language. Suppose the browser language is set to Finnish and your Web pages are not available in that language. You might default to English, not realizing that the user's second language is Russian, not English. Furthermore, there is not much advantage to the last two methods if the Session object is disabled in the first place.
Rather than selecting one particular algorithm as the preferred solution for all scenarios, it is wiser to base your solution upon the specific business case at hand. As you will see in the the sample code that follows, the decision on which language to use to display content is isolated within the GetSessionLanguage() function. In order to make GetSessionLanguage() universal, the function returns the language ID. However, it would be better to set the Session's UI language variable. As a general rule, you should set a language for each user's session. This way you allow different languages for different users, and you can confine your decision about which UI language to a single instance-within the initialization code. Some applications might avoid using Session states, in which case a language ID returned as a string comes in handy. Note also that GetSessionLanguage cannot be made completely universal; it will depend on the naming conventions you adopt for the XML files in your product, the set of languages you have localized into, and the default language of your application. The following code illustrates one mechanism possible for dynamic construction of a language-specific UI. To make things more general, it is not specified whether the code is executed in a Global.asa initialization of a session or later. Error handling was also removed from the code, but you should be able to handle errors that might arise, such as when the XML file for a particular language cannot be found.
<%...%>
The XML file can contain all the information needed for on-the-fly generation of localized content. If you need to apply directionality to your localized pages, use language-specific graphics or URLs. In the previous code sample, if English, French, German, and Japanese resources are defined, the structure of the "resource" tree looks like the one shown in Figure 6.
The assumption is that the same information is kept in each language version of the XML resources, thus preserving the same schema among the languages. With this model you can use the same XSL transformation on all of the files, which will simplify your code. Adding new language resources becomes easy as well.
Figure 6: Structure of the resource tree
A Win32-based console application finds and loads resources in the same manner as any Win32 code. However, when handling resources in console (or text-mode) code, you must take into account some specifics of the Windows console subsystem. An obvious requirement for any code is that it produce a UI that is easily readable. Additionally, the UI language and settings must be based on the user's preferences. Unfortunately, there can be no warranty against the situation when the UI language cannot be represented in the console output code page. If this happens, loading and displaying resources in the language of the user's UI does no good-the console will not be able to display the output.
The easiest way to prevent the UI from breaking is to make resource loading dependent upon the console code page. Picking which language resource files to load can be tricky, though. One possible method, which can be applied to many scenarios, involves the following steps:
The advantage of loading English resources is that they can always be displayed; but you should be aware that, by doing so, you are losing cultural accuracy. You can also select another approach, such as setting the language that you are going to load to match the console code page, regardless of the UI language. The catch here is that the UI language is a per-user setting, and the console code page follows the system locale; the user cannot always change the system locale. Remember also that setting the ThreadLocale affects locale-sensitive operations such as date formatting, and should be used cautiously. If you use multilingual resource sections and change the thread-locale value to select the language to load, do it only for the time resources are loaded, and restore the original value after that. Console applications depend heavily on text resources, often building the output at run time from string table entries, using the functions of the printf family for run-time parameter substitution. In many cases, this practice makes localizability of the code difficult. When multiple parameters have to be substituted at run time, their relative positions might differ from one language to another, but the order of the printf arguments cannot be changed without code modifications. Using message tables together with FormatMessage API eliminates this problem.
Message tables are a Win32 resource that uses sequential numbers rather than escape letters to mark replacement parameters, making it convenient to store messages that contain several (up to 99) replacement parameters. The Format-Message API function will substitute variables according to each placemarker's numeric label and not according to its position in the string. Localizers can freely change a string's word order, and FormatMessage will still return correct results.
Message tables are defined using message compiler (.mc) files and are compiled into resource files using the message compiler, mc.exe. The format of the message table is designed so that multilingual error messages are easier to interpret. To accomplish this, the message table includes flags for error severity and for the facility that caused an error. For instance, the message text file contains a header that defines names and language identifiers used by the message definitions in the body of the file. The header can contain the following statements:
These flags are optional and can be omitted if the message table is used to store ordinary text resources. The following code sample illustrates how multilingual message tables with no "redundant flags" are created:
//.
The following code shows how the previous message table can be used in a program. The language ID here is set to NULL. The resources will be loaded according to the ThreadLocale settings.
wchar_t lpBuf[60];LPVOID lppArgs[10];DWORD len = FormatMessage(FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_ARGUMENT_ARRAY, NULL, // message source - 0 stands for current module idMsg, // ID of the message to be loaded NULL, // Language ID lpBuf, // Destination buffer sizeof(lpBuf)/sizeof(TCHAR), lppArgs); // Array of message inserts - list of strings