Click here to Skip to main content
15,919,245 members
Articles / Desktop Programming / MFC

Yet another way to localize your MFC apps

Rate me:
Please Sign up or sign in to vote.
3.50/5 (12 votes)
27 Jun 2005CPOL3 min read 54.1K   463   15   21
Another way to localize your MFC application.

Introduction

This article will show you another way to localize your MFC apps. I'm sure it's not the best way, but it works for me.

History

There are many, many articles on the Internet on this topic (even on this site). Maybe you know the way Microsoft planned to do this in MFC, using resource DLLs: if you put all your program resources in a DLL, and you call it, for instance, "appSPA.dll", then when your app runs in Spanish Windows, it loads this DLL and your app is shown in Spanish.

This sounds good at first. But let's put this into practice. Let's say I have an application I want to translate to five languages (at least). Quickly I will realise I'll have to face two big problems:

  1. I have to give my DLL to a poor translator that knows nothing about programming, DLL libraries, ... How will he/she manage to do it? Not without our help and our time...
  2. And the big one: If I make changes to a resource, I have to replicate those changes to all the other language DLLs! What a nightmare! I'll surely forget some changes... what a mess!

Another approach

To get rid of the first problem, you must use plain text files, so that's what I have done. Then, my application loads them and does the translation automatically. You only have to put all your program strings in the app resources (this is a must!).

And how do we get rid of the second one ? That's easier! You only have to make all your dialogs translate themselves, and the app menu too.

Some code...

To illustrate all this, I've made a class called CTranslator. This class can translate any string, any dialog, any menu, any message box, ...

First, we must load our language file (remember, it is plain text!) using LoadLanguage(). Now we can use all the other methods...

To translate a string:

C++
CString str = GetTranslatedString(IDS_STRING);

Let's see how it works....

First we load the string from the resources...

C++
CString CTranslator::GetTranslatedString(UINT uiID)
{
    CString strOriginal;

    strOriginal.LoadString(uiID);

    if (strOriginal.IsEmpty())
    {
        return _T("");
    }

    return GetTranslatedString(strOriginal);
}

Here is how the real translation occurs...

C++
CString CTranslator::GetTranslatedString(CString strOriginal)
{
    if (strOriginal.IsEmpty())
    {
        return _T("");
    }

    CString s = strOriginal;

    TakeOutBlanksArroundTab(s);

    s = "[" + s + "]";

    int iIndex = -1;

    const int iInfoSize = (int)m_info.size();

    for (int i=0; i<iInfoSize; i++)
    {
        char szAux[256];
        strcpy(szAux, m_info[i].szOriginal);

        if (strcmp(m_info[i].szOriginal, LPCTSTR(s)) == 0)
        {
            iIndex = i;
            break;
        }
    }

    if (iIndex == -1)
    {
        return strOriginal;
    }

    CString strTranslated(m_info[iIndex].szTranslated);

    if (strTranslated.IsEmpty())
    {
        return strOriginal;
    }

//     TRACE2("Original : [%s]\tTranslated : [%s]\n", LPCTSTR(strOriginal), 
//            LPCTSTR(strTranslated));

    return strTranslated;
}

The original text is between brackets (see attached file "french.txt" to see what I mean). In m_info, we store the strings, the original version, and the translated version. We only have to search...

To translate a dialog, simply put in OnInitDialog() a call to TranslateDialog(this);.

But how is the translation of a dialog done? There is a function that searches for all dialog controls and tries to translate them.

C++
void CTranslator::TranslateDialog(CDialog *pDialog)
{
    CString strOriginal(_T(""));

    for (int iID = 0; iID < _APS_NEXT_CONTROL_VALUE; iID++)
    {
        pDialog->GetDlgItemText(iID, strOriginal);

        if (!strOriginal.IsEmpty())
        {
            CString s = GetTranslatedString(strOriginal);

            // TRACE2("%s is translated to %s\n", LPCTSTR(strOriginal), LPCTSTR(s));

            if (!s.IsEmpty() && strOriginal != s)
            {
                pDialog->SetDlgItemText(iID, s);
            }
        }
    }
    
    // Also translate dialog caption

    strOriginal = _T("");

    pDialog->GetWindowText(strOriginal);

    if (!strOriginal.IsEmpty())
    {
        CString s = GetTranslatedString(strOriginal);

        if (!s.IsEmpty() && strOriginal != s)
        {
            pDialog->SetWindowText(s);
        }
    }
}

Also, I've managed to use AfxMessageBox without any problems... Simply use this function (instead of ::AfxMessageBox):

C++
int CTranslator::AfxMessageBox(LPCTSTR lpszText, UINT nType, UINT nIDHelp)
{
    CString strOriginal = CString(lpszText);

    CString s = GetTranslatedString(strOriginal);

    return ::AfxMessageBox(s, nType, nIDHelp);
}

int AFXAPI CTranslator::AfxMessageBox(UINT nIDPrompt, UINT nType, UINT nIDHelp)
{
    CString strOriginal;

    strOriginal.LoadString(nIDPrompt);

    CString s = GetTranslatedString(strOriginal);

    return ::AfxMessageBox(s, nType, nIDHelp);
}

void CTranslator::AfxFormatString1(CString & rString, UINT nIDS, LPCTSTR lpsz1)
{
    CString strFormat = GetTranslatedString(nIDS);
    
    AfxFormatStrings(rString, strFormat, &lpsz1, 1);
}

void CTranslator::AfxFormatString2(CString & rString, UINT nIDS, LPCTSTR lpsz1, LPCTSTR lpsz2)
{
    LPCTSTR rglpsz[2];
    rglpsz[0] = lpsz1;
    rglpsz[1] = lpsz2;

    CString strFormat = GetTranslatedString(nIDS);
    
    AfxFormatStrings(rString, strFormat, rglpsz, 2);
}

And for translating the menu, I've created a recursive method...

C++
void CTranslator::TranslateMenu(CFrameWnd *pFrameWnd)
{
    ASSERT(pFrameWnd != NULL);

    // Destroy previous menu.

    pFrameWnd->SetMenu(NULL);

    if (::IsMenu(pFrameWnd->m_hMenuDefault))
    {
        ::DestroyMenu(pFrameWnd->m_hMenuDefault);
    }

    // Load default spanish menu

    CMenu *pMenu = new CMenu();

    ASSERT(pMenu != NULL);

    pMenu->LoadMenu(IDR_MAINFRAME);

    ::SetMenu(pFrameWnd->GetSafeHwnd(), pMenu->GetSafeHmenu());

    pFrameWnd->m_hMenuDefault = pMenu->GetSafeHmenu();

    // Now we can translate the menu...

    TranslateMenu(pMenu);

    // Detach the menu handle as we do not want to loose it.

    pMenu->Detach();

    delete pMenu;
    
    pMenu = NULL;

    // Redraw the new menu

    pFrameWnd->DrawMenuBar();
}

void CTranslator::TranslateMenu(CMenu *pMenu)
{
    CString strOriginal(_T(""));
    CString strTranslated(_T(""));

    WORD wMenuState;

    if (pMenu == NULL || !::IsMenu(pMenu->m_hMenu))
    {
        return;
    }

    int iSize = pMenu->GetMenuItemCount();

    // loop all the menu items in this level

    MENUITEMINFO menuItemInfo;

    for (int i=0; i<iSize; i++)
    {
        wMenuState = (WORD) pMenu->GetMenuState(i, MF_BYPOSITION);

        BOOL bIsPopup = wMenuState & MF_POPUP;

        // Get the menu string
        // pMenu->GetMenuString(i, strOriginal, MF_BYPOSITION);

        ZeroMemory(&menuItemInfo, sizeof(MENUITEMINFO));

        menuItemInfo.cbSize = sizeof(MENUITEMINFO);
        menuItemInfo.fMask = MIIM_TYPE;

        pMenu->GetMenuItemInfo(i, &menuItemInfo, TRUE);

        if (menuItemInfo.cch > 0)
        {
            menuItemInfo.cch++;
            menuItemInfo.dwTypeData = new char [menuItemInfo.cch];

            pMenu->GetMenuItemInfo(i, &menuItemInfo, TRUE);
            
            strOriginal = CString(menuItemInfo.dwTypeData);

            delete [] menuItemInfo.dwTypeData;
            menuItemInfo.dwTypeData = NULL;
        }
        else
        {
            strOriginal = _T("");
        }

        if (!strOriginal.IsEmpty())
        {
            strTranslated = GetTranslatedString(strOriginal);

            if (!strTranslated.IsEmpty() && strTranslated != strOriginal)
            {
                UINT uiID = 0;
                UINT uiFlags = MF_STRING | MF_BYPOSITION;

                uiID = pMenu->GetMenuItemID(i);

                if (bIsPopup)
                {
                    uiFlags |= MF_POPUP;
                    
                    HMENU hPopupMenu = pMenu->GetSubMenu(i)->m_hMenu;

                    pMenu->ModifyMenu(i, uiFlags, (UINT)hPopupMenu, strTranslated);
                }
                else
                {
                    pMenu->ModifyMenu(i, uiFlags, uiID, strTranslated);
                }
            }
        }

        if (bIsPopup) // is a popup, so add in the submenus
        {
            CMenu *pSubMenu = pMenu->GetSubMenu(i);

            if (pSubMenu != NULL && ::IsMenu(pSubMenu->m_hMenu))
            {
                TranslateMenu(pSubMenu);
            }
        }
    }
}

But this won't work if you use Bent Cockrum's Cool Owner Drawn Menus... In the sample file, you'll find the TranslateBCMenu method that does the same work as TranslateMenu, but for this type of menu.

That's it?

Of course, that's not all... There are questions unsolved....How do you make this language file? How do you update it? Well... I've done a little application called res2txt that scans all program resources and builds/updates the language file... I'm planning an article explaining how this is done.

Final notes

For this code to work, you can't have all your dialog static text controls called "IDC_STATIC", as Microsoft Visual Studio does. They must have different IDs.

There are a lot of people who are uncomfortable having plain text files with their application. You can simply compress or encrypt (or both!) your language files before distributing them (in fact, this is what I do).

As suggested by Mihai Nita, I'd like to point out that this localization approach is only for Latin (Roman) languages. I have not tried translating Asiatic or Arabic languages. You must have noticed that the source code above is not UNICODE friendly at all!

Also, if you have any comments, doubts, criticisms, ... feel free to post them here, I'll check and I'll try to answer all of them.

Thanks

I'd like to thank Brent Corkum for his Cool Owner Drawn Menus.

And also thanks to CodeProject, where learning is possible...

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Spain Spain
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralOpensource solutions to localize application Pin
Rolf Kristensen21-Mar-11 0:23
Rolf Kristensen21-Mar-11 0:23 
GeneralMy vote of 5 Pin
andwan02-Feb-11 5:32
andwan02-Feb-11 5:32 
GeneralYet another way to localize your MFC apps Pin
whiteduke19-Dec-10 20:55
whiteduke19-Dec-10 20:55 
GeneralRe: Yet another way to localize your MFC apps Pin
Isildur10-Jan-11 10:07
Isildur10-Jan-11 10:07 
QuestionHow About Using a XML file instead of Text File Pin
Shridhar127-Aug-10 1:21
Shridhar127-Aug-10 1:21 
AnswerRe: How About Using a XML file instead of Text File Pin
Isildur27-Aug-10 22:51
Isildur27-Aug-10 22:51 
GeneralNice article Pin
Hillaryy26-Nov-07 3:37
Hillaryy26-Nov-07 3:37 
GeneralThanks for the source Pin
General Diensten26-Apr-06 23:42
General Diensten26-Apr-06 23:42 
GeneralIt's not really a bad idea at all Pin
Joey Bloggs28-Jun-05 22:05
Joey Bloggs28-Jun-05 22:05 
GeneralRe: It's not really a bad idea at all Pin
Isildur29-Jun-05 0:41
Isildur29-Jun-05 0:41 
GeneralRe: It's not really a bad idea at all Pin
David Patrick29-Jun-05 2:38
David Patrick29-Jun-05 2:38 
GeneralRe: It's not really a bad idea at all Pin
Isildur4-Jul-05 11:52
Isildur4-Jul-05 11:52 
GeneralBad idea Pin
Mihai Nita27-Jun-05 20:21
Mihai Nita27-Jun-05 20:21 
GeneralRe: Bad idea Pin
Isildur28-Jun-05 0:48
Isildur28-Jun-05 0:48 
GeneralRe: Bad idea Pin
Sudhir Mangla28-Jun-05 2:34
professionalSudhir Mangla28-Jun-05 2:34 
GeneralRe: Bad idea Pin
Isildur28-Jun-05 2:38
Isildur28-Jun-05 2:38 
GeneralRe: Bad idea Pin
Naya200518-Aug-05 16:49
Naya200518-Aug-05 16:49 
GeneralRe: Bad idea Pin
Mihai Nita28-Jun-05 8:33
Mihai Nita28-Jun-05 8:33 
Ok, let me start by apologizing for the tone, just a bad day Smile | :)
But only disregard how I said it, not what I said Smile | :)

Isildur wrote:
Well, I forgot to tell that this is of course only for latin (Roman) languages. I do not try to tranlsate to Chinese or Japanese... You must have noticed this is not UNICODE friendly at all. Big Grin | :-D
Then maybe you should make it clear in the article.

Isildur wrote:
I haven't got any problem with sizes. As I start in Spanish, there's room enough for all the other languages. Try it, it doesn't look ugly at all.
Yes, Spanish is quite long. But:
- You post the article on a English site, with coders from all the world. They will use it for their languages
- There are languages longer than Spanish. Sometimes not longer in general, but for some labels only. Just add enough languages and you will have the problem. This is whay I say that does not scale and try with "30 languages"
- If everything is big enough to fit Spanish, is will look really bad in short languages (huge labels with almost nothing in them)

Isildur wrote:
I don't quite understand this point, too. I only say that can be a pain in the ass to manually update de text file, so I use a little application I've developed.
I am saying that professional translators have their own advanced tools. You don't need to write your own. If I hire a contracting developer using notepad, .bat files to build, and zip files to archive, low productivity, I don't write an IDE, a make and a version contol system for him. I hire a pro, using the proper tools. If he does not have the proper tools, he is probably not too good anyway.

Isildur wrote:
I don't know if you have maintained a big application using this system, but I have, and it can be a mess.
I did. I have done internationalization and localization work and consulting for the last 8 years.

Isildur wrote:
I don't see why a label must be translated differently than a button. You don't have to use only ID_NEW, you'll use ID_NEW_PROGRAM or ID_NEW_PAGE, so you'll know the gender, number, ... no ?
Ok. The English "Print" is translated into French as "Imprimer" for commands (buttons and such) and "Impression" for information things (labels and titles).
In you file you have the English as key. So I cannot have duplicated keys. You cannot have
[Print]<br />
  Imprimer<br />
[Print]<br />
  Impression

because if you retrieve by English string (Print), you have no way to decide which one do you want. So you have to have "Print" only once. Which messes up my translation, because forces me to one translation only. Remember, you have both buttons and labels in you application!
[Print]<br />
  ???

Other example:
[Scan]<br />
  ???

Consider that "Scan" is used both as "scan the disk" and "scan a page" in the same application. Is "Scan" translated the same into Spanish? In Portuguese it is not. And you cannot have two entries for "Scan"

Isildur wrote:
Could you address me to those articles, please ? But nothing about multiple resource dlls, please... thanks!
Well, like it or not, for Windows this is "industry best practice". And it is the result of a long evolution.
For Linux take a look at Qt. For Java see property files. Big note for Java and Qt: both have layout managers, so the resizing is not an issue. Qt still uses some xml type of resources, with dialog definitions (good context and good control). Java is easy to localize, but a mess to design a dialog that resizes properly. In general, Java is much worse to deal with, if the application has a lot of dialogs.
GeneralRe: Bad idea Pin
Isildur28-Jun-05 11:25
Isildur28-Jun-05 11:25 
GeneralProfessional tools Pin
smm2004-Aug-05 22:58
smm2004-Aug-05 22:58 
GeneralRe: Professional tools Pin
Duncan Mackay27-Feb-10 4:07
Duncan Mackay27-Feb-10 4:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.