Single Worldwide Binary: What's the Problem?
It is commonly accepted that the best strategy to provide support multiple languages with one single EXE is to use resource DLLs (a.k.a. Satellite DLLs). By the way, generating such resource DLLs with appTranslator is as easy as filling one checkbox!
But why do we have to bother playing with a whole set of DLLs? After all, several translations of one given resource (such as a string or a menu) can easily be stored in a single EXE. And if you call SetThreadLocale() to select your language, voilà, your resources will be loaded in the expected language. Not!
SetThreadLocale() is not the solution
Once upon a time, there was Windows NT4 (and even, 3.51, 3.5,...). In these times, the single worldwide binary was a realistic option... as long as you didn't have to target Win9x, under which SetThreadLocale() was a no-no!Then came Windows 2000 and its vastly improved support for internationalization. Paradoxically, the single worldwide binary was no longer an option. Because one could no longer rely on SetThreadLocale() to globally select the language for resources, as the docs say:
Windows 2000/XP: Do not use SetThreadLocale to select a UI language. To select the proper resource that is defined with a LANGUAGE statement, use FindResourceEx.
Docs say 'don't touch!' but they don't say why. And if you try to look further, you won't find much info. As always, Michel Kaplan's blog is a good place to look up, even if it's to get confirmation that there's not much to find out.
Well, there are a few lines somewhere: This article briefly explains that when MUI was first introduced back in Windows 2000, the Microsofties had to modify the way resources are loaded (If you know why, please let me know). Actually, they modified the default language selection algorithm used to load 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.
In other words, Windows will not load resources the way you expect if you happen to ask for the same language as the user preference (user default locale)! Yikes! You don't believe me? Fair enough, me neither! :-) Let's make a test.
The Sample Program
- The program contains a String Table with a string in German, French (Belgium) and English.
- The program selects German as its default UI language : SetThreadLocale(German)
- It then loads a string from the String Table(displayed in the message box title).
You expect the loaded string to be "German string". Play with you user locale and you'll see that the docs are unfortunately right.
Here is what I get with User Locale = French (Belgium): The german string is correctly loaded.
Then I modify my user locale and select German:
When I re-run the program, I still expect "German String" but I get something else :-(
What language does it pick then?
If you look carefully at the screenshots above, you might notice something weird: The docs (quoted above) say that in case of ThreadLocale==UserLocale, the language picked should be the first one in ID order, which for my program is... German! (German=0x407, French-Belgium =0x80C and English=0x409).So in this case, German should have been picked, as a side effect. But it was not: English was picked. How come?
I made further tests and it appears that when UserLocale==ThreadLocale, Windows tries to load the resources in its own UI language (even if it's different from User/Thread Locale). Anyway, this irrelevant since we just demonstrated that SetThreadLocale() cannot be trust to achieve the Single Worldwide Binary dream.
BTW, why would FindResourceEx help?
Remember the remark in the docs? Use resource DLLs or use FindResourceEx().
The reason FindResourceEx() would help is simply because it takes an explicit langiuage ID, as opposed to the most usual resource-loading APIs such as LoadString(), CreateDialog() et al. But it's not a realistic solution because it would mean that you must re-implement your own LoadString() et al using FindResourceEx().
No, you don't want to do that! So we'll keep with resource DLLs.