|
|||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
AbstractWhile preparing your application to support localization, probably the most tedious step consists of extracting the string literals from the source code and adding the code that loads these strings from the resource string table. Adding the code to extract the strings is not difficult. But since it must be performed hundreds of times in an application, we need to simplify the code as much as possible. That can save us hours of boring work. Also, the resulting code should be as readable as possible. Which gives us a second good reason to use the simplest possible code. This article features two small yet very helpful classes that do just that: load string literals from the String table with as little and non-intrusive calling code as possible. It also covers a few string formatting considerations related to localization requirements. IntroductionWhy should I use the string table?While preparing for localization and translation of your app, all translatable items (such as strings) must be stored in the resources instead of in the source code, in order to keep translators away from your source code. Localization tools such as appTranslator can easily manipulate your application resources, but you don't want them to play with your source code! Therefore, all the strings that need to be translated must be stored in the resources. And the String table is the appropriate location. SetWindowText(_T("Weather Forecast")); // We must move this string to // the resource string table Note: You may want to store strings in the Message table, which is an alternative resource that looks much like the String table. Although originally designed for better localization support, it is less often used. In addition, Visual Studio does not provide a custom editor for the Message table, making it much less appealing. Daniel Lohmann wrote an excellent article about the Message table. The LoadString() API: Not straightforward enoughProgrammatically extracting a string from the String table involves calling the Note: In addition, the
CString strMyString; strMyString.LoadString(IDS_MYSTRING); // wrapping this call into VERIFY() // would be a good idea ! SetWindowText(strMyString); // Example of use of my string It's better than the raw API version. But it's still too long! CMsg: A one-liner!SetWindowText(CMsg(IDS_MYSTRING)); // This is straightforward! The That's all you need to know to use Of course, if the use of a string is not limited to a function call, you can create an explicit object for it: CMsg strTitle(IDS_MYSTRING); // The string is loaded during the object // construction SetWindowText(strTitle); SomeOtherFunctionThatUsesTheString(strTitle); Update!Luis Barreira mentioned (in the comments section) that CString strTitle(MAKEINTRESOURCE(IDS_MYSTRING)); // Straightforward as well although not as compact Good catch, Luis! However, bear with me because the second class I introduce (below) is even more useful. By the way, why such a cryptic name as CMsg?Why not What if the string ID is incorrect?In such a case, the CFMsg: A one-liner sprintf() wrapperVery often, one needs to format a message before passing it to a function. And since we're speaking of translatable strings, the formatting message must be loaded from the String table as well. Let's re-use our example above and set a more elaborated window text: CString strMyTitle, strMyTitleFormat; strMyTitleFormat.LoadString(IDS_MYTITLE); // "The weather today in %1" where // %1 is a placeholder for the city // name strMyTitle.FormatMessage(strMyTitleFormat, LPCTSTR(strCity)); SetWindowText(strMyTitle); // Example of use of my string Pfeeew... four lines instead of one because of a variable parameter in the string :-( Luckily, SetWindowText( CFMsg(IDS_MYTITLE, LPCTSTR(strCity)) ); // This is // straightforward! Now, if you think this is too condensed and would rather want the message built outside of the CFMsg csTitle(IDS_MYTITLE, LPCTSTR(strCity)); // "The weather today in %1" // where %1 is a placeholder // for the city name SetWindowText( csTitle ); Actually, the paragraph title above is misleading. Localization requires formatted string arguments to be numberedA format message looks much like the E.g.: In French, adjectives usually come after nouns, as opposed to English.
A formatting string with variables for color and name of the animal can be translated only by using such an argument numbering:
Format specification : FormatMessage/CFMsg vs. sprintfIf you're used to
FormatMessage() arguments are not limited to strings!People often believe that Update: Well, not _every_ format specifier is supported: floating-point specifiers (e, E, f, and g) are not supported. How to adapt your existing strings to take advantage of CFMsg?That's simple. Number the arguments in their current order. If an argument is not E.g.: CFMsg is your friend even if you don't use the String tableWe've seen that the first argument of the SetWindowText(CFMsg(_T("The weather today in %1"), LPCTSTR(strCity))); Using the codeSimply add Msg.h and Msg.cpp to your project. Include Msg.h in .cpp files where you would use the class. I recommend you to include Msg.h in stdafx.h as you will most likely use Store every string literal in the String table (using the string editor) and replace it in the source code by The demo projectYou can see The zip file contains VC6 and VC7 project files. The compiled EXE enclosed in the zip file was compiled using VC .NET 2003, hence requires MFC71.dll. If you are using VC6, you may need to re-compile the project. You don't always need CMsgBe aware that some MFC functions of class members, such as AfxMessageBox(_T("Operation Failed."), MB_ICONERROR); After extraction of the string, the code becomes: AfxMessageBox(IDS_ERROR, MB_ICONERROR); // No need to use CMsg here. // AfxMessageBox() will load the // string for us. However, you may want to keep using AfxMessageBox( CFMsg(IDS_ERROR, LPCTSTR(strError)), MB_ICONERROR); GuidelinesThere are a lot of string literals used in a program. Most of them are used only once. Common practice leads us to think that strings exported to the String table should have a symbolic identifier rather than simply a raw numerical ID. However, this practice hits a limit when used with the String table: there are so many strings that finding an easy-to-use, self-speaking and unique identifier for each string quickly becomes a nightmare. Developers have no other choice than create identifiers that are kind of copies of the string literal, with adapted syntax (such as underscores instead of spaces). These identifiers are (very) long and particularly difficult to manipulate. I suggest you to drop identifiers for such strings (except for the ones used several times throughout the source code!). Instead of a symbol, use the raw numerical string ID in the source code and append a copy of the string as a comment at the end of the line. E.g.: pWnd->SetWindowText("Please enter your name"); After extraction of the text to the String table, modify the source code to: pWnd->SetWindowText(4635); // Please enter your name instead of: pWnd->SetWindowText(IDS_PLEASE_ENTER_YOUR_NAME);
I successfully used this method with thousands of string literals. It's easier and much quicker to code, yet more readable. Experience shows that teammates can very easily read each other's code when they consistently use this technique. Of course, your own coding style and practice may vary. ConclusionThere is no magic in
|
||||||||||||||||||||||||||||||