I was digging wxForum searching for a solution of some of my problems and realized that many people ask questions related to wxLocale and multilingual applications and it seems that setting up the development of multilingual applications is hard enough for junior programmers. So, today I want to tell you about how to start…. start the development of software which supports different languages.
First of all we have to install the tools which will allow to create translations of our applications. This tools are gettext and poEdit. gettext package contains a set of tools which allow to extract string data from the source code and to create the catalogue of translated strings.
The latest version is 0.14.4 but you can always see this page for newer versions if you need.
poEdit is a small application written with wxWidgets which provides convenient enough interface for editing catalogs of strings.
SourceForge link for downloading poEdit.
Now after you downloaded and installed gettext and poEdit, you have to add path to gettext/bin directory to your PATH variable.
After the preparation stage is finished, we can start the development stage. First, we create small wxWidgets project with single frame. Our simople project consists of 4 files:
wxTranslationTestMainFrame.h
#ifndef _WX_TRANSLATION_TEST_MAINFRAME_H #define _WX_TRANSLATION_TEST_MAINFRAME_H #include <wx/wx.h> #include <wx/listbox.h> class wxTranslationTestMainFrame : public wxFrame { wxListBox * m_ListBox; void CreateControls(); public: wxTranslationTestMainFrame(wxWindow * parent = NULL, wxWindowID id = wxID_ANY, const wxString title = _("wxTranslation Test")); bool Create(wxWindow * parent = NULL, wxWindowID id = wxID_ANY, const wxString title = _("wxTranslation Test"), const wxPoint & pos = wxDefaultPosition, const wxSize & size = wxDefaultSize, long style = wxCAPTION|wxSYSTEM_MENU|wxCLOSE_BOX|wxMINIMIZE_BOX|wxMAXIMIZE_BOX|wxRESIZE_BORDER); DECLARE_EVENT_TABLE() void OnExit(wxCommandEvent & event); void OnToggleStatusBar(wxCommandEvent & event); void OnAbout(wxCommandEvent & event); void OnToggleStatusBarUpdateUI(wxUpdateUIEvent & event); }; #endif
wxTranslationTestMainFrame.cpp
#include "wxTranslationTestMainFrame.h" #include "wxTranslationTestApp.h" #include <wx/textdlg.h> enum wxTranslationTestMainFrameIDs { ID_TOGGLE_STATUSBAR = 10001, ID_LISTBOX }; BEGIN_EVENT_TABLE(wxTranslationTestMainFrame, wxFrame) EVT_MENU(wxID_EXIT, wxTranslationTestMainFrame::OnExit) EVT_MENU(wxID_ABOUT, wxTranslationTestMainFrame::OnAbout) EVT_MENU(ID_TOGGLE_STATUSBAR, wxTranslationTestMainFrame::OnToggleStatusBar) EVT_UPDATE_UI(ID_TOGGLE_STATUSBAR, wxTranslationTestMainFrame::OnToggleStatusBarUpdateUI) END_EVENT_TABLE() wxTranslationTestMainFrame::wxTranslationTestMainFrame(wxWindow * parent, wxWindowID id, const wxString title) { Create(parent, id, title); } bool wxTranslationTestMainFrame::Create(wxWindow * parent, wxWindowID id, const wxString title, const wxPoint & pos, const wxSize & size, long style) { bool res = wxFrame::Create(parent, id, title, pos, size, style); if(res) { CreateControls(); } return res; } void wxTranslationTestMainFrame::CreateControls() { wxMenuBar * menuBar = new wxMenuBar; SetMenuBar(menuBar); wxMenu * fileMenu = new wxMenu; fileMenu->Append(wxID_EXIT, _("Exit\tAlt+F4")); wxMenu * viewMenu = new wxMenu; viewMenu->AppendCheckItem(ID_TOGGLE_STATUSBAR, _("Status Bar")); wxMenu * helpMenu = new wxMenu; helpMenu->Append(wxID_ABOUT, _("About...")); menuBar->Append(fileMenu, _("File")); menuBar->Append(viewMenu, _("View")); menuBar->Append(helpMenu, _("Help")); wxBoxSizer * sizer = new wxBoxSizer(wxVERTICAL); SetSizer(sizer); m_ListBox = new wxListBox(this, ID_LISTBOX); sizer->Add(m_ListBox, 1, wxEXPAND); CreateStatusBar(2); } void wxTranslationTestMainFrame::OnExit(wxCommandEvent & event) { Close(); } void wxTranslationTestMainFrame::OnToggleStatusBar(wxCommandEvent & event) { wxStatusBar * statusBar = GetStatusBar(); if(statusBar) { SetStatusBar(NULL); statusBar->Destroy(); } else { CreateStatusBar(2); } } void wxTranslationTestMainFrame::OnAbout(wxCommandEvent & event) { wxMessageBox(_("wxTranslation Test")); } void wxTranslationTestMainFrame::OnToggleStatusBarUpdateUI(wxUpdateUIEvent & event) { event.Check(GetStatusBar() != NULL); }
wxTranslationTestApp.h
#ifndef _WX_TRANSLATION_TEST_APP_H #define _WX_TRANSLATION_TEST_APP_H #include <wx/wx.h> class wxTranslationTestApp : public wxApp { public: virtual bool OnInit(); virtual int OnExit(); void RecreateGUI(); }; DECLARE_APP(wxTranslationTestApp); #endif
wxTranslationTestApp.cpp
#include "wxTranslationTestApp.h" #include "wxTranslationTestMainFrame.h" IMPLEMENT_APP(wxTranslationTestApp) bool wxTranslationTestApp::OnInit() { RecreateGUI(); return true; } int wxTranslationTestApp::OnExit() { return 0; } void wxTranslationTestApp::RecreateGUI() { wxWindow * topwindow = GetTopWindow(); if(topwindow) { SetTopWindow(NULL); topwindow->Destroy(); } wxTranslationTestMainFrame * frame = new wxTranslationTestMainFrame; SetTopWindow(frame); frame->Centre(); frame->Show(); }
Well, the first question you can ask is “Why the creation of top window was separated to RecreateGUI method?”. You will get the answer later but for now let’s assume that it was a good solution 🙂
Now let’s think about desired functionality
- We need the application to have the ability to switch language of its GUI
- Also it would be nice to avoid restart of the application after switching to new language
- One more convenient thing is the ability to save the language settings on exit and load them at startup
- The ability to find all installed languages could be also very convenient
- The solution should be easy-to-use
- And certainly it would be very nice to have the reusable source code
Well, after we have the requirements, we can begin implementing the needed functionality. Since there is no existing component with needed functionality, we will create the new one. Let’s call it wxTranslationHelper.
wxTranslationHelper.h
#ifndef _WX_TRANSLATION_HELPER_H #define _WX_TRANSLATION_HELPER_H #include <wx/wx.h> #include <wx/intl.h> class wxTranslationHelper { wxApp & m_App; wxLocale * m_Locale; public: wxTranslationHelper(wxApp & app); ~wxTranslationHelper(); wxLocale * GetLocale(); void GetInstalledLanguages(wxArrayString & names, wxArrayLong & identifiers); bool AskUserForLanguage(wxArrayString & names, wxArrayLong & identifiers); bool Load(); void Save(bool bReset = false); }; #endif
So, why does our component contains the reference to wxApp object? This reference will help us to avoid using of wxGetApp() function and wil allow the separate the component to the stand-alone library in future. Also our component contains the pointer to wxLocale object which, as said in wxWidgets’s documentation, “encapsulates all language-dependent settings and is a generalization of the C locale concept”. In other words wxLocale allows to switch languages in wxWidgets applications. Now let’s see the implementation
wxTranslationHelper.cpp
#include "wxTranslationHelper.h" #include <wx/dir.h> #include <wx/config.h> #include <wx/filename.h> wxTranslationHelper::wxTranslationHelper(wxApp & app) : m_App(app), m_Locale(NULL) { Load(); } wxTranslationHelper::~wxTranslationHelper() { Save(); if(m_Locale) { wxDELETE(m_Locale); } } wxLocale * wxTranslationHelper::GetLocale() { return m_Locale; } bool wxTranslationHelper::Load() { wxConfig config(m_App.GetAppName()); long language; if(!config.Read(wxT("wxTranslation_Language"), &language, wxLANGUAGE_UNKNOWN)) { language = wxLANGUAGE_UNKNOWN; } if(language == wxLANGUAGE_UNKNOWN) return false; wxArrayString names; wxArrayLong identifiers; GetInstalledLanguages(names, identifiers); for(size_t i = 0; i < identifiers.Count(); i++) { if(identifiers[i] == language) { if(m_Locale) wxDELETE(m_Locale); m_Locale = new wxLocale; m_Locale->Init(identifiers[i]); m_Locale->AddCatalogLookupPathPrefix(wxPathOnly(m_App.argv[0])); m_Locale->AddCatalog(m_App.GetAppName()); return true; } } return false; } void wxTranslationHelper::Save(bool bReset) { wxConfig config(m_App.GetAppName()); long language = wxLANGUAGE_UNKNOWN; if(!bReset) { if(m_Locale) { language = m_Locale->GetLanguage(); } } config.Write(wxT("wxTranslation_Language"), language); config.Flush(); } void wxTranslationHelper::GetInstalledLanguages(wxArrayString & names, wxArrayLong & identifiers) { names.Clear(); identifiers.Clear(); wxDir dir(wxPathOnly(m_App.argv[0])); wxString filename; const wxLanguageInfo * langinfo; wxString name = wxLocale::GetLanguageName(wxLANGUAGE_DEFAULT); if(!name.IsEmpty()) { names.Add(_("Default")); identifiers.Add(wxLANGUAGE_DEFAULT); } for(bool cont = dir.GetFirst(&filename, wxT("*.*"), wxDIR_DIRS); cont; cont = dir.GetNext(&filename)) { wxLogTrace(wxTraceMask(), _("wxTranslationHelper: Directory found = \"%s\""), filename.GetData()); langinfo = wxLocale::FindLanguageInfo(filename); if(langinfo != NULL) { if(wxFileExists(dir.GetName()+wxFileName::GetPathSeparator()+ filename+wxFileName::GetPathSeparator()+ m_App.GetAppName()+wxT(".mo"))) { names.Add(langinfo->Description); identifiers.Add(langinfo->Language); } } } } bool wxTranslationHelper::AskUserForLanguage(wxArrayString & names, wxArrayLong & identifiers) { wxCHECK_MSG(names.Count() == identifiers.Count(), false, _("Array of language names and identifiers should have the same size.")); long index = wxGetSingleChoiceIndex(_("Select the language"), _("Language"), names); if(index != -1) { if(m_Locale) { wxDELETE(m_Locale); } m_Locale = new wxLocale; m_Locale->Init(identifiers[index]); m_Locale->AddCatalogLookupPathPrefix(wxPathOnly(m_App.argv[0])); wxLogTrace(wxTraceMask(), _("wxTranslationHelper: Path Prefix = \"%s\""), wxPathOnly(m_App.argv[0]).GetData()); m_Locale->AddCatalog(m_App.GetAppName()); wxLogTrace(wxTraceMask(), _("wxTranslationHelper: Catalog Name = \"%s\""), m_App.GetAppName().GetData()); return true; } return false; }
Here are some explanations:
- wxTranslationHelper::GetInstalledLanguages – searches for directories in a directory with our application, finds the directories whose name is the same as canonical name of some of supported locales, then it checks if the catalog (.mo-file) with the same name as our application’s name exists in this directory. If file exists then adds the name and identifier of the language to the list of installed languages.
- wxTranslationHelper::AskUserForLanguage – shows dialog box with names of installed languages and lets the user select one of them. After that, if the language has been selected, wxTranslationHelper creates the wxLocale object and loads the catalog (.mo-file)
- wxTranslationHelper::Save method – saves the identifier of currently selected language to platform-specific configuration storage (e.g. Windows registry for MS Windows)
- wxTranslationHelper::Load – tries to load the language identifier from platform-specific configuration storage (e.g. Windows registry for MS Windows). If succeed then tries to find the language in the list of installed languages and then, if the language has been found, creates the wxLocale object and loads the catalog (.mo-file)
We recreate the wxLocale object each time because… well, I got an error message in debug mode which says that wxLocale::Init should be called only once for wxLocale object. So, if we can’t call it twice or more times, we can recreate the object and this should be the solution of this problem (at least I get no error messages and get no memory leask… and also this solution works with VC++ and MinGW and I can assume that it is acceptable).
After our helper class is finished, we can integrate it into our application.
wxTranslationTestApp.h
... class wxTranslationTestApp : public wxApp { wxTranslationHelper * m_TranslationHelper; public: ... bool SelectLanguage(); }; ...
wxTranslationTestApp.cpp
... bool wxTranslationTestApp::OnInit() { m_TranslationHelper = new wxTranslationHelper(*this); RecreateGUI(); return true; } int wxTranslationTestApp::OnExit() { if(m_TranslationHelper) { wxDELETE(m_TranslationHelper); } return 0; } ... bool wxTranslationTestApp::SelectLanguage() { wxArrayString names; wxArrayLong identifiers; m_TranslationHelper->GetInstalledLanguages(names, identifiers); return m_TranslationHelper->AskUserForLanguage(names, identifiers); }
We added new class member to our application class and also added a new method which allows user to select the language.
Now let’s make some changes to our main frame class to make it use new functionality.
wxTranslationTestMainFrame.h
... class wxTranslationTestMainFrame : public wxFrame { ... DECLARE_EVENT_TABLE() ... void OnSelectLanguage(wxCommandEvent & event); ... }; ...
wxTranslationTestMainFrame.cpp
... enum wxTranslationTestMainFrameIDs { ... ID_SELECT_LANGUAGE, ... }; BEGIN_EVENT_TABLE(wxTranslationTestMainFrame, wxFrame) ... EVT_MENU(ID_SELECT_LANGUAGE, wxTranslationTestMainFrame::OnSelectLanguage) ... END_EVENT_TABLE() ... void wxTranslationTestMainFrame::CreateControls() { wxMenuBar * menuBar = new wxMenuBar; SetMenuBar(menuBar); ... wxMenu * viewMenu = new wxMenu; viewMenu->Append(ID_SELECT_LANGUAGE, _("Language")); viewMenu->AppendCheckItem(ID_TOGGLE_STATUSBAR, _("Status Bar")); ... } ... void wxTranslationTestMainFrame::OnSelectLanguage(wxCommandEvent & event) { if(wxGetApp().SelectLanguage()) { wxGetApp().RecreateGUI(); } } ...
As you can see here, the application recreates the whole its GUI each time when user selects the language. This means that our main frame class should not store any data related to applications’s core because in this case all data will be deleted with frame and recreated anew. So, you should store all data in application class when using this way. Another way is recreating all controls without deleting and recreating of main frame. But this way is more complex. That’s why I recreate the whole GUI in this tutorial.
You can see the sample of storing the data inside the application class below:
wxTranslationTestApp.h
... class wxTranslationTestApp : public wxApp { wxArrayString m_SomeKindOfDocument; public: ... wxArrayString & GetDocument(); }; ...
wxTranslationTestApp.cpp
... wxArrayString & wxTranslationTestApp::GetDocument() { return m_SomeKindOfDocument; } ...
wxTranslationTestMainFrame.h
... class wxTranslationTestMainFrame : public wxFrame { private: void RefreshData(); ... DECLARE_EVENT_TABLE() ... void OnAddValue(wxCommandEvent & event); ... }; ...
wxTranslationTestMainFrame.cpp
... enum wxTranslationTestMainFrameIDs { ... ID_ADD_VALUE, ... }; ... void wxTranslationTestMainFrame::CreateControls() { wxMenuBar * menuBar = new wxMenuBar; SetMenuBar(menuBar); ... wxMenu * documentMenu = new wxMenu; documentMenu->Append(ID_ADD_VALUE, _("Add new item to document")); ... menuBar->Append(documentMenu, _("Document")); ... } ... void wxTranslationTestMainFrame::OnAddValue(wxCommandEvent & event) { wxString new_value = wxGetTextFromUser( _("Add new item to document"), _("Change some data")); if(!new_value.IsEmpty()) { wxGetApp().GetDocument().Add(new_value); RefreshData(); } } ... void wxTranslationTestMainFrame::RefreshData() { m_ListBox->Freeze(); m_ListBox->Clear(); for(size_t i = 0; i < wxGetApp().GetDocument().Count(); i++) { m_ListBox->Append(wxGetApp().GetDocument()[i]); } m_ListBox->Thaw(); }
We added new class member (array of strings) to our application class and created the ability to add values to this array and display them.
Here is the summary of what we already created:
- Helper class which allows to load and save the language settings, allows to list the installed translations and select one of them
- Application which stores the data used in our application
- Small frame with menu bar, listbox and status bar, which allows to display the data from application class
- Ability to switch the language on-the-fly without restarting the application (just by executing two methods of our helper class)
As you can see, there is nothing complex. Just basic functionality.
Now we can start preparation of translations. The screenshot below displays the structure of my project’s directory
- bin – this directory contains the executable file
- bin/* – this directories will contain the translations for appropriate languages
- lang – this directory contains files needed for creating the translations
Let’s switch to lang directory and create the batch files which will extract the string constants from our source code and will create the catalogs with translations
POcreate.bat – extracts string constants
cd .. dir /B *.c* > files.txt xgettext --from-code=cp1251 -a --no-location -s --no-wrap --escape -ffiles.txt -olang/wxTranslation.po pause
wxTranslation.po should appear after we executed POcreate.bat
Let’s open it with poEdit and translate the string constants (see the screenshot)
After all string constants have been translated, we can setup the catalog settings (select language and encoding)
Now after translation is finished, we have to save our .po-file and close poEdit.
Then we have to create .mo-file from our .po-file.
MOcreate.bat – creates .mo-file
msgfmt wxTranslation.po -owxTranslation.mo pause
The next step is copying our .mo-file to the appropriate subdirectory of bin directory (e.g. ru subdirectory for Russian language or uk_UA subdirectory for Ukrainian).
So the needed steps are:
- Create .po-file
- Translate it with poEdit
- Create .mo-file
- Copy it to appropriate place
- Translate .po-file with poEdit to another language
- Create .mo-file
- Copy it to appropriate place
- …
There is also nothing complex.
Now we can execute our application and see how it works.
Here is the screenshot of how the application’s main frame looked before switching to new language
And screenshot after switching to new language
That’s all 🙂
2 Comments
Keith
That easy, huh? 🙂 But seriously, my use of wxLocale works fine in Linux, but nothing gets translated at all in Windows.
T-Rex
Mmm, do you mean that you are using the approach suggested in this article and it does not work for you in Windows?