|
|
Line 4: |
Line 4: |
| 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? | | I should better ask: How not to do it? |
Line 13: |
Line 13: |
| language key to the resource ID. Here's a sample: | | language key to the resource ID. Here's a sample: |
|
| |
|
| <pre><small> | | <pre> |
| STRINGTABLE | | STRINGTABLE |
| BEGIN | | BEGIN |
Line 19: |
Line 19: |
| STR_TEXT_D, "Der deutsche Text" | | STR_TEXT_D, "Der deutsche Text" |
| END | | END |
| </small></pre>
| | </pre> |
|
| |
|
| <font SIZE=2> | | <font SIZE=2> |
Line 27: |
Line 27: |
| 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. | | 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>
| | ==Use Resource Only DLLs== |
|
| |
|
| Using resource only dynamic link libraries seems to me the better solution. | | Using resource only dynamic link libraries seems to me the better solution. |
Line 38: |
Line 38: |
| out that all resource DLLs are on the same 'resource level'. | | out that all resource DLLs are on the same 'resource level'. |
|
| |
|
| <h2>What Do I Need?</h2>
| | ==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. | | 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. |
Line 45: |
Line 45: |
|
| |
|
| 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 ma |
| | |
| 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.
| |
| | |
| <h2>Load The Right DLL</h2>
| |
| | |
| 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>
| |
| | |
| //*********************************************************
| |
| // 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);
| |
| }
| |
| | |
| </small></pre>
| |
| | |
| <font SIZE=2>
| |
| Figure 2: Main function of single language programs,
| |
| </font>
| |
| | |
| 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 *
| |
| //*********************************************************
| |
| 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);
| |
| }
| |
| </small></pre>
| |
| | |
| <font SIZE=2>
| |
| Figure 3: Main function of multilingual programs,
| |
| </font>
| |
| | |
| 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.
| |
| | |
| <h2>A Global Helper - setupLanguage</h2>
| |
| | |
| The function setupLanguage is defined in multilin.cpp.
| |
| | |
| <pre><small>
| |
| //**********************************************************
| |
| // 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);
| |
| }
| |
| | |
| </small></pre>
| |
| | |
| <font SIZE=2>
| |
| Figure 4: The global function setupLanguage (),
| |
| </font>
| |
| | |
| 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.
| |
| | |
| <h2>The Resource DLL Helper Class</h2>
| |
| | |
| The class AResourceDLL is defined in resdll.hpp and resdll.cpp.
| |
| | |
| <pre><small>
| |
| 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
| |
| };
| |
| </small></pre>
| |
| | |
| <font SIZE=2>
| |
| Figure 5: The definition of AResourceDLL,
| |
| </font>
| |
| | |
| 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 *
| |
| // *
| |
| // 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);
| |
| }
| |
| </small></pre>
| |
| | |
| <font SIZE=2>
| |
| Figure 6: AResourceDLL::loadResourceDLL,
| |
| </font>
| |
| | |
| 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.
| |
| | |
| <pre><small>
| |
| //**********************************************************
| |
| // 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
| |
| }
| |
| </small></pre>
| |
| | |
| <font SIZE=2>
| |
| Figure 7: AResourceDLL::loadResDLLFromAppDir,
| |
| </font>
| |
| | |
| 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.
| |
| | |
| <pre><small>
| |
| //*********************************************
| |
| // 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;
| |
| }
| |
| }
| |
| </small></pre>
| |
| | |
| <font SIZE=2>
| |
| Figure 8: AResourceDLL::loadString,
| |
| </font>
| |
| | |
| 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.
| |
| | |
| <h2>Change The Language</h2>
| |
| | |
| The class AMultilinTest is defined in multilin.hpp and multilin.cpp.
| |
| | |
| <pre><small>
| |
| //**********************************************************
| |
| // 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);
| |
| }
| |
| </small></pre>
| |
| | |
| <font SIZE=2>
| |
| | |
| Figure 9: AMultilinTest::changeLanguage,
| |
| </font>
| |
| | |
| 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.
| |
| | |
| <h2>The Makefile</h2>
| |
| | |
| 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.
| |
| | |
| <h2>The Sources</h2>
| |
| | |
| 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.
| |
| | |
| <h2>What's Left To Say?</h2>
| |
| | |
| 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,
| |
| <a HREF="../0509/index.html">EDM/2 volume 5 issue 9, 1997</a>
| |
| | |
| [[Category:Localization Articles]]
| |