In today's world, localization and translation of software has become an important feature since it dramatically helps in boosting sales. As far as Win32/MFC applications are concerned, managing different languages for your app requires the use of satellite DLLs.
This article describes an easy-to-use method to support multiple languages in your C++/MFC applications. It shows how to add support for satellite DLLs (also known as resource DLLs) in your app by simply adding a few of lines of code. This includes:
- Automatically selecting the most appropriate language at start-up according to user preferences.
- Providing a submenu for language selection (in case the user is not happy with the default choice). See the picture above.
It also explains how to create satellite/resource DLLs, although this is already covered in many other articles. By the way, I am a developer of appTranslator, a localization tool that (among other things) creates resource DLLs for you, freeing you from the hassle of managing Visual Studio projects for all these resource DLLs.
There are quite a few articles on CodeProject that deal with localization and resource DLLs. (This one is a very good introduction to localization of MFC apps.) This article was published after I started writing mine! However I decided to go on and publish mine because I believe the topic of language selection menu was not covered in any of the articles on CodeProject. Also, other articles don't cover MFC7.
A few words on resource DLLs
It is commonly accepted that the most flexible method to support multiple languages in your app is to use the so-called resource DLLs (also known as satellite DLLs). The idea is to create one DLL per language. The DLL contains a copy of all your application resources translated into one given language. Therefore, if your app's original version is English and you translate it to French, German and Japanese, you'll end up with three resource DLLs: The English resources stay in the .exe and there is one DLL for French, one for German and one for Japanese. Whenever you make a new translation of your app, you simply need to add one more DLL to your installer.
At start-up, the application decides which language it should use (according to user preferences) and accordingly loads the resource DLL. Resource DLLs can be created using a dedicated Visual Studio project. Or they can be created by using the localization tools such as appTranslator. One nice thing about appTranslator is that the developer need not worry about the creation and maintenance of resource DLLs: Just hit the Build button and it creates them for you!
By the way, packing all the languages into a single EXE is theoretically possible. But it just doesn't work The reason is that most high level APIs that load resources (such as
DialogBox() et al won't let you specify the language you want. And SetThreadLocale() has stopped working the way you expect since Windows 2000 (and it never existed on Win9x).
The step-by-step method to support resource DLLs
Here are the steps required to add support for resource DLLs (and language menu) in your main application:
- Add LanguageSupport.h and LanguageSupport.cpp to your project.
- In MyApp.h and MyApp.cpp (assuming
CMyApp is your application class), add the lines shown in bold:
#include "LanguageSupport.h" // The class that handles
class CMyApp : public CWinApp
CLanguageSupport m_LanguageSupport; ...
CWinApp::InitInstance(); <-- Comment out this line to
- In your main menu, add a menu item labeled 'Language'. I usually add it in my Tools menu (if any), but it is up to you.
- In your
CMainFrame class, add an update menu handler for the Language menu item. Let's assume you named it
OnUpdateToolsLanguage(). Fill it as follows:
void CMainFrame::OnUpdateToolsLanguage(CCmdUI *pCmdUI)
- Add the menu handler for the languages menu item:
In MainFrm.h, add the handler declaration somewhere in the protected part of
afx_msg void OnLanguage(UINT nID);
In MainFrm.cpp, add the handler definition and its message map entry:
void CMainFrame::OnLanguage(UINT nID)
Note: This handler cannot be added using the wizard because it is a command range menu handler: On handler for all language items in the Language submenu.
- Last step:
In the String table (resources), add a string named
IDS_RESTART with the text "Please restart %1". (Note: You can replace %1 with your app's name).
(By the way, I have written another article about String table and how to easily extract and format strings).
How to create the resource DLLs
First of all, the
CLanguageSupport class assumes that the DLLs are named MyAppXXX.dll where MyApp.exe is the name of your executable file and XXX is the 3-letter acronym of the language they contain (e.g.: FRA stands for French, DEU for German and JPN for Japanese). Also, both your EXE and the DLLs should have a version info resource whose language matches the three letters acronym in the file name.
The easiest way to create the DLL is to use the appTranslator, since the tool creates the dialog for you (you simply have to check 'Satellite DLL' in the properties). But of course, I won't assume that every one uses my tool , so here's the manual way of doing it:
- Create a Win32 DLL project: In Visual Studio 2003, choose File/New/Project..., enter a project name such as MyAppDEU to create a German version and click OK. Then, in the Application Settings tab, select DLL and Empty Project.
- Turn it into a resource DLL (also known as resource-only DLL or satellite DLL): Open the project properties, select All Configurations in the configurations ComboBox and then open the tab Linker/Advanced. Set Resource Only DLL to Yes. Note to VC6 users: This setting does not exist in VC6 project property pages. You must manually add /NOENTRY in the linker settings edit box with the command line settings.
- Create a copy of your EXE resource file and add it to the DLL project. I recommend you to rename your MyApp.rc file to MyAppDEU.rc (or whatever language acronym applies).
- In the Resource View, right-click MyAppFRA.rc (MyAppFRA in Visual Studio 6) and open 'Resource Includes...'. Modify the path/filename of all the included resource files to include their translation. The MFC resources translation is stored in a sub-directory (per language): l.xxx\ where xxx is the three-letter acronym of the language. e.g.: Change
#include "afxres.rc" to
- In the Resource View, open the version info (or create one if you don't have one) and set the language property of the Block Header to the right language (e.g.: German (Germany)). Make sure the language matches the three-letter acronym used in the DLL name.
You can now compile the DLL. I suggest you to edit the output settings of the DLL project to copy the file side-by-side with your main EXE (i.e. in the same directory). You now have a resource DLL. Of course, it's not translated yet but that's the job of the translator.
Start your app; open the Tools menu (or the menu where you created the Language item). The submenu should contain English and German. Follow the same procedure to create DLLs for the other translations you need. Once your DLL is available, the only thing you need to do is copy it side by side with your app (.exe) and it will automatically be taken into account for language selection.
Does CLanguageSupport work with Unicode? And with ANSI?
CLanguageSupport works like a charm in both Unicode and ANSI builds. As explained below, it even makes its best to check for support for the targeted languages on the user's computer.
Does it support on-the-fly language switch?
Yes! But this requires some work on your side that
CLanguageSupport can't do for you, such as updating the menus, views and the control bars, making sure your app doesn't cache any resource (such as texts from the string table),... By default,
CLanguageSupport displays a message box asking the user to restart the application. To enable on-the-fly language switch, modify the menu handler call as follows (use '
true' as the second optional argument to the call):
void CMainFrame::OnToolsLanguage(UINT nID)
In addition, you must add the code that updates the current display of your app as (menus, views,...). This article tells you a little more about this part of the job but be aware there is going to be some custom work based on your app architecture and contents.
The sample application
It consists of a simple MFC AppWizard-generated project in which I have followed the step-by-step method described above to add a Language submenu. I have also created two resource DLLs (French and German) whose translation is more or less complete. (The French one is pretty much completed. The German one is about half done.)
In order to test the app, you should first compile the three projects (the EXE + the 2 DLLs). Start the project and see in which language the app starts. If you have either French or German Windows, the app will start in French or German. Otherwise it will start in English. The Language menu is located under Tools. The zip file contains the project files (and workspace/solution) for both VC6 and VS.NET. I have also included a copy of the executable files (the EXE and the resource DLLs - ANSI Release build).
Credits: The sample app's About dialog uses Paul DiLascia's
CStaticLink class (with minor modifications).
How does CLanguageSupport work?
LoadLanguage(): What does that imply?
LoadBestLanguage() function performs two tasks: The identification of the language which is the preferred language and the loading of the DLL.
Identifying the language to load : If the user had earlier selected a language in the language menu, we load it (we know it by looking up in the registry). If he has never made a selection before, we look for a few possible languages that fit into the user's preferences. As soon as we find a resource DLL for our app that matches that language, we load it. If we don't find a match, we eventually fall back on the original version of the app: The language stored in the EXE itself.
Loading the DLL is rather a simple task: We load the DLL using
LoadLibrary() and set it as the default resource container using
CreateMenu(): Creation of the languages submenu
This function looks for the DLLs available in the directory of the EXE whose name matches the pattern MyAppXXX.dll. It then looks for its language in the DLL's version info resource. (It doesn't identify it by the three letter DLL because... there's no simple way to find the language given the acronym. Brute enumeration of the languages supported by Windows would be the only solution, which according to me is a horrible method).
It then builds the menu according to the list of languages found.
CreateMenu() tries to display each language name in its own language (native name). It is careful enough to check if the language is supported by the Windows version of the user, in order to avoid displaying garbage (such as displaying Japanese on an English Win9x or NT4 system). If it finds that Windows can't display the language name; it falls back on the language name in the current user's language (as set in the Regional Options applet of the Control Panel).
Note: This detection is not 100% perfect: The fact that a language (actually the charset) is supported by Windows doesn't necessarily mean that the fonts for that language are installed. In such a case, the menu may display garbage. Now, this is probably not a major issue since your app wouldn't display well in that language anyway.
OnLanguageSwitch(): Take user's language choice into account
This one is the simplest function. All it does is store the user's choice into the registry (HKCU\MyCompany\MyApp\Settings : Language = (DWORD) LangId). It also asks the user to restart the app to load the new language. If caller wants to switch languages on the fly, the DLL for the new language is loaded right away.
Why a custom class? Doesn't MFC handle all that?
MFC7.1 (Visual Studio 2003) does some part of that work: It does the same kind of work at start-up (in
CWinApp::InitInstance(). That is why we must comment out the call in the appwizard-generated code: We don't want MFC and our code to step on each other's toes). But there's one important thing MFC doesn't care about: It doesn't support manual selection of the language, which also means that MFC doesn't offer support for a language menu.
It's a pity because there are many scenarios where the user could make a better choice than what MFC does for him. Imagine for example an app available in English and French. On an Italian or Spaniard's computer, MFC would choose English. But many Italians and Spaniards understand French better than English. It would be a shame to prevent them from selecting a language they understand better. That is why it's important to have a language selection menu in addition to automated language detection.
Things are even worse: You'd think we could re-use the MFC code and simply tweak it to take user selection into account. Bad luck: Part of this code is in
static MFC functions that can neither be called nor overridden. Some of the functions are
virtual but since overrides can't call the
private helper functions, we pretty much have to rewrite everything from scratch.
CLanguageSupport, managing resource DLLs should be really easy.
Creating the DLLs (and managing the corresponding projects) is not difficult at all but it's certainly a boring task. If you are serious about localization, I recommend you to give a look into the tools such as appTranslator that not only helps you manage the translation of your apps but also creates the resource DLLs for you.
One last note: I hope you don't mind the ads