Multilingual Resources

From EDM2
Jump to: navigation, search

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. Anytime 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 for 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 for C++. The ones ending with .mac aren't MAC files but the CSet++ 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 a 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