Jump to content

Multilingual Resources: Difference between revisions

From EDM2
Prokushev (talk | contribs)
No edit summary
 
Ak120 (talk | contribs)
mNo edit summary
 
(10 intermediate revisions by 3 users not shown)
Line 1: Line 1:
Written by [[Stefan Ruck]]
''Written by [[Stefan Ruck]]''


<p>You want your program to be multilingual? Why not, it's really easy to do.
You want your program to be multilingual? Why not, it's really easy to do. I want to show you what you need.
I want to show you what you need.


<h2>How To Do It?</h2>
==How To Do It?==
I should better ask: How not to do it?


<p>I should better ask: How not to do it?
One way occurred to me is as follows:


<p>One way occurred to me is as follows:
You can write all languages needed into one resource file and add a language key to the resource ID. Here's a sample:
 
<pre>
<p>You can write all languages needed into one resource file and add a
language key to the resource ID. Here's a sample:
 
<pre><small>
STRINGTABLE
STRINGTABLE
   BEGIN
   BEGIN
     STR_TEXT_E, "The english text"
     STR_TEXT_E, "The English text"
     STR_TEXT_D, "Der deutsche Text"
     STR_TEXT_D, "Der deutsche Text"
   END
   END
</small></pre>
</pre>
 
''Figure 1: Sample using one resource file for all languages''
<font SIZE=2>
Figure 1: Sample using one resource file for all languages,
</font>
 
<p>But this leads to many switch statements in your program. Anytime you need
a resource (a string, a dialog, a menu), you have to set a different resource
ID depending on the current language.
 
<h2>Use Resource Only DLLs</h2>
 
<p>Using resource only dynamic link libraries seems to me the better solution.


But this leads to many switch statements in your program. Any time you need a resource (a string, a dialog, a menu), you have to set a different resource ID depending on the current language.


<p>There is one resource file (and of course one DLL) for each language. Your
==Use Resource Only DLLs==
only task is to load the right resource DLL when the program starts. You can
Using resource only dynamic link libraries seems to me the better solution.
(should) always use the same resource ID, independent on the selected
language. Because you've loaded the right resource DLL, everything is in the
right language.


<p>OK, it's not all bright sunshine. Each resource you define, you have to
There is one resource file (and of course one DLL) for each language. Your only task is to load the right resource DLL when the program starts. You can (should) always use the same resource ID, independent on the selected language. Because you've loaded the right resource DLL, everything is in the right language.
define for each language. Adding something, you have to take care not to
forget the other resource libraries. And always take care on updates. Watch
out that all resource DLLs are on the same 'resource level'.


<h2>What Do I Need?</h2>
OK, it's not all bright sunshine. Each resource you define, you have to define for each language. Adding something, you have to take care not to forget the other resource libraries. And always take care on updates. Watch out that all resource DLLs are on the same 'resource level'.


<p>First, I think you should create one project and therefore one sub-directory
==What Do I Need?==
for each language. I locate these sub-directories underneath the main
First, I think you should create one project and therefore one sub-directory for each language. I locate these sub-directories underneath the main project's directory.
project's directory.


<p>The main project includes the one and only resource header file (.rch). So
The main project includes the one and only resource header file (.rch). So there is just one place where to keep the declaration of the resources up to date.
there is just one place where to keep the declaration of the resources up to
date.


<p>Each language project has its own resource file (.rc) for the string
Each language project has its own resource file (.rc) for the string table. Accelerator and help tables can be part of a central file of the main project, which has to be rcincluded by the language specific resource files. If you like to change the accelerator keys with the language, then you have to put them inside of each specific resource file too.
table. Accelerator and help tables can be part of a central file of the main
project, which has to be rcincluded by the language specific resource files.
If you like to change the accelerator keys with the language, then you have
to put them inside of each specific resource file too.


<p>You have to decide yourself whether building the menus or dialogs using
You have to decide yourself whether building the menus or dialogs using the string table (means during runtime by setting the text) or defining each menu and dialog separately. I have chosen to define each resource, menus and dialogs too, separately. This is more work during design, but less coding. So these resources are part of each resource file or must be rcincluded.
the string table (means during runtime by setting the text) or defining each
menu and dialog separately. I have chosen to define each resource, menus and
dialogs too, separately. This is more work during design, but less coding.
So these resources are part of each resource file or must be rcincluded.


<p>Beside the 'normal' files like a make or definition file you need an empty
Beside the 'normal' files like a make or definition file you need an empty cpp file for compilation.
cpp file for compilation.


<p>The main project resource file should include one dialog, the language
The main project resource file should include one dialog, the language selection dialog. When the user starts the program the first time, he should be able to select the desired language. This choice can be saved by an initialization file (see [1]) as demonstrated in the sample program.
selection dialog. When the user starts the program the first time, he should
be able to select the desired language. This choice can be saved by an
initialization file (see [1]) as demonstrated in
the sample program.
 
<h2>Load The Right DLL</h2>
 
<p>When all resource only DLLs are built, you have to load the resource DLL
before the main window comes up. This leads to a main function which is a
little bit larger than those of single language programs.
 
<pre><small>


==Load The Right DLL==
When all resource only DLLs are built, you have to load the resource DLL before the main window comes up. This leads to a main function which is a little bit larger than those of single language programs.
<pre>
//*********************************************************
//*********************************************************
// main  - Application entry point                        *
// main  - Application entry point                        *
Line 99: Line 58:
   return (0);
   return (0);
}
}
</pre>
''Figure 2: Main function of single language programs,''


</small></pre>
Figure 3 shows the application entry point of the sample program. Here, the user can set the language also by passing a parameter (-e for English or -d for German).
 
<pre>
<font SIZE=2>
Figure 2: Main function of single language programs,
</font>
 
<p>Figure 3 shows the application entry point of the sample program. Here,
the user can set the language also by passing a parameter (-e for english or
-d for german).
 
<pre><small>
//*********************************************************
//*********************************************************
// main  - Application entry point                        *
// main  - Application entry point                        *
Line 194: Line 146:


   return(0);
   return(0);
}
}</pre>
</small></pre>
''Figure 3: Main function of multilingual programs,''


<font SIZE=2>
Now this is really a little bit more work. Let me tell you what I am doing here.
Figure 3: Main function of multilingual programs,
</font>


<p>Now this is really a little bit more work. Let me tell you what I am doing
First I save the arguments because they are needed by setupLanguage().
here.


<p>First I save the arguments because they are needed by setupLanguage().
Then the start-up value of the language is set by setupLanguage. Please notice that the language is kept as a decimal value, not as a string. This makes it easier to compare and use the value by switch statements.


<p>Then the start-up value of the language is set by setupLanguage. Please
Next, the DLL name is set using the decimal value. I'm using public static methods of the application class AMultilinTest to be sure that I never misspell the DLL name.
notice that the language is kept as a decimal value, not as a string. This
makes it easier to compare and use the value by switch statements.


<p>Next, the DLL name is set using the decimal value. I'm using public static
Now that we know which resource DLL we need, AResourceDLL::loadResourceDLL is called to load it. If this fails (AResourceDLL::loadResourceDLL returns false), a message box will come up and the program terminates. The message box's text is build during runtime. I think it's not worth to put these strings into the program's resources. You still need a switch statement to build the message using the right language when you use resource IDs. You should not call IApplication::current().run() when the program fails to load the resource DLL. Because there is no window the user can close, the program can't be terminated (except the user has one of these nice little process killers like psPM).
methods of the application class AMultilinTest to be sure that I never
misspell the DLL name.


<p>Now that we know which resource DLL we need, AResourceDLL::loadResourceDLL
When the resource DLL is successfully loaded, the program starts like single resource programs.
is called to load it. If this fails (AResourceDLL::loadResourceDLL returns
false), a message box will come up and the program terminates. The message
box's text is build during runtime. I think it's not worth to put these
strings into the program's resources. You still need a switch statement to
build the message using the right language when you use resource IDs. You
should not call IApplication::current().run() when the program fails to load
the resource DLL. Because there is no window the user can close, the program
can't be terminated (except the user has one of these nice little process
killers like psPM).


<p>When the resource DLL is successfully loaded, the program starts like
==A Global Helper - setupLanguage==
single resource programs.
The function setupLanguage is defined in multilin.cpp.
 
<pre>
<h2>A Global Helper - setupLanguage</h2>
 
<p>The function setupLanguage is defined in multilin.cpp.
 
<pre><small>
//**********************************************************
//**********************************************************
// setupLanguage - global function to set the language    *
// setupLanguage - global function to set the language    *
Line 239: Line 170:
{
{
   // we have some runtime arguments
   // we have some runtime arguments
   if(IApplication :: current (). argc () &gt; 1)
   if(IApplication :: current (). argc () > 1)
     {
     {
       if(IApplication :: current (). argv (1). lowerCase ()
       if(IApplication :: current (). argv (1). lowerCase ()
Line 278: Line 209:
   // isn't it none?
   // isn't it none?
   if(pIniConfig
   if(pIniConfig
     &amp;&amp; pIniConfig -&gt; isLanguageSaved ()
     && pIniConfig -> isLanguageSaved ()
     &amp;&amp; pIniConfig -&gt; getLanguage () != AMultilinTest::none)
     && pIniConfig -> getLanguage () != AMultilinTest::none)
     {                                  // yes
     {                                  // yes
       // set the return-value to the saved language
       // set the return-value to the saved language
       ulRet = pIniConfig -&gt; getLanguage ();
       ulRet = pIniConfig -> getLanguage ();
     }
     }
   else
   else
     { // no, bring up the language selection dialog
     { // no, bring up the language selection dialog
       ALanguage * dlgLanguage = new ALanguage ();
       ALanguage * dlgLanguage = new ALanguage ();
       ulRet = dlgLanguage -&gt; showModally();
       ulRet = dlgLanguage -> showModally();
       delete dlgLanguage;
       delete dlgLanguage;
     }
     }
Line 295: Line 226:
   return (ulRet);
   return (ulRet);
}
}
</pre>
''Figure 4: The global function setupLanguage (),''


</small></pre>
setupLanguage is the global function to set the language at the program's start-up.
 
<font SIZE=2>
Figure 4: The global function setupLanguage (),
</font>
 
<p>setupLanguage is the global function to set the language at the program's
start-up.


<p>When any runtime arguments are passed (argc > 1; argc == 1 is only the
When any runtime arguments are passed (argc > 1; argc == 1 is only the program name), the function checks whether the first argument matches one of the defined parameters. If so, setupLangage returns the corresponding language. Otherwise setupLanguage tries to query the language from the initialization file. And at least, when no matching argument is passed and no initialization value is found, a language selection dialog is started. This usually happens only when a program is called the very first time.
program name), the function checks whether the first argument matches one of
the defined parameters. If so, setupLangage returns the corresponding
language. Otherwise setupLanguage tries to query the language from the
initialization file. And at least, when no matching argument is passed and
no initialization value is found, a language selection dialog is started.
This usually happens only when a program is called the very first time.


<h2>The Resource DLL Helper Class</h2>
==The Resource DLL Helper Class==
 
The class AResourceDLL is defined in resdll.hpp and resdll.cpp.
<p>The class AResourceDLL is defined in resdll.hpp and resdll.cpp.
<pre>
 
<pre><small>
class AResourceDLL : public IBase
class AResourceDLL : public IBase


Line 330: Line 248:
                   // load a resource DLL from the app's dir
                   // load a resource DLL from the app's dir


static void loadString(IString&amp; dest,
static void loadString(IString& dest,
                       const unsigned long ulResourceID,
                       const unsigned long ulResourceID,
                       const char * pszText);
                       const char * pszText);
                       // load a string from the resources
                       // load a string from the resources
};
};
</small></pre>
</pre>
''Figure 5: The definition of AResourceDLL,''


<font SIZE=2>
AResourceDLL should help you to handle the different resource DLLs. The methods AResourceDLL::loadResourceDLL and AResourceDLL::loadResDLLFromAppDir calls at least ICurrentApplication::setUserResourceLibrary to change the resource library.
Figure 5: The definition of AResourceDLL,
<pre>
</font>
 
<p>AResourceDLL should help you to handle the different resource DLLs. The
methods AResourceDLL::loadResourceDLL and AResourceDLL::loadResDLLFromAppDir
calls at least ICurrentApplication::setUserResourceLibrary to change the
resource library.
 
<pre><small>
//**********************************************************
//**********************************************************
// AResourceDLL :: loadResourceDLL - load the resource DLL *
// AResourceDLL :: loadResourceDLL - load the resource DLL *
Line 375: Line 286:
     }
     }


   catch (IException&amp;)
   catch (IException&)
   {
   {
   return (AResourceDLL::loadResDLLFromAppDir (strDLLName));
   return (AResourceDLL::loadResDLLFromAppDir (strDLLName));
Line 384: Line 295:
   return (true);
   return (true);
}
}
</small></pre>
</pre>
''Figure 6: AResourceDLL::loadResourceDLL,''


<font SIZE=2>
AResourceDLL::loadResourceDLL first checks whether the passed pointer to the DLL name is valid (means is not equal to 0). If it is 0, IException::assertParameter is called which throws an IAssertationFailure exception.
Figure 6: AResourceDLL::loadResourceDLL,
</font>


<p>AResourceDLL::loadResourceDLL first checks whether the passed pointer to
When the parameter is OK, AResourceDLL::loadResourceDLL tries to load the DLL from the current directory or the libpath. If this fails, AResourceDLL::loadResDLLFromAppDir is called to load the resource DLL from the program's directory.
the DLL name is valid (means is not equal to 0). If it is 0,
<pre>
IException::assertParameter is called which throws an IAssertationFailure
exception.
 
<p>When the parameter is OK, AResourceDLL::loadResourceDLL tries to load the
DLL from the current directory or the libpath. If this fails,
AResourceDLL::loadResDLLFromAppDir is called to load the resource DLL from
the program's directory.
 
 
<pre><small>
//**********************************************************
//**********************************************************
// AResourceDLL :: loadResDLLFromAppDir                    *
// AResourceDLL :: loadResDLLFromAppDir                    *
Line 420: Line 320:
   PTIB pTib = NULL;
   PTIB pTib = NULL;


   DosGetInfoBlocks (&amp;pTib, &amp;pPib);  // get the info blocks
   DosGetInfoBlocks (&pTib, &pPib);  // get the info blocks


   char pszFilename [CCHMAXPATH];
   char pszFilename [CCHMAXPATH];


   if (DosQueryModuleName (pPib -&gt; pib_hmte,
   if (DosQueryModuleName (pPib -> pib_hmte,
                           sizeof (pszFilename),
                           sizeof (pszFilename),
                           pszFilename))
                           pszFilename))
Line 451: Line 351:
       // try to load it
       // try to load it
     }
     }
   catch(IException&amp;)
   catch(IException&)
         {
         {
         // can't load the resource DLL, so return (false)
         // can't load the resource DLL, so return (false)
Line 458: Line 358:


   return (true);        // resource DLL successfully loaded
   return (true);        // resource DLL successfully loaded
}
}</pre>
</small></pre>
''Figure 7: AResourceDLL::loadResDLLFromAppDir,''
 
<font SIZE=2>
Figure 7: AResourceDLL::loadResDLLFromAppDir,
</font>
 
<p>AResourceDLL::loadResDLLFromAppDir also first checks whether the passed
pointer to the DLL name is valid. If not, also IException::assertParameter is
called.
 
<p>AResourceDLL::loadResDLLFromAppDir then queries the full module name
(including drive and path), cuts off the program name and adds the passed DLL
name. When the filename is complete,
ICurrentApplication::setUserResourceLibrary is called to load the resource
library.


<p>In this sample application, also ICurrentApplication::argv(0) can be used
AResourceDLL::loadResDLLFromAppDir also first checks whether the passed pointer to the DLL name is valid. If not, also IException::assertParameter is called.
to get the full module name. But you can't be sure that
ICurrentApplication::setArgs() is always called. So I prefer
DosQueryModuleName() here.


<p>When you look at the source code maybe you will notice that
AResourceDLL::loadResDLLFromAppDir then queries the full module name (including drive and path), cuts off the program name and adds the passed DLL name. When the filename is complete, ICurrentApplication::setUserResourceLibrary is called to load the resource library.
AResourceDLL::loadResourceDLL cuts off the filename's suffix '.DLL' if it is
part of the passed filename, while AResourceDLL::loadResDLLFromAppDir add
this suffix if it's not part of it. I can't tell why this is necessary, but
I've tested it and saw that it is.


In this sample application, also ICurrentApplication::argv(0) can be used to get the full module name. But you can't be sure that ICurrentApplication::setArgs() is always called. So I prefer DosQueryModuleName() here.


<pre><small>
When you look at the source code maybe you will notice that AResourceDLL::loadResourceDLL cuts off the filename's suffix '.DLL' if it is part of the passed filename, while AResourceDLL::loadResDLLFromAppDir add this suffix if it's not part of it. I can't tell why this is necessary, but I've tested it and saw that it is.
<pre>
//*********************************************
//*********************************************
// AResourceDLL :: loadString                *
// AResourceDLL :: loadString                *
// - loads a string from the resource library *
// - loads a string from the resource library *
//*********************************************
//*********************************************
void AResourceDLL :: loadString (IString&amp; dest,
void AResourceDLL :: loadString (IString& dest,
                                 const unsigned long ulResourceID,
                                 const unsigned long ulResourceID,
                                 const char * pszText)
                                 const char * pszText)
Line 504: Line 385:
     }
     }


   catch (IException&amp;)
   catch (IException&)
         {
         {
           dest = pszText;
           dest = pszText;
         }
         }
}
}</pre>
</small></pre>
''Figure 8: AResourceDLL::loadString,''


<font SIZE=2>
AResourceDLL::loadString is the method to load a string from the resources. The parameters are the IString object which should keep the string at least, the resource ID and a text pointer.
Figure 8: AResourceDLL::loadString,
</font>


<p>AResourceDLL::loadString is the method to load a string from the
When the current user resource library doesn't contains a string with the passed ID, IResourceLibrary::loadString throws an exception. In this case the destination string is set to the text. This should avoid empty text in your program.
resources. The parameters are the IString object which should keep the string
at least, the resource ID and a text pointer.


<p>When the current user resource library doesn't contains a string with the
You may ask why I put these functions into a class. They could also be declared as global ones. That's right. But I like to put things together which belongs together. Creating a class therefore is a nice way to it.
passed ID, IResourceLibrary::loadString throws an exception. In this case the
destination string is set to the text. This should avoid empty text in your
program.


<p>You may ask why I put these functions into a class. They could also be
Assume there is a need for different global functions using the same name. I think one good solution is to make these functions unique by putting them into different classes without changing their names. So the function's name in conjunction with the class name will show the task of the function.
declared as global ones. That's right. But I like to put things together
which belongs together. Creating a class therefore is a nice way to it.


<p>Assume there is a need for different global functions using the same name.
If you don't like to type AResourceDLL::... all the time, just derive your class from AResourceDLL.
I think one good solution is to make these functions unique by putting them
into different classes without changing their names. So the function's name
in conjunction with the class name will show the task of the function.


<p>If you don't like to type AResourceDLL::... all the time, just derive your
==Change The Language==
class from AResourceDLL.
The class AMultilinTest is defined in multilin.hpp and multilin.cpp.
 
<pre>
<h2>Change The Language</h2>
 
<p>The class AMultilinTest is defined in multilin.hpp and multilin.cpp.
 
<pre><small>
//**********************************************************
//**********************************************************
// AMultilinTest :: changeLanguage - called to change the  *
// AMultilinTest :: changeLanguage - called to change the  *
//                                  language              *
//                                  language              *
//**********************************************************
//**********************************************************
AMultilinTest&amp; AMultilinTest :: changeLanguage ()
AMultilinTest& AMultilinTest :: changeLanguage ()
{
{
   ALanguage * pDlgLanguage = new ALanguage (this);
   ALanguage * pDlgLanguage = new ALanguage (this);


   unsigned long ulNewLanguage = pDlgLanguage
   unsigned long ulNewLanguage = pDlgLanguage
                                 -&gt; showModally ();
                                 -> showModally ();
   // bring up the language selection dialog
   // bring up the language selection dialog


   delete pDlgLanguage;
   delete pDlgLanguage;


   if (ulNewLanguage == m_pIniConfig -&gt; getLanguage ())
   if (ulNewLanguage == m_pIniConfig -> getLanguage ())
       // did the language changed? no -&gt; return
       // did the language changed? no -> return
       return (*this);
       return (*this);


Line 604: Line 468:


   // change the menu
   // change the menu
   m_pMenuBar -&gt; setMenu (WND_MAIN);
   m_pMenuBar -> setMenu (WND_MAIN);


   return (*this);
   return (*this);
}
}
</small></pre>
</pre>
 
''Figure 9: AMultilinTest::changeLanguage,''
<font SIZE=2>
 
Figure 9: AMultilinTest::changeLanguage,
</font>
 
<p>AMultilinTest::changeLanguage is called each time the user selects the
menu item to change the language.
 
<p>First of all the language selection dialog is shown. If the language has
not changed, the method returns. Otherwise the DLL name is set and
AResourceDLL::loadResourceDLL is called to load the new resource DLL. The
possibly thrown exception is not caught here. This task is done by the
default exception handler of AMultilinTest. Loading the error title and text
from the resource DLL is OK. Because the change was not possible, the old DLL
remains active. After loading the new resource DLL, the new language is
saved, the text of the window title and the menu are switched to the new
values.


<p>Changing the menu on the fly is a little bit dangerous. In doing this you will
AMultilinTest::changeLanguage is called each time the user selects the menu item to change the language.
lose all item states set during runtime. That's no surprise because the menu
changes completely. You can see this at the 'Change language' or 'Sprache
&Auml;ndern' item of the sample application. When the program starts, it is
checked.  As soon as you change the language, it is unchecked.


<p>Three ways came up to me to avoid this. One is to save all item states
First of all the language selection dialog is shown. If the language has not changed, the method returns. Otherwise the DLL name is set and AResourceDLL::loadResourceDLL is called to load the new resource DLL. The possibly thrown exception is not caught here. This task is done by the default exception handler of AMultilinTest. Loading the error title and text from the resource DLL is OK. Because the change was not possible, the old DLL remains active. After loading the new resource DLL, the new language is saved, the text of the window title and the menu are switched to the new values.
before re-loading the menu and reset them. The danger is forgetting one.


<p>The next way is not to reload the complete menu but to change each item's
Changing the menu on the fly is a little bit dangerous. In doing this you will lose all item states set during runtime. That's no surprise because the menu changes completely. You can see this at the 'Change language' or 'Sprache Ändern' item of the sample application. When the program starts, it is checked. As soon as you change the language, it is unchecked.
text. Depending on your menu this may be a lot of coding.


<p>The last is what I prefer. Just save the new language and bring up a
Three ways came up to me to avoid this. One is to save all item states before re-loading the menu and reset them. The danger is forgetting one.
message box that the changes will take effect when the program starts next
time. This saves you from changing all language dependent things (normally
not just the menu but also some status text or whatever). And I think it is
acceptable for the users because they do not change the language ten times a
day.


<p>The rest of the classes is not interesting here. I think it's not too
The next way is not to reload the complete menu but to change each item's text. Depending on your menu this may be a lot of coding.
complicated to understand it when you read the source code.


<h2>The Makefile</h2>
The last is what I prefer. Just save the new language and bring up a message box that the changes will take effect when the program starts next time. This saves you from changing all language dependent things (normally not just the menu but also some status text or whatever). And I think it is acceptable for the users because they do not change the language ten times a day.


<p>When you use VisualAge for C++, you don't have to worry about that. Just
The rest of the classes is not interesting here. I think it's not too complicated to understand it when you read the source code.
generate a project from the project smarts and edit your resources.


<p>Using C Set++ (as I still prefer), you can copy the makefile and the
==The Makefile==
definition file from the sample application. Replacing some strings is all
When you use VisualAge C++, you don't have to worry about that. Just generate a project from the project smarts and edit your resources.
you have to do to use them for new projects.


<h2>The Sources</h2>
Using C Set++ (as I still prefer), you can copy the makefile and the definition file from the sample application. Replacing some strings is all you have to do to use them for new projects.


<p>When you extract the source zip, two sub-directories will be created. They
==The Sources==
are ENGLISH and GERMAN. In these directories you will find the files needed
When you extract the source zip, two sub-directories will be created. They are English and German. In these directories you will find the files needed for the different language resource DLLs.
for the different language resource DLLs.


<p>There are two makefiles for each of the three projects. The ones ending
There are two makefiles for each of the three projects. The ones ending with .mav made for IBM's VisualAge C++. The ones ending with .mac aren't MAC files but the C Set++ makefiles.
with .mav made for IBM's VisualAge for C++. The ones ending with .mac aren't
MAC files but the CSet++ makefiles.


<h2>What's Left To Say?</h2>
==What's Left To Say?==
When you write a program perhaps you also include on-line help. Adding language specific help files is as easy as loading different resource DLLs. Just use IHelpWindow::addLibraries. Maybe you like to transform AResourceDLL into an AHelpFile class to handle the loading from different directories.


<p>When you write a program perhaps you also include on-line help. Adding
[1] Stefan Ruck, [[Manage Your Configuration Files and Data]], EDM/2 volume 5 issue 9, 1997
language specific help files is as easy as loading different resource DLLs.
Just use IHelpWindow::addLibraries. Maybe you like to transform AResourceDLL
into a AHelpFile class to handle the loading from different directories.


<p>[1] Stefan Ruck, Manage Your Configuration Files and Data,
[[Category:C++ Articles]]
    <a HREF="../0509/index.html">EDM/2 volume 5 issue 9, 1997</a>

Latest revision as of 03:31, 9 November 2018

Written by Stefan Ruck

You want your program to be multilingual? Why not, it's really easy to do. I want to show you what you need.

How To Do It?

I should better ask: How not to do it?

One way occurred to me is as follows:

You can write all languages needed into one resource file and add a language key to the resource ID. Here's a sample:

STRINGTABLE
  BEGIN
    STR_TEXT_E, "The English text"
    STR_TEXT_D, "Der deutsche Text"
  END

Figure 1: Sample using one resource file for all languages

But this leads to many switch statements in your program. Any time you need a resource (a string, a dialog, a menu), you have to set a different resource ID depending on the current language.

Use Resource Only DLLs

Using resource only dynamic link libraries seems to me the better solution.

There is one resource file (and of course one DLL) for each language. Your only task is to load the right resource DLL when the program starts. You can (should) always use the same resource ID, independent on the selected language. Because you've loaded the right resource DLL, everything is in the right language.

OK, it's not all bright sunshine. Each resource you define, you have to define for each language. Adding something, you have to take care not to forget the other resource libraries. And always take care on updates. Watch out that all resource DLLs are on the same 'resource level'.

What Do I Need?

First, I think you should create one project and therefore one sub-directory for each language. I locate these sub-directories underneath the main project's directory.

The main project includes the one and only resource header file (.rch). So there is just one place where to keep the declaration of the resources up to date.

Each language project has its own resource file (.rc) for the string table. Accelerator and help tables can be part of a central file of the main project, which has to be rcincluded by the language specific resource files. If you like to change the accelerator keys with the language, then you have to put them inside of each specific resource file too.

You have to decide yourself whether building the menus or dialogs using the string table (means during runtime by setting the text) or defining each menu and dialog separately. I have chosen to define each resource, menus and dialogs too, separately. This is more work during design, but less coding. So these resources are part of each resource file or must be rcincluded.

Beside the 'normal' files like a make or definition file you need an empty cpp file for compilation.

The main project resource file should include one dialog, the language selection dialog. When the user starts the program the first time, he should be able to select the desired language. This choice can be saved by an initialization file (see [1]) as demonstrated in the sample program.

Load The Right DLL

When all resource only DLLs are built, you have to load the resource DLL before the main window comes up. This leads to a main function which is a little bit larger than those of single language programs.

//*********************************************************
// main  - Application entry point                        *
//*********************************************************
int main(int argc, char ** argv)
{
  IApplication :: current(). setArgs (argc, argv);

  // Create our main window
  AMyMainWindow mainWindow (WND_MAIN);

  // Get the current application and run it
  IApplication :: current (). run ();

  return (0);
}

Figure 2: Main function of single language programs,

Figure 3 shows the application entry point of the sample program. Here, the user can set the language also by passing a parameter (-e for English or -d for German).

//*********************************************************
// main  - Application entry point                        *
//*********************************************************
int main(int argc, char ** argv)
{
  IApplication :: current(). setArgs (argc, argv);

  unsigned long ulLanguage = setupLanguage ();

  const char * pszDLLName;

  switch (ulLanguage)
          {
           case AMultilinTest :: english:
             pszDLLName = AMultilinTest :: englishDLLName();
             break;
           case AMultilinTest :: german:
             pszDLLName = AMultilinTest :: germanDLLName ();
             break;
           default:
             pszDLLName = AMultilinTest :: englishDLLName();
             break;
          }

  try
     { // try to load the language specific resource DLL
      if (!AResourceDLL :: loadResourceDLL (pszDLLName))
          {
           IMessageBox msgBox (IWindow :: desktopWindow ());
           msgBox .setTitle ("Muiltilin.exe");
           IString strText;
           switch (ulLanguage)
            {
             case AMultilinTest :: english:
              strText = "Can not load ResourceDLL ";
              strText += AMultilinTest :: englishDLLName ();
              strText += ".";
              break;

             case AMultilinTest :: german:
              strText = "Kann ResourceDLL ";
              strText += AMultilinTest :: germanDLLName ();
              strText += " nicht laden.";
              break;

             default:
              strText = "Can not load ResourceDLL ";
              strText += AMultilinTest :: englishDLLName ();
              strText += ".";
              break;
           }
           msgBox.show (strText,
                        IMessageBox::cancelButton
                        | IMessageBox::errorIcon
                        | IMessageBox::applicationModal
                        | IMessageBox::moveable);
          // do not call IApplication :: current (). run (),
          // you can never stop it again
           exit (3);
          }
     }
  catch (IException& rException)
         { // the parameter passed to
           // AResourceDLL :: loadResourceDLL was wrong
          IMessageBox msgBox (IWindow :: desktopWindow ());
          msgBox. setTitle (rException. name ());
          msgBox.show (buildExceptionText (rException),
                       IMessageBox::cancelButton
                       | IMessageBox::errorIcon
                       | IMessageBox::applicationModal
                       | IMessageBox::moveable);
          // do not call IApplication :: current (). run (),
          // you can never stop it again
          exit (3);
         }

  // Create our main window on the desktop
  AMultilinTest mainWindow (WND_MAIN, ulLanguage);

  // Get the current application and run it
  IApplication :: current (). run ();

  return(0);
}

Figure 3: Main function of multilingual programs,

Now this is really a little bit more work. Let me tell you what I am doing here.

First I save the arguments because they are needed by setupLanguage().

Then the start-up value of the language is set by setupLanguage. Please notice that the language is kept as a decimal value, not as a string. This makes it easier to compare and use the value by switch statements.

Next, the DLL name is set using the decimal value. I'm using public static methods of the application class AMultilinTest to be sure that I never misspell the DLL name.

Now that we know which resource DLL we need, AResourceDLL::loadResourceDLL is called to load it. If this fails (AResourceDLL::loadResourceDLL returns false), a message box will come up and the program terminates. The message box's text is build during runtime. I think it's not worth to put these strings into the program's resources. You still need a switch statement to build the message using the right language when you use resource IDs. You should not call IApplication::current().run() when the program fails to load the resource DLL. Because there is no window the user can close, the program can't be terminated (except the user has one of these nice little process killers like psPM).

When the resource DLL is successfully loaded, the program starts like single resource programs.

A Global Helper - setupLanguage

The function setupLanguage is defined in multilin.cpp.

//**********************************************************
// setupLanguage - global function to set the language     *
//**********************************************************
unsigned long setupLanguage (void)
{
  // we have some runtime arguments
  if(IApplication :: current (). argc () > 1)
     {
      if(IApplication :: current (). argv (1). lowerCase ()
                              . isAbbreviationFor ("-e", 2))
         // the user selected english
         return (AMultilinTest :: english);
      else
         if(IApplication :: current (). argv (1)
                            . lowerCase ()
                            . isAbbreviationFor ("-d", 2))
            // the user selected german
            return (AMultilinTest :: german);
     }

  AIniConfig * pIniConfig = 0L;

  try
     { // initialize the initialization file
      pIniConfig = new AIniConfig ();
     }
  catch(IException& rException)              // any problems
        {                             // show the error text
         IMessageBox msgBox (IWindow :: desktopWindow ());
         msgBox. setTitle (rException. name ());
         msgBox.show (buildExceptionText (rException),
                      IMessageBox::okButton
                      | IMessageBox::informationIcon
                      | IMessageBox::applicationModal
                      | IMessageBox::moveable);
        }

  // set the language

  unsigned long ulRet;

  // do we have an initialization file?
  // was a value for the language saved?
  // isn't it none?
  if(pIniConfig
     && pIniConfig -> isLanguageSaved ()
     && pIniConfig -> getLanguage () != AMultilinTest::none)
     {                                   // yes
      // set the return-value to the saved language
      ulRet = pIniConfig -> getLanguage ();
     }
  else
     { // no, bring up the language selection dialog
      ALanguage * dlgLanguage = new ALanguage ();
      ulRet = dlgLanguage -> showModally();
      delete dlgLanguage;
     }

  delete pIniConfig;

  return (ulRet);
}

Figure 4: The global function setupLanguage (),

setupLanguage is the global function to set the language at the program's start-up.

When any runtime arguments are passed (argc > 1; argc == 1 is only the program name), the function checks whether the first argument matches one of the defined parameters. If so, setupLangage returns the corresponding language. Otherwise setupLanguage tries to query the language from the initialization file. And at least, when no matching argument is passed and no initialization value is found, a language selection dialog is started. This usually happens only when a program is called the very first time.

The Resource DLL Helper Class

The class AResourceDLL is defined in resdll.hpp and resdll.cpp.

class AResourceDLL : public IBase

{
public:
static Boolean loadResourceDLL(const char * pszDLLName);
                   // throws IAssertationFailure
                   // load a resource DLL

static Boolean loadResDLLFromAppDir (const char * pszDLLName);
                   // throws IAssertationFailure
                   // load a resource DLL from the app's dir

static void loadString(IString& dest,
                       const unsigned long ulResourceID,
                       const char * pszText);
                       // load a string from the resources
};

Figure 5: The definition of AResourceDLL,

AResourceDLL should help you to handle the different resource DLLs. The methods AResourceDLL::loadResourceDLL and AResourceDLL::loadResDLLFromAppDir calls at least ICurrentApplication::setUserResourceLibrary to change the resource library.

//**********************************************************
// AResourceDLL :: loadResourceDLL - load the resource DLL *
//                                                         *
// throws IAssertationFailure                              *
//**********************************************************
Boolean AResourceDLL :: loadResourceDLL (const char * pszDLLName)

{
  if (!pszDLLName)
      IException :: assertParameter ("pszDLLName is NULL!",
                    IExceptionLocation ("resdll.cpp",
                           "AResourceDLL :: loadResourceDLL",
                           28));

  IString strDLLName (pszDLLName);

  strDLLName. upperCase ();

  // remove the filename suffix
  strDLLName. remove (strDLLName. lastIndexOf (".DLL"));

  try
     {
      IApplication :: current ()
                      . setUserResourceLibrary (strDLLName);
      // try to load the DLL from the current directory (or path)
     }

  catch (IException&)
  {
   return (AResourceDLL::loadResDLLFromAppDir (strDLLName));
   // try to load the resource from the app's dir and return
   // loadResDllFromAppDir's return value
  }

  return (true);
}

Figure 6: AResourceDLL::loadResourceDLL,

AResourceDLL::loadResourceDLL first checks whether the passed pointer to the DLL name is valid (means is not equal to 0). If it is 0, IException::assertParameter is called which throws an IAssertationFailure exception.

When the parameter is OK, AResourceDLL::loadResourceDLL tries to load the DLL from the current directory or the libpath. If this fails, AResourceDLL::loadResDLLFromAppDir is called to load the resource DLL from the program's directory.

//**********************************************************
// AResourceDLL :: loadResDLLFromAppDir                    *
// -load the resource DLL from the application's directory *
//                                                         *
// throws IAssertationFailure                              *
//**********************************************************
Boolean AResourceDLL :: loadResDLLFromAppDir (const char * pszDLLName)

{
  if (!pszDLLName)
    IException :: assertParameter ("pszDLLName is NULL!",
                   IExceptionLocation ("resdll.cpp",
                    "AResourceDLL :: loadResDLLFromAppDir",
                    52));

  PPIB pPib = NULL;
  PTIB pTib = NULL;

  DosGetInfoBlocks (&pTib, &pPib);  // get the info blocks

  char pszFilename [CCHMAXPATH];

  if (DosQueryModuleName (pPib -> pib_hmte,
                          sizeof (pszFilename),
                          pszFilename))
      // can't query module name? return (false)
      return (false);

  IString strFilename = pszFilename; // keep the module name

  strFilename. remove (strFilename. lastIndexOf ('\\'));
                // remove the filename, leaves the path

  strFilename += "\\";               // add the \

  strFilename += pszDLLName;         // add the DLL name

  strFilename. upperCase ();

  // do we have the DLL suffix
  if (!strFilename. lastIndexOf (".DLL"))
      strFilename += ".DLL";         // no, so add it

  try
     {
      IApplication :: current ()
                     . setUserResourceLibrary (strFilename);
      // try to load it
     }
  catch(IException&)
        {
         // can't load the resource DLL, so return (false)
         return (false);
        }

  return (true);         // resource DLL successfully loaded
}

Figure 7: AResourceDLL::loadResDLLFromAppDir,

AResourceDLL::loadResDLLFromAppDir also first checks whether the passed pointer to the DLL name is valid. If not, also IException::assertParameter is called.

AResourceDLL::loadResDLLFromAppDir then queries the full module name (including drive and path), cuts off the program name and adds the passed DLL name. When the filename is complete, ICurrentApplication::setUserResourceLibrary is called to load the resource library.

In this sample application, also ICurrentApplication::argv(0) can be used to get the full module name. But you can't be sure that ICurrentApplication::setArgs() is always called. So I prefer DosQueryModuleName() here.

When you look at the source code maybe you will notice that AResourceDLL::loadResourceDLL cuts off the filename's suffix '.DLL' if it is part of the passed filename, while AResourceDLL::loadResDLLFromAppDir add this suffix if it's not part of it. I can't tell why this is necessary, but I've tested it and saw that it is.

//*********************************************
// AResourceDLL :: loadString                 *
// - loads a string from the resource library *
//*********************************************
void AResourceDLL :: loadString (IString& dest,
                                 const unsigned long ulResourceID,
                                 const char * pszText)

{
  try
     {
      dest = IApplication :: current ()
                             . userResourceLibrary ()
                             .loadString (ulResourceID);
     }

  catch (IException&)
         {
          dest = pszText;
         }
}

Figure 8: AResourceDLL::loadString,

AResourceDLL::loadString is the method to load a string from the resources. The parameters are the IString object which should keep the string at least, the resource ID and a text pointer.

When the current user resource library doesn't contains a string with the passed ID, IResourceLibrary::loadString throws an exception. In this case the destination string is set to the text. This should avoid empty text in your program.

You may ask why I put these functions into a class. They could also be declared as global ones. That's right. But I like to put things together which belongs together. Creating a class therefore is a nice way to it.

Assume there is a need for different global functions using the same name. I think one good solution is to make these functions unique by putting them into different classes without changing their names. So the function's name in conjunction with the class name will show the task of the function.

If you don't like to type AResourceDLL::... all the time, just derive your class from AResourceDLL.

Change The Language

The class AMultilinTest is defined in multilin.hpp and multilin.cpp.

//**********************************************************
// AMultilinTest :: changeLanguage - called to change the  *
//                                   language              *
//**********************************************************
AMultilinTest& AMultilinTest :: changeLanguage ()
{
  ALanguage * pDlgLanguage = new ALanguage (this);

  unsigned long ulNewLanguage = pDlgLanguage
                                -> showModally ();
  // bring up the language selection dialog

  delete pDlgLanguage;

  if (ulNewLanguage == m_pIniConfig -> getLanguage ())
      // did the language changed? no -> return
      return (*this);

  const char * pszDLLName;

  switch (ulNewLanguage)                 // set the DLL name
          {
           case AMultilinTest :: english:
                pszDLLName = englishDLLName ();
                break;
           case AMultilinTest :: german:
                pszDLLName = germanDLLName ();
                break;
           default:
                pszDLLName = englishDLLName ();
                break;
          }

  IString strText;

  if (!AResourceDLL :: loadResourceDLL (pszDLLName))
      {                       // can't load the resource DLL
       IMessageBox msgBox (this);
       AResourceDLL :: loadString (strText,
                                   STR_CHANGE_LANGUAGE,
                                   "Change Language");
       msgBox. setTitle (strText);
       AResourceDLL :: loadString (strText,
                     STR_ERROR_LOAD_DLL,
                     "Can't load the needed resource DLL.");
       msgBox. show (strText, IMessageBox::okButton
                              | IMessageBox::informationIcon
                              | IMessageBox::applicationModal
                              | IMessageBox::moveable);
       return (*this);
      }

  // save the new language
  setLanguage (ulNewLanguage);

  // load the title
  AResourceDLL :: loadString (strText, WND_MAIN,
                "Multilingual Resource Sample Application");

  // change the title
  ITitle title (this, strText);

  // change the menu
  m_pMenuBar -> setMenu (WND_MAIN);

  return (*this);
}

Figure 9: AMultilinTest::changeLanguage,

AMultilinTest::changeLanguage is called each time the user selects the menu item to change the language.

First of all the language selection dialog is shown. If the language has not changed, the method returns. Otherwise the DLL name is set and AResourceDLL::loadResourceDLL is called to load the new resource DLL. The possibly thrown exception is not caught here. This task is done by the default exception handler of AMultilinTest. Loading the error title and text from the resource DLL is OK. Because the change was not possible, the old DLL remains active. After loading the new resource DLL, the new language is saved, the text of the window title and the menu are switched to the new values.

Changing the menu on the fly is a little bit dangerous. In doing this you will lose all item states set during runtime. That's no surprise because the menu changes completely. You can see this at the 'Change language' or 'Sprache Ändern' item of the sample application. When the program starts, it is checked. As soon as you change the language, it is unchecked.

Three ways came up to me to avoid this. One is to save all item states before re-loading the menu and reset them. The danger is forgetting one.

The next way is not to reload the complete menu but to change each item's text. Depending on your menu this may be a lot of coding.

The last is what I prefer. Just save the new language and bring up a message box that the changes will take effect when the program starts next time. This saves you from changing all language dependent things (normally not just the menu but also some status text or whatever). And I think it is acceptable for the users because they do not change the language ten times a day.

The rest of the classes is not interesting here. I think it's not too complicated to understand it when you read the source code.

The Makefile

When you use VisualAge C++, you don't have to worry about that. Just generate a project from the project smarts and edit your resources.

Using C Set++ (as I still prefer), you can copy the makefile and the definition file from the sample application. Replacing some strings is all you have to do to use them for new projects.

The Sources

When you extract the source zip, two sub-directories will be created. They are English and German. In these directories you will find the files needed for the different language resource DLLs.

There are two makefiles for each of the three projects. The ones ending with .mav made for IBM's VisualAge C++. The ones ending with .mac aren't MAC files but the C Set++ makefiles.

What's Left To Say?

When you write a program perhaps you also include on-line help. Adding language specific help files is as easy as loading different resource DLLs. Just use IHelpWindow::addLibraries. Maybe you like to transform AResourceDLL into an AHelpFile class to handle the loading from different directories.

[1] Stefan Ruck, Manage Your Configuration Files and Data, EDM/2 volume 5 issue 9, 1997