65.9K
CodeProject is changing. Read more.
Home

Yet another way to localize your MFC apps

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.50/5 (12 votes)

Jun 27, 2005

CPOL

3 min read

viewsIcon

54691

downloadIcon

466

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:

CString str = GetTranslatedString(IDS_STRING);

Let's see how it works....

First we load the string from the resources...

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...

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.

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):

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...

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...