Dynamic Language Switching
For most applications, detecting the user's preferred language in main() and loading the appropriate .qm files there is perfectly satisfactory. But there are some situations where users might need the ability to switch language dynamically. An application that is used continuously by different people in shifts may need to change language without having to be restarted. For example, applications used by call center operators, by simultaneous translators, and by computerized cash register operators often require this capability.
Making an application able to switch language dynamically requires a little more work than loading a single translation at startup, but it is not difficult. Here's what must be done:
- Provide a means by which the user can switch language.
- For every widget or dialog, set all of its translatable strings in a separate function (often called retranslateStrings()) and call this function when the language changes.
Let's review the relevant parts of a Call Center application's source code. The application provides a Language menu to allow the user to set the language at run-time. The default language is English.
Figure 15.1. The Call Center application's Language menu
Since we don't know which language the user will want to use when the application is started, we no longer load translations in the main() function. Instead we will load them dynamically when they are needed, so all the code that we need to handle translations must go in the main window and dialog classes.
Let's have a look at the Call Center application's QMainWindow subclass:
MainWindow::MainWindow(QWidget *parent, const char *name) : QMainWindow(parent, name) { journalView = new JournalView(this); setCentralWidget(journalView); qmPath = qApp->applicationDirPath() + "/translations"; appTranslator = new QTranslator(this); qtTranslator = new QTranslator(this); qApp->installTranslator(appTranslator); qApp->installTranslator(qtTranslator); createActions(); createMenus(); retranslateStrings(); }
In the constructor, we set the central widget to be a JournalView, a QListView subclass. Then we set up a few private member variables related to translation:
- The qmPath variable is a QString that specifies the path of the directory that contains the application's translation files.
- The appTranslator variable is a pointer to the QTranslator object used for storing the current application translation.
- The qtTranslator variable is a pointer to the QTranslator object used for storing Qt's translation.
At the end, we call the createActions() and createMenus() private functions to create the menu system, and we call retranslateStrings(), also a private function, to set the user-visible strings for the first time.
void MainWindow::createActions() { newAct = new QAction(this); connect(newAct, SIGNAL(activated()), this, SLOT(newFile())); ... aboutQtAct = new QAction(this); connect(aboutQtAct, SIGNAL(activated()), qApp, SLOT(aboutQt())); }
The createActions() function creates the QAction objects as usual, but without setting any of the texts or accelerator keys. These will be done in retranslateStrings().
void MainWindow::createMenus() { fileMenu = new QPopupMenu(this); newAct->addTo(fileMenu); openAct->addTo(fileMenu); saveAct->addTo(fileMenu); exitAct->addTo(fileMenu); ... createLanguageMenu(); }
The createMenus() function creates menus, but does not insert these menus into the menu bar. Again, this will be done in retranslateStrings().
At the end of the function, we call createLanguageMenu() to fill the Language menu with the list of supported languages. We will review its source code in a moment. First, let's look at retranslateStrings():
void MainWindow::retranslateStrings() { setCaption(tr("Call Center")); newAct->setMenuText(tr("&New")); newAct->setAccel(tr("Ctrl+N")); newAct->setStatusTip(tr("Create a new journal")); ... aboutQtAct->setMenuText(tr("About &Qt")); aboutQtAct->setStatusTip(tr("Show the Qt library's About box")); menuBar()->clear(); menuBar()->insertItem(tr("&File"), fileMenu); menuBar()->insertItem(tr("&Edit"), editMenu); menuBar()->insertItem(tr("&Reports"), reportsMenu); menuBar()->insertItem(tr("&Language"), languageMenu); menuBar()->insertItem(tr("&Help"), helpMenu); }
The retranslateStrings() function is where all the tr() calls for the MainWindow class occur. It is called at the end of the MainWindow constructor and also every time a user changes the application's language using the Language menu.
We set each QAction's menu text, accelerator, and status tip. We also insert the menus into the menu bar, with their translated names. (The call to clear() is necessary when retranslateStrings() is called more than once.)
The createMenus() function referred to earlier called createLanguageMenu() to populate the Language menu with a list of languages:
void MainWindow::createLanguageMenu() { QDir dir(qmPath); QStringList fileNames = dir.entryList("callcenter_*.qm"); for (int i = 0; i < (int)fileNames.size(); ++i) { QTranslator translator; translator.load(fileNames[i], qmPath); QTranslatorMessage message = translator.findMessage("MainWindow", "English"); QString language = message.translation(); int id = languageMenu->insertItem( tr("&%1 %2").arg(i + 1).arg(language), this, SLOT(switchToLanguage(int))); languageMenu->setItemParameter(id, i); if (language == "English") languageMenu->setItemChecked(id, true); QString locale = fileNames[i]; locale = locale.mid(locale.find('_') + 1); locale.truncate(locale.find('.')); locales.push_back(locale); } }
Instead of hard-coding the languages supported by the application, we create one menu entry for each .qm file located in the application's translations directory. For simplicity, we assume that English also has a .qm file. An alternative would have been to call clear() on the QTranslator objects when the user chooses English.
One particular difficulty is to present a nice name for the language provided by each .qm file. Just showing "en" for "English" or "de" for "Deutsch", based on the name of the .qm file, looks crude and will confuse some users. The solution used in createLanguageMenu() is to check the translation of the string "English" in the "MainWindow" context. That string should be translated to "Deutsch" in a German translation, to "Français" in a French translation, and to "
We create menu items using QPopupMenu::insertItem(). They are all connected to the main window's switchToLanguage(int) slot, which we will review next. The parameter to the switchToLanguage(int) slot is the value set using setItemParameter(). This is very similar to what we did in Chapter 3 when we implemented the Spreadsheet application's recently opened files list (p. 54).
At the end, we append the locale in a QStringList called locales, which we will use for implementing switchToLanguage().
void MainWindow::switchToLanguage(int param) { appTranslator->load("callcenter_" + locales[param], qmPath); qtTranslator->load("qt_" + locales[param], qmPath); for (int i = 0; i < (int)languageMenu->count(); ++i) languageMenu->setItemChecked(languageMenu->idAt(i), i == param); retranslateStrings(); }
The switchToLanguage() slot is called when the user chooses a language from the Language menu. We start by loading the translation files for the application and for Qt. Then we update the check marks next to the Language menu entries so that the language in use is ticked, and we call retranslateStrings() to retranslate all the strings for the main window.
On Microsoft Windows, an alternative to providing a Language menu is to respond to LocaleChange events, a type of event emitted by Qt when it detects a change in the environment's locale. The event type exists on all platforms supported by Qt, but is only actually generated on Windows, when the user changes the system's locale settings (in the Regional and Language Options from the Control Panel). To handle LocaleChange events, we can reimplement QObject::event() as follows:
bool MainWindow::event(QEvent *event) { if (event->type() == QEvent::LocaleChange) { appTranslator->load(QString("callcenter_") + QTextCodec::locale(), qmPath); qtTranslator->load(QString("qt_") + QTextCodec::locale(), qmPath); retranslateStrings(); } return QMainWindow::event(event); }
If the user switches locale while the application is being run, we attempt to load the correct translation files for the new locale and call retranslateStrings() to update the user interface.
In all cases, we pass the event on to the base class's event() function, since one of our base classes may also be interested in LocaleChange events.
We have now finished our review of the MainWindow code. We will now review the code for one of the application's widget classes, the JournalView class, to see what changes are needed to make it support dynamic translation.
JournalView::JournalView(QWidget *parent, const char *name) : QListView(parent, name) { ... retranslateStrings(); }
The JournalView class is a QListView subclass. At the end of the constructor, we call the private function retranslateStrings() to set the widget's strings. This is similar to what we did for MainWindow.
bool JournalView::event(QEvent *event) { if (event->type() == QEvent::LanguageChange) retranslateStrings(); return QListView::event(event); }
We reimplement the event() function to call retranslateStrings() on LanguageChange events.
Qt generates a LanguageChange event when the contents of a QTranslator currently installed on QApplication changes. In the Call Center application, this occurs when we call load() on appTranslator or qtTranslator, either from MainWindow::switchToLanguage() or from MainWindow::event().
LanguageChange events are not the same as LocaleChange events. A LocaleChange event tells the application, "Maybe you should load a new translation." In contrast, a LanguageChange event tells the application's widgets, "Maybe you should retranslate all your strings."
When we implemented MainWindow, we didn't need to respond to Language-Change. Instead, we simply called retranslateStrings() whenever we called load() on a QTranslator.
void JournalView::retranslateStrings() { for (int i = columns() - 1; i >= 0; --i) removeColumn(i); addColumn(tr("Time")); addColumn(tr("Priority")); addColumn(tr("Phone Number")); addColumn(tr("Subject")); }
The retranslateStrings() function recreates the QListView column headers with newly translated texts. We do this by removing all column headings and then adding new column headings. This operation only affects the QListView header, not the data stored in the QListView.
This completes the translation-related code of a hand-written widget. For widgets and dialogs developed with Qt Designer, the uic tool automatically generates a function similar to our retranslateStrings() function that is automatically called in response to LanguageChange events. All we need to do is to load a translation file when the user switches language.