Writing Win32 Multilingual User Interface Applications
Windows 2000 set new standards with enhanced support for the global market, providing international features such as full Unicode support, built-in support for hundreds of languages and mirroring technology for right-to-left languages. Several articles explaining how to write Unicode, Complex Script, and Mirrored applications for Windows 2000 have already been published in the Microsoft Systems Journal (MSJ).
Windows 2000 Multilanguage User Interface (also known as MUI) is one of the most useful international features in Windows 2000, allowing users to set and change the language of the system User Interface (UI) into any of the localized languages in which Windows 2000 has been released.
This article explains how to provide switchable UI functionality in your own applications. In this article you will:
Learn why you should consider investing in writing multilingual applications.
See examples of three different implementations of Multilingual User Interfaces.
Discover recommended best practices, along with guidelines and code samples.
.gif)
On This Page
Why bother writing multilingual applications?
Why make it Unicode?
Implementing a Multilingual User Interface
Summary
Glossary
Why bother writing multilingual applications?
There are several reasons for writing multilingual applications. In addition to the obvious reason – to increase revenue and distribution of software by entering into international markets (this is, after all, the era of the World-Wide Web) – there are two other convincing arguments:
Ship a single binary. Shipping one core functionality binary to all platforms (and for all different language versions) reduces the hassle and cost of development significantly, eliminating the need for compilation conditions and maintenance of separate source code for each language. Microsoft Windows 2000, Microsoft Office 2000, and Microsoft Internet Explorer 5.0 are all single binaries – the same core gdi32.dll, for example, ships on the English US, Japanese and Arabic versions of Windows 2000. A potential update to this module in future Service Packs can be applied to all languages with no additional engineering effort.
Avoid being your own competitor. Delaying the shipment of different language versions of your software can wreak havoc with your customers’ deployments, delaying your revenue. Making customers wait a few months between the release of the English version 1.0 of an application and the German version 1.0 might not seem like a big deal. But many customers wait until the release of the first update to an application before deploying. Add the release delta for the update to the release delta from the original release and you may find you’re delaying your customers deployments a lot longer than you first realized. Ensuring that your applications are multilingual can help avoid this.
Top of page
Why make it Unicode?
It’s important to realize that no matter which approach you use to ship a localized product, your application must be fully Unicode enabled. Implementing Unicode support frees you from having to worry about the language and codepage support of the platform your application runs on. Unicode is a 16-bit character encoding capable of representing most of the languages in common use throughout the world (a far cry from the old 8-bit character encodings such as ANSI which restrict language support to approximately 220 different characters). A Unicode-enabled application can process and display characters in any of these languages. For more information about implementing Unicode and shipping a single binary that will run on both Windows 9x and Windows NT, see the article on developing applications using Microsoft Layer for Unicode.
Windows 2000 supports legacy ANSI applications through the System Locale setting, configured from the Regional Options control panel (figure 1). This means that the behavior of ANSI-based applications is entirely dependent on the system setting (figure 2). A much better approach is to use Unicode character encodings throughout.
.jpg)
Figure 1: The system locale is configured through the Set default… button in the Regional Options control panel.
|
.jpg)
Figure 2: ANSI localized message box running on a Windows 2000 system with the System locale not matching the application language.
|
Top of page
Implementing a Multilingual User Interface
There are generally three methods used to implement Multilingual User Interface binaries:
Language dependent binary with built in resources
One core binary with one international resource DLL
One core binary with one resource DLL per target language
Figure 3 illustrates the file distribution of an international application using each of these techniques and targeting the English, German and Japanese languages.
.jpg)
Language dependent binary
.jpg)
One core binary and an international resource DLL
.jpg)
Language based satellite DLLs
Figure 3: File distribution comparison
The following sections discuss the advantages, disadvantages and technical limitations of each of these implementations. An important assumption is made here: a single core binary is to be shipped, and the core functionality of the application is to be the same regardless of the language of the user interface. This should be the ultimate goal in any implementation of a multilingual user interface.
Method 1: Language-dependent binary
This traditional approach involves compiling one binary, containing both source code and resources. A separate binary is required for each of the languages targeted.
| Summary of pros and cons for Method 1 (Language dependent binary) |
| Advantages | Disadvantages |
| Separate source tree required for each language Resource changes require a full binary compilation Different instances of the application required to switch UI Waste of disk space: multiple copies of core binary
|
The disadvantages of language-dependent binaries are such that this approach is now considered obsolete. The two approaches discussed below are considered better for UI-switching applications.
Method 2: One resource DLL for all languages
The main idea behind this approach is to separate out the resources from the source code, creating a resource-only DLL containing all the localized resources for all targeted languages. Multiple copies of the same resource ID are defined in an RC file under different language tags. In the sample below, string ID IDS_ENUMSTRTEST is defined for French and English.
// French (France) resources
#ifdef _WIN32LANGUAGE LANG_FRENCH, SUBLANG_FRENCH
#pragma code_page(1252)
#endif //_WIN32
// String Table
STRINGTABLE DISCARDABLE
BEGINIDS_ENUMSTRTEST "Cette phrase est en français...(France)"
END
#endif // French (France) resources
// English (U.S.) resources
#ifdef _WIN32
LANGUAGE LANG_ENGLISH,SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif _win32
// String TableSTRINGTABLE DISCARDABLE
BEGINIDS_ENUMSTRTEST "This is an English string...(USA)"
END
#endif // English (U.S.) resources
To access the resources at run time, the resource DLL is loaded through the LoadLibrary API. EnumResourceLanguages can then be used to find the list of available languages for a given control/resource, and FindResourceEx to determine the location of the resource with the specified type, name, and language. Displaying a given language is then just a matter of selecting the right resources within the DLL. Language switching can be implemented by re-loading the newly selected resources and refreshing the client area.
It’s important to note that the Windows resource loader always defaults to the current user locale (the thread locale gets inherited from the currently logged in user's user locale - see Figure 1). GetThreadLocale allows querying of thread locale. In this method, predefined resource loading APIs (LoadIcon, LoadString, LoadCursor…) always return resources associated with this locale. For example, in the resource sample above, the French resources cannot be loaded by using LoadString if the system locale is set to English (0x0409). Actually this is not entirely true: a thread locale, once inherited – upon thread creation – is independent from the user locale, which means in this case the thread locale can be changed to French (0x040C) by using SetThreadLocale and calling LoadString to get the French string back.
// if our thread locale is English to start with…
g_hInst = LoadLibrary(_TEXT(“intl_res.dll”));
LoadString(g_hInst, IDS_ENUMSTRTEST,g_szTemp, MAX_STR);
// g_szTemp would then point to the English resources
// changing our thread locale to French.
// Always make sure that French is in fact one of the
// valid languages returned by EnumResourceLanguages()
// Save a copy of the current thread-locale to set back later
Lcid = GetThreadLocale();SetThreadLocale(MAKELCID(0x040c, SORT_DEFAULT));
LoadString(g_hInst, IDS_ENUMSTRTEST, g_szTemp, MAX_STR);
// g_szTemp would then point to the French resources
The catch here is that if the thread locale is the same as the currently selected user locale, system’s resource loader will by default use the language ID 0 (neutral). If the desired resource is defined as a neutral language, then this value will be returned. Otherwise, all of the language resources will be enumerated (in language ID order) and the first matching resource ID – regardless of its language – will be returned.
To clarify, consider an example (using our English – US and French – France resources) in which the user locale is set to Japanese, then: our original thread locale is Japanese (0x0411). The first call to LoadString will return English resources since 0x0411 version if our string ID cannot be found and English (0x0409) is enumerated before French (0x40C). Now, if the user locale to start with is French: our original thread locale is French (0x040C). Since the user locale and thread locale match, the system resource loader will use language ID 0 and will start enumerating resources and will once again return the English version!
The best way around this is to give up changing the thread locale in the first place. Resources can be manually loaded from within multilingual resource files by making calls to FindResourceEx . Another workaround is to define the resources within an owner-defined language tag. In the RC example above, the English section was defined as:
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
This prevents the thread locale from being switched back to English US. However, defining the resources as:
LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL
will guarantee the success of the SetThreadLocale call since there is no matching user locale for this language ID and the predefined resource loader APIs can still be used.
| Summary of pros and cons for Method 2 (One resource DLL for all languages) |
| Advantages | Disadvantages |
| Difficult to update with new languages Difficult to install a subset of UI languages Unsupported languages waste memory and disk space No straightforward way to implement direct resource loading interfaces (LoadMenu, LoadString, etc.) Cannot change thread locale on Windows 9x
|
Method 3: One resource DLL per target language
This is an expansion of the previous technique. Instead of a single DLL containing all languages, a separate DLL is created per language. This approach is the one that Microsoft Office 2000, Microsoft Internet Explorer 5.x, and Microsoft Windows 2000 are using. The technique is named differently depending on each product’s implementation but satellite DLLs is a commonly used name for this approach.
This technique again requires the resource DLL to be loaded through the LoadLibrary API. This time, however, the appropriate language DLL must be selected. Typically it is safe to assume that the Windows UI language is the user’s preferred language and the application can be started by loading this language resource. Then, when the user selects another language, the current language can be freed and a new one loaded. Of course, this approach requires recreating windows and initializing dialogs with the newly loaded resources.
Detecting the system’s UI language is handled differently across the various the versions of Windows:
Under Windows 2000, the GetUserDefaultUILanguage API allows you to find the UI language of the current user (note that in the Windows 2000 MultiLanguage Version it is possible to have different users with different UI languages selected).
Under Windows 95, Windows 98, and Windows 98 SE, 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 (e.g. 00000409 for English).
Under Windows NT 3.5x and Windows NT 4.0, the absence of a relevant API and consistent registry entries means that the safest way to check the language of the OS is to check the version stamp of NTDLL.DLL. The language of this file is the same as the language of the User Interface. Again, the only exception is Arabic, Hebrew, and Thai where the version stamping can help you to detect an enabled OS. Thus the steps are:
Load the ntdll.dll file
Enumerate languages in it's version-stamping resource
If there is more than one language available, it's the non-English-US language
If you find only English US, you still might be dealing with enabled languages and should check against the active code page.
Since the language detection APIs and registry keys return the UI language’s LangID, it makes a lot of sense to name satellite DLLs correspondingly: Instead of naming files res_eng.dll or res_en.dll for English, use res409.dll.
The following code sample shows how to detect the UI language and load the right resource DLL under Windows 2000:
wLangId = GetUserDefaultUILanguage();
_stprintf(g_tcsTemp, _TEXT("res%x.dll"), wLangId);
if((hRes = LoadLibrary(g_tcsTemp)) == NULL)
{
// we didn't find the desired language satellite DLL, lets go with English (default).
hRes = LoadLibrary(_TEXT("res409.dll"));
}
In the case of failure to load the appropriate language DLL (when only a subset of the OS languages are supported in the application, for example) the only solution will be to assume that your default/preferred language DLL is present on the system.
One other possible technique is to include the English (or your default language) resources in the main executable file and ship satellite DLLs for other languages (a mix of methods 1 and 3). Since there is no real advantage to this approach and since English is, after all, just another localized language, this technique is not discussed any further.
| Summary of pros and cons for Method 3 (One resource DLL per target language) |
| Advantages | Disadvantages |
Allows user interface switching “No compile” resource update possible Complete control over the languages installed Easy to update with new languages Language-specific updates do not affect all languages No need to worry about user, system and thread locales Can be implemented for Windows 9x, Windows NT and Windows 2000
| |
Top of page
Summary
Writing single-binary code is the first step towards true truly world-ready products, and helps reduce your development and support costs. Implementing a multilingual user interface that allows users to switch between all supported languages is the next logical step towards achieving customer satisfaction worldwide. Writing Unicode-aware code and creating satellite DLLs for your language resources makes these goals much easier to achieve than you may have imagined.
Top of page
Glossary
Thread Locale - Locale of a given thread. Gets inherited upon creation from the current User Locale and can be changed at run time to any valid locale (per thread). Calls to NLS APIs can use this locale to format numbers, date, time ...
System Locale - Not really a locale. Determines which script non-Unicode applications will support. Thus it is the locale emulated by the system, as seen by applications. The system locale is system wide, in that it applies to all users. Changing the system locale requires a reboot.
UI Language - Language in which the operating system displays its menus, help files, and dialog boxes.
User Locale - The user preferences for formatting of dates, currencies, numbers, etc. The user locale is a per-user setting, and does not require a reboot or logoff/logon."
Top of page