Multilanguage Programming

David presents a technique that lets multiple language resources be built into a single resource DLL and automatically referenced by Windows applications.


November 01, 2000
URL:http://www.drdobbs.com/cpp/multilanguage-programming/184404314

Nov00: Multilanguage Programming

David is a programmer for IBM. He can be contacted at [email protected].


The world-wide distributing power of the Internet has made internationalization of software applications more important. Unfortunately, multilanguage support cannot be simply added to a program that was not designed for international use. This capability must be designed into the product in the early stages of development. There are many techniques to multilanguage-enable an application, depending on the language and available tools.

In a simple application, internationalizing simply means separating the user-interface text from the source code. However, a more complicated application must be sensitive to screen layout and locale-dependent sorting. Multilanguage Windows apps written in Visual C++ can be created with language-specific information in a resource DLL. Resource DLLs do not usually contain any code logic but encapsulate all the application resources (strings, dialogs, menus). Generally, a resource DLL is developed for each language (English, French, German, and so on) for the application and loaded programmatically depending on the system's locale settings. The problem with this approach is that adding or removing support for a new language may cause changes in either the install or program logic.

In this article, I'll present a technique that lets multiple language resources be built into a single resource DLL and automatically referenced by Windows apps. The complete source code and related files are available electronically; see "Resource Center," page 5). Basically, the technique consists of:

There are limitations to this approach. This technique will only work if all the translated language characters are in the same code page. For most single-byte languages, Windows ANSI code is used. Therefore, these languages can be compiled into and referenced from a single DLL. Double-byte languages like Japanese could not be included in such a DLL.

Anatomy of a Resource Script File

Using the Visual C++ resource editor, resources can be created individually and assigned to a locale setting using the LANGUAGE keyword. These resource files (.rc) generally have a structure as in Table 1.

The code_page identifier tells the resource compiler what kind of characters are found in the .rc file. Although the LANGUAGE keyword can be overridden per resource, the code pages cannot. The resource compiler cannot mix different character sets into a single compilation -- only the first #pragma code_page statement found is honored.

Generally, the application and the resources are developed in a single native language (such as English), and these resources are sent to a translation center that returns different language versions of the files.

A Main Resource Script

To develop a multilanguage-resource DLL for use by a Visual C++ program, the resources must be compiled together in a single unit. The technique presented creates a main .rc file to include each of the target language .rc files. In this way, the structure of each .rc file is the same although the content is language dependent.

Why use a new resource file to include the others instead of using an existing one like the English .rc file to include them? Changing the native .rc file so it has unique directives changes its structure, which makes it difficult to reconcile with the translated files. Translators sometimes use software to translate text automatically in script files. Translation will go easier if all the language script files (including the English one) have the same directives and structure as the native ones.

Each language file contains a unique LANGUAGE keyword identifying the language and sublanguage ID for the file. The LANGUAGE keyword can also be included on individual resources if required.

Listing One is an example script file containing STRINGTABLE declarations using the LANGUAGE identifier. When using the MFC classes or the Win32 resource APIs, Windows will automatically load the appropriate strings, dialogs, and so on, by matching the current locale setting to the LANGUAGE keyword attributes. See the Microsoft Developer Network Library (MSDN) for more information on how these resources are resolved. Note that Listing Two is equivalent to the Listing One STRINGTABLE example and will be the style used in the example presented in this article.

Creating a Test Application

First, a simple dialog program will be created with three language resources in a single resource DLL. The resources to be used include an icon, a set of strings, and the dialog itself. The test application will basically display the dialog, filling in appropriate controls with text. The application can best be created using the Visual C++ MFC AppWizard.

After starting Visual C++, select File-> New to create a new project. On the Project page, select the MFC AppWizard (exe) project type and enter a project name (MLTest). Pressing OK shows the first page of the AppWizard. Select "Dialog based" and click Finish. This will create an MFC application complete with resources for the main dialog, some strings, and an icon. These resources are bound to the executable, which is not what we want. Also, there are some extra resources like the About Box that are not needed. The project needs to be modified to use the resources provided by a separate DLL (created later). At this time, the test application can be built and run successfully.

Under the workspace window, the ResourceView tab should show two dialogs, icon, string table, and version info. In this view, delete all resources except for the VS_VERSION_INFO. This resource provides information to the user about the .EXE in the Properties dialog presented by Windows Explorer. This information includes a version number and company name that are not translated. The version info should always be bound to the executable.

At this time, the project will not be compilable because the source code is still referencing the resources just deleted. (This will be corrected later.)

Creating the Resource DLL

The next step is to create a subproject for the DLL that will hold the resources. From within the MLTest project just created, select File->New. Under the Projects tab, select Win32 Dynamic-Link Library, select the "Add to current workspace radio button," check the "Dependency of" check box, and enter "MLDLL" for the project name. The project name will also be the name of the resource DLL; see Figure 1.

These selections will make the resource DLL a dependency on the test program and add it to the existing workspace. In this way, the resource DLL is built as needed whenever the test program is built. On the Win32 Dynamic-Link Library -- Step 1 of 1 dialog, select "An empty DLL project" and press Finish. This will create a project with no source files that can build a Windows DLL with no code logic. The resources will be added to this DLL. The project wizard adds the MLDLL project to the MLTest workspace and automatically makes it the active project.

The first file to be created in the MLDLL project will be the main.rc file, which will include the other language .rc files. Select File->New to show the New dialog. Under the Files page, select Resource Script and enter main.rc as the filename. This will create a main.rc file in the project. Resources that are common to all languages (like icons) can be added to this file, but translatable resources (such as strings and dialogs) should be added to a language-specific file (created next).

The aforementioned process also creates a resource.h file that is not automatically added to the project. This file will later be added manually. Select File->New to show the New dialog again. Under the Files page, select Resource Script, enter english.rc for the filename, and press OK. A message will appear saying resource.h will be overwritten. The main.rc and english.rc files are going to share this header file, and the resource editor is smart enough to manage the IDs properly, so overwriting the file is fine. Select Yes, and a message box will appear that indicates that only one .rc file can be assigned to the project as its main resource file. Other .rc files can be included in the project, but they will not be compiled individually. This is fine. The main.rc file will include the other language-specific .rc files, so building main.rc will compile all of the necessary files.

Next, the resource.h file must be added manually to the MLDLL project. Select Project->Add to project->Files menu option and pick the resource.h file found under the MLDLL subdirectory. You will see a resource.h file in the MLTest subdirectory. Do not use it. This is the one used for the version info in the MLTest project.

For the MLTest->MLDLL dependency relationship to work, a .LIB file needs to be generated when the DLL is built. But because there is no code (no functions to invoke) in the DLL, the Visual C++ linker does not automatically create a .LIB file. A .DEF file must be added to the project. Create a new text file in the MLDLL project called "MLDLL.DEF" and add the lines in Listing Three. This tells the linker to generate an MLDLL.LIB file with no exported functions or data. Finally, the MLDLL project settings must be changed to build the resource DLL correctly and place the resulting DLL in the proper directory. Select Project->Settings and select the Link page in the Project Settings dialog. Change the output filename so the DLL is created in the MLTest subdirectory. Also, add the /noentry link parameter to the project options. These changes should be made for the Release build settings as well. The output file name is changed so that the DLL does not need to be moved when running the test application -- it will coexist with the application's MLTest.EXE. The /noentry is required because no functions (code) actually exist in the DLL. Although there are no resources yet in the source files, the DLL can now be built successfully. The MLTest project will still not compile at this time.

Adding Resources

At this point, the expanded workspace ResourceView shows "No Resources" for the main and English resources. Now, some typical translatable and nontranslatable resources will be added.

First, add an icon to the main resource file. Generally, icons do not contain text and, therefore, do not need to be translated. Therefore, these can be added to the main resource file. Right-click "main resources" in the ResourceView and select Insert.

At the Insert Resource dialog, select Icon and click Import. In the File Dialog, select the mltest.ico file found in the MLTest\res directory. This is the standard MFC icon created by the AppWizard when building MLTest. For convenience in later steps, change the ID of the icon from IDI_ICON1 to IDR_MAINFRAME.

Next, add a dialog to the English resource file. Right-click on "english resources" in the ResourceView and select Insert. On the Insert Resource dialog, select Dialog and click New. A default dialog is created, which is called "IDD_DIALOG1." It contains OK and Cancel buttons. Change the dialog title to say "Welcome."

Add a static control, a combobox, and a listbox so the dialog uses the default ID values. The translatable text in the dialog are in the title bar, the static control, and the buttons. Strings will be created to add to the combobox and the listbox. To add a string table, right-click on "english resources" and select Insert. On the Insert Resource dialog, select String Table and click New. Add the strings with appropriate IDs so they match Table 2.

Once completed, the MLDLL can be built. Select Yes to the message asking if it is OK to overwrite resource.h. Because the resource.h file is being shared, this message appears periodically.

Once english.rc contains the translatable resources, main.rc needs to be modified to include this file. This is done manually using only the Visual C++ text editor.

Start by saving and closing the MLTest workspace. Closing the workspace is not technically necessary, but will cause less confusion later. Next, select File->Open and change the "Open As" selection to "Text." Select the main.rc found in the MLTest\MLDLL directory and press Open. If "Open As" is not set to "Text" when an .rc file is selected, Visual C++ opens it for the resource editor complete with a ResourceView. The file contains all the sections as described previously including the LANGUAGE keyword identifying English for this source file. Inside the main.rc, locate the sections of code in Listing Four and add the lines shown in bold. The bottom section tells the resource compiler to include the three language .rc files in this compilation unit. The top section tells the Visual C++ resource editor that these resource files are included on purpose. The best way to explain this is to describe what happens if you do not include these lines. If the main.rc is loaded into the resource editor and you change the resources in any way (for example, add a new icon), the resource editor recreates the main.rc inlining the other three .rc files and removing the #include statements. Adding the lines in the 3 TEXTINCLUDE section tells the resource editor not to inline these files. The french.rc and german.rc files do not yet exist and are created next by copying the english.rc.

Save and close main.rc and open the english.rc file, again using the "Open As Text" selection in the File Dialog. Next, select Edit->Select All and copy the entire source file onto the clipboard. Close english.rc and select File->New and create a text file called french.rc. Paste the contents of the clipboard into this file and save this file. Repeat the procedure to create a german.rc file. At the top of the french.rc file, change the lines around the LANGUAGE keyword to read:

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_FRA)

#ifdef _WIN32

LANGUAGE LANG_FRENCH, SUBLANG_FRENCH

#pragma code_page(1252)

#endif //_WIN32

And the german.rc file to read:

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_DEU)

#ifdef _WIN32

LANGUAGE LANG_GERMAN, SUBLANG_GERMAN

#pragma code_page(1252)

#endif //_WIN32

It is probably wise to also change any comments that say "English Resources" to the appropriate language as well. Save and close both files. Reopen the MLTest workspace (it should be available under File-> Recent Workspaces) and add the french.rc and german.rc files to the MLDLL project. The FileView should look like Figure 2.

The MLDLL project should now build successfully, but the MLTest project still will not build. The MLTest project is fixed up next.

The resource files are now ready to be translated. Usually, the translation centers receive the french.rc and german.rc files with English content as created here, then return the files with the appropriate text translated.

Using the Resource DLL

Once the MLTest program has been modified to use the MLDLL, change to the FileView in the MLTest workspace and set the MLTest project as active. Open the MLTest.h file and change the line that includes resource.h to the following:

#include "MLDLL\resource.h" // main symbols

The resources that MLTest needs are defined in MLDLL's resource.h file. Next, open the MLTestDlg.h file and change the dialog IDD line to the following:

enum { IDD = IDD_DIALOG1 };

This is the main dialog for MLTest and has an ID of IDD_DIALOG1. Save and close both files. Inside MLTestDlg.cpp, code that refers to the About Box needs to be removed because the dialog was not created in the MLDLL resources. Enabling the About dialog and its dependent resources is left as an exercise for the reader. Remove the source code from MLTestDlg .cpp that references the About Box. The code should look like Listing Five when finished.

The MLTest project should now be buildable, but still won't run. The final task to complete the test application is to add the program logic to use the resource DLL and to load the combobox and listbox. Open the MLTest.cpp file and add to CMLTestApp::InitInstance() the lines in bold shown in Listing Six. This logic loads and sets the resource DLL for this application. If the MLDLL cannot be found or loaded, an error message (yes, in English) indicates a problem and exits the program. After AfxSetResourceHandle() is called, all resources referenced by the application using MFC or Win32 methods will be loaded from the DLL. To fill the combobox and listbox, add Listing Seven to the CMLTestDlg::OnInitDialog() method in MLTestDlg.cpp. Add the following lines to the end of the CMLTestDlg::DoDataExchange() method in the same file:

DDX_Control( pDX, IDC_COMBO1, m_comboBox );

DDX_Control( pDX, IDC_LIST1, m_listBox );

Then add these member variables to the CMLTestDlg class in MLTestDlg.h:

CComboBox m_comboBox;

CListBox m_listBox;

Build and run the MLTest program. Note the dialog operation including the combobox.

Conclusion

The advantage of this method is obviously that one DLL can replace the many language DLLs required for your software, reducing the file count and distribution size and simplifying the product installation. The method also simplifies source code because no special logic is required to load the correct resources. Windows handles picking the appropriate resources from the multilanguage DLL by matching the LANGUAGE keyword to the current locale setting.

DDJ

Listing One

STRINGTABLE PRELOAD DISCARDABLE
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
BEGIN
  IDS_LINE1, "line 1"
  IDS_LINE2, "line 2"
END

STRINGTABLE PRELOAD DISCARDABLE
LANGUAGE LANG_FRENCH, SUBLANG_FRENCH
BEGIN
  IDS_LINE1, "ligne 1"
  IDS_LINE2, "ligne 2"
END

Back to Article

Listing Two

// start in English
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US

STRINGTABLE PRELOAD DISCARDABLE
BEGIN
  IDS_LINE1, "line 1"
  IDS_LINE2, "line 2"
END

// switch to French
LANGUAGE LANG_FRENCH, SUBLANG_FRENCH

STRINGTABLE PRELOAD DISCARDABLE
BEGIN
  IDS_LINE1, "ligne 1"
  IDS_LINE2, "ligne 2"
END

Back to Article

Listing Three

LIBRARY      "MLDLL"
DESCRIPTION  'Multi-Language Resource DLL'
EXPORTS
    ; no exports for this DLL

Back to Article

Listing Four

3 TEXTINCLUDE DISCARDABLE
BEGIN
  "#include ""english.rc""\r\n"                 
  "#include ""french.rc""\r\n"
  "#include ""german.rc""\r\n"
  "\0"
END
 ...
#ifndef APSTUDIO_INVOKED
// Generated from the TEXTINCLUDE 3 resource.
#include "english.rc"
#include "french.rc"
#include "german.rc"

#endif  //this is the last line of the file

Back to Article

Listing Five

// MLTestDlg.cpp : implementation file

#include "stdafx.h"
#include "MLTest.h"
#include "MLTestDlg.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

///////////////////////////////////////////////////////////
// CMLTestDlg dialog

CMLTestDlg::CMLTestDlg(CWnd* pParent /*=NULL*/)
           : CDialog(CMLTestDlg::IDD, pParent)
{
   //{{AFX_DATA_INIT(CMLTestDlg)
           // NOTE: the ClassWizard will add member initialization here
   //}}AFX_DATA_INIT
   // Note that LoadIcon does not require a subsequent DestroyIcon in Win32
   m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void CMLTestDlg::DoDataExchange(CDataExchange* pDX)
{
   CDialog::DoDataExchange(pDX);
   //{{AFX_DATA_MAP(CMLTestDlg)
           // NOTE: the ClassWizard will add DDX and DDV calls here
   //}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CMLTestDlg, CDialog)
  //{{AFX_MSG_MAP(CMLTestDlg)
  ON_WM_SYSCOMMAND()
  ON_WM_PAINT()
  ON_WM_QUERYDRAGICON()
  //}}AFX_MSG_MAP
END_MESSAGE_MAP()

// CMLTestDlg message handlers
BOOL CMLTestDlg::OnInitDialog()
{
   CDialog::OnInitDialog();
   // Set the icon for this dialog.  The framework does this automatically
   //  when the application's main window is not a dialog
   SetIcon(m_hIcon, TRUE);                 // Set big icon
   SetIcon(m_hIcon, FALSE);                // Set small icon

   // TODO: Add extra initialization here

   return TRUE;  // return TRUE  unless you set the focus to a control
}
void CMLTestDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
   CDialog::OnSysCommand(nID, lParam);
}
void CMLTestDlg::OnPaint()
{
   if (IsIconic())
   {
      CPaintDC dc(this); // device context for painting
      SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
      // Center icon in client rectangle
      int cxIcon = GetSystemMetrics(SM_CXICON);
      int cyIcon = GetSystemMetrics(SM_CYICON);
      CRect rect;
      GetClientRect(&rect);
      int x = (rect.Width() - cxIcon + 1) / 2;
      int y = (rect.Height() - cyIcon + 1) / 2;
      // Draw the icon
      dc.DrawIcon(x, y, m_hIcon);
   }
   else
   {
      CDialog::OnPaint();
   }
}
HCURSOR CMLTestDlg::OnQueryDragIcon()
{
   return (HCURSOR) m_hIcon;
}

Back to Article

Listing Six

 ...
#ifdef _AFXDLL
      Enable3dControls();
#else
      Enable3dControlsStatic();
#endif

HANDLE hResHdl = LoadLibrary( "MLDLL.DLL" );
if( hResHdl )
   AfxSetResourceHandle( (HINSTANCE)hResHdl );
else
{
   AfxMessageBox( "Could not load resource module" );
   return FALSE;
}
CMLTest dlg; // existing line
 ...

Back to Article

Listing Seven

  m_comboBox.AddString( CString((LPCSTR)IDS_CB_STRING1) );
  m_comboBox.AddString( CString((LPCSTR)IDS_CB_STRING2) );
  m_comboBox.AddString( CString((LPCSTR)IDS_CB_STRING3) );
  m_comboBox.SetCurSel( 0 );

  m_listBox.AddString( CString((LPCSTR)IDS_LB_STRING1) );
  m_listBox.AddString( CString((LPCSTR)IDS_LB_STRING2) );
  m_listBox.AddString( CString((LPCSTR)IDS_LB_STRING3) );


Back to Article

Nov00: Multilanguage Programming

Figure 1: Win32 Resource DLL project.

Nov00: Multilanguage Programming

Figure 2: MLTest FileView.

Nov00: Multilanguage Programming

Table 1: Anatomy of a resource script.

Nov00: Multilanguage Programming

Table 2: String Table entries.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.