Storing Strings

Glossary

Win32 supports two resource types for storing strings: string tables and message tables. String tables make sense for short strings and for strings containing only one replacement parameter; message tables are more convenient for alert and error messages that contain more than one replacement parameter. (Message tables support up to 99 parameters.) The FormatMessage API function will substitute variables according to each place marker'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. (See Figure 4-7.) The message table file format is not complicated; you can create message tables with a simple text editor.

// SAMPLE.MC
LanguageNames=(German=2:msg00002)

MessageId=1 SymbolicName=IDS_NOFILE
Language=English
Cannot open file %1
.
Language=German
Die Datei %1 kann nicht geöffnet werden.
.
.
MessageId=2 SymbolicName=IDS_OTHERIMAGE
Language=English
%1 is a %2 image.
.
Language=German
%2-Abbild ein %1 ist.
.

Figure 4-7. A message table that contains English and German translations of the same strings. Note that the German translation of IDS_OTHERIMAGE reverses the positions of the replacement parameters.

The syntax for formatting the first message in Figure 4-7 is as follows:

// lpBuf must be large enough to hold the formatted message!
DWORD langID = MAKELANGID(LANG_GERMAN, SUBLANG_GERMAN);
HMODULE hModule = LoadLibrary(...);
TCHAR lpBuf[60];
LPVOID lppArgs[10];

DWORD len = FormatMessage(FORMAT_MESSAGE_FROM_HMODULE |
FORMAT_MESSAGE_ARGUMENT_ARRAY,
hModule, idMsg, langID,
lpBuf, sizeof(lpBuf), lppArgs);

You can use FormatMessage with string tables as well as with message tables, but it is more efficient to use the function with message tables. FormatMessage can retrieve message table strings directly, but it cannot access string tables. To format a string from a string table, you would first have to retrieve it with the LoadString function and then pass it in a buffer to FormatMessage. Not only is this an extra step—it's a convoluted extra step.

FormatMessage is particularly useful for a number of reasons. In conjunction with the API call GetLastError, you can use it to format error messages returned by the system. (Consult the Win32 API Quick Reference topic titled "FormatMessage" for more details.) An example of this technique is given in Chapter 3 in the ReportError routine. FormatMessage also allows you to specify the language of the string you want to retrieve from a message table. LoadString can retrieve only resources associated with the language of the current thread's locale. (Thread locale is explained in the section titled "Multiple-Language Resources".)

The syntax for creating multiple-language message tables is quite simple. The message table in Figure 4-7 above contains English and German translations. The Language= statements define the language of each message string. (The message table format also has a CODEPAGE option to allow text to be encoded in more than one code page in a single file). The LanguageNames statement associates a "friendly" name you choose, such as "German," with a binary filename, such as "MSG00002." ("MSG00001" is the English default.) The message compiler, MC.EXE, uses these names to generate the binary files MSG00001.BIN and MSG00002.BIN. It also creates an .RC file that includes the binary files, tagged with the appropriate language, and a header file that incorporates the message numbers and symbolic names. (See Figure 4-8.) You can #include these files in other .RC files.

// SAMPLE.RC
LANGUAGE 9, 1
MESSAGE_TABLE MSG00001.BIN
LANGUAGE 7, 1
MESSAGE_TABLE MSG00002.BIN

...

// SAMPLE.H
#define IDS_NOFILE 1
#define IDS_OTHERIMAGE 2

Figure 4-8. The .RC and .H files that the message compiler generates for the message table in Figure 4-7.

Notes on String Tables

When adding a string to a string table, always create a new identifier at the end of the string table. Don't shift identifiers or reuse old identifiers. Localization tools often associate strings with their IDs. If you insert a string in the middle of a table and shift all the resource IDs, translations that rely on those IDs will suddenly be off by one. For example, three strings might be initially defined and translated as follows:

String ID English Spanish
IDS_STRING1 Open Abrir
IDS_STRING2 Find Buscar
IDS_STRING3 Copy Copiar

Inserting a string in the middle of the string table and shifting the identifiers causes problems, as shown below. Even though the English strings for IDS_STRING2 and IDS_STRING3 are now "Save" and "Find," the Spanish strings still correspond to "Find" and "Copy."

String ID English Spanish
IDS_STRING1 Open Abrir
IDS_STRING2 Save Buscar
IDS_STRING3 Find Copiar
IDS_STRING4 Copy Copy

LoadString, the function for retrieving strings from string tables, doesn't automatically tell you how large your string buffer needs to be. The maximum length of a Win32 string resource is 4k characters (8k bytes), though some tools limit strings to a maximum of 256 bytes. Allocating a buffer using the system constant LARGE_SIZE should accommodate the longest string in your resource file, as in this example:

CHAR pszStr[LARGE_SIZE];

if (LoadString(hMod, IDS_STRING1, pszStr, LARGE_SIZE))
<proceed>;
else
<error>;