- Working with Unicode
- Making Applications Translation-Aware
- Dynamic Language Switching
- Translating Applications
Making Applications Translation-Aware
If we want to make our applications available in multiple languages, we must do two things:
- Make sure that every user-visible string goes through tr().
- Load a translation (.qm) file at startup.
Neither of these is necessary for applications that will never be translated. However, using tr() requires almost no effort and leaves the door open for doing translations at a later date.
The tr() function is a static function defined in QObject and overridden in every subclass defined with the Q_OBJECT macro. When writing code inside a QObject subclass, we can call tr() without formality. A call to tr() returns a translation if one is available; otherwise, the original text is returned. Inside a non-QObject class, we can either write QObject::tr() with the class prefix or use the Q_DECLARE_TR_FUNCTIONS() macro to add tr() to the class, as we did in Chapter 8 (p. 200).
To prepare translation files, we must run Qt's lupdate tool. This tool extracts all the string literals that appear in tr() calls and produces translation files that contain all of these strings ready to be translated. The files can then be sent to a translator to have the translations added. This process is explained in the "Translating Applications" section later in this chapter.
A tr() call has the following general syntax:
Context::tr(sourceText, comment)
The Context part is the name of a QObject subclass defined with the Q_OBJECT macro. We don't need to specify it if we call tr() from a member function of the class in question. The sourceText part is the string literal that needs to be translated. The comment part is optional; it can be used to provide additional information to the translator.
Here are a few examples:
RockyWidget::RockyWidget(QWidget *parent) : QWidget(parent) { QString str1 = tr("Letter"); QString str2 = RockyWidget::tr("Letter"); QString str3 = SnazzyDialog::tr("Letter"); QString str4 = SnazzyDialog::tr("Letter", "US paper size"); }
The first two calls to tr() have "RockyWidget" as their context, and the last two calls have "SnazzyDialog". All four have "Letter" as their source text. The last call also has a comment to help the translator understand the meaning of the source text.
Strings in different contexts (classes) are translated independently of each other. Translators typically work on one context at a time, often with the application running and showing the widget or dialog being translated.
When we call tr() from a global function, we must specify the context explicitly. Any QObject subclass in the application can be used as the context. If none is appropriate, we can always use QObject itself. For example:
int main(int argc, char *argv[]) { QApplication app(argc, argv); ... QPushButton button(QObject::tr("Hello Qt!")); button.show(); return app.exec(); }
In every example so far, the context has been a class name. This is convenient, because we can almost always omit it, but this doesn't have to be the case. The most general way of translating a string in Qt is to use the QCoreApplication::translate() function, which accepts up to three arguments: the context, the source text, and the optional comment. For example, here's another way to translate "Hello Qt!":
QCoreApplication::translate("Global Stuff", "Hello Qt!")
This time, we put the text in the "Global Stuff" context.
The tr() and translate() functions serve a dual purpose: They are markers that lupdate uses to find user-visible strings, and at the same time they are C++ functions that translate text. This has an impact on how we write code. For example, the following will not work:
// WRONG const char *appName = "OpenDrawer 2D"; QString translated = tr(appName);
The problem here is that lupdate will not be able to extract the "OpenDrawer 2D" string literal, as it doesn't appear inside a tr() call. This means that the translator will not have the opportunity to translate the string. This issue often arises in conjunction with dynamic strings:
// WRONG statusBar()->showMessage(tr("Host " + hostName + " found"));
Here, the string we pass to tr() varies depending on the value of hostName, so we can't reasonably expect tr() to translate it correctly.
The solution is to use QString::arg():
statusBar()->showMessage(tr("Host %1 found").arg(hostName));
Notice how it works: The string literal "Host %1 found" is passed to tr(). Assuming that a French translation file is loaded, tr() would return something like "Hôte %1 trouvé". Then the "%1" parameter is replaced with the contents of the hostName variable.
Although it is generally inadvisable to call tr() on a variable, it can be made to work. We must use the QT_TR_NOOP() macro to mark the string literals for translation before we assign them to a variable. This is mostly useful for static arrays of strings. For example:
void OrderForm::init() { static const char * const flowers[] = { QT_TR_NOOP("Medium Stem Pink Roses"), QT_TR_NOOP("One Dozen Boxed Roses"), QT_TR_NOOP("Calypso Orchid"), QT_TR_NOOP("Dried Red Rose Bouquet"), QT_TR_NOOP("Mixed Peonies Bouquet"), 0 }; for (int i = 0; flowers[i]; ++i) comboBox->addItem(tr(flowers[i])); }
The QT_TR_NOOP() macro simply returns its argument. But lupdate will extract all the strings wrapped in QT_TR_NOOP() so that they can be translated. When using the variable later on, we call tr() to perform the translation as usual. Even though we have passed tr() a variable, the translation will still work.
There is also a QT_TRANSLATE_NOOP() macro that works like QT_TR_NOOP() but also takes a context. This macro is useful when initializing variables outside of a class:
static const char * const flowers[] = { QT_TRANSLATE_NOOP("OrderForm", "Medium Stem Pink Roses"), QT_TRANSLATE_NOOP("OrderForm", "One Dozen Boxed Roses"), QT_TRANSLATE_NOOP("OrderForm", "Calypso Orchid"), QT_TRANSLATE_NOOP("OrderForm", "Dried Red Rose Bouquet"), QT_TRANSLATE_NOOP("OrderForm", "Mixed Peonies Bouquet"), 0 };
The context argument must be the same as the context given to tr() or translate() later on.
When we start using tr() in an application, it's easy to forget to surround some user-visible strings with a tr() call, especially when we are just beginning to use it. These missing tr() calls are eventually discovered by the translator or, worse, by users of the translated application, when some strings appear in the original language. To avoid this problem, we can tell Qt to forbid implicit conversions from const char * to QString. We do this by defining the QT_NO_CAST_FROM_ASCII preprocessor symbol before including any Qt header. The easiest way to ensure that this symbol is set is to add the following line to the application's .pro file:
DEFINES += QT_NO_CAST_FROM_ASCII
This will force every string literal to require wrapping by tr() or by QLatin1String(), depending on whether it should be translated. Strings that are not suitably wrapped will produce a compile-time error, thereby compelling us to add the missing tr() or QLatin1String() wrapper.
Once we have wrapped every user-visible string by a tr() call, the only thing left to do to enable translation is to load a translation file. Typically, we would do this in the application's main() function. For example, here's how we would try to load a translation file depending on the user's locale:
int main(int argc, char *argv[]) { QApplication app(argc, argv); ... QTranslator appTranslator; appTranslator.load("myapp_" + QLocale::system().name(), qmPath); app.installTranslator(&appTranslator); ... return app.exec(); }
The QLocale::system() function returns a QLocale object that provides information about the user's locale. Conventionally, we use the locale's name as part of the .qm file name. Locale names can be more or less precise; for example, fr specifies a French-language locale, fr_CA specifies a French Canadian locale, and fr_CA.ISO8859-15 specifies a French Canadian locale with ISO 8859-15 encoding (an encoding that supports '€', 'Œ', 'œ', and '').
Assuming that the locale is fr_CA.ISO8859-15, the QTranslator::load() function first tries to load the file myapp_fr_CA.ISO8859-15.qm. If this file does not exist, load() next tries myapp_fr_CA.qm, then myapp_fr.qm, and finally myapp.qm, before giving up. Normally, we would only provide myapp_fr.qm, containing a standard French translation, but if we need a different file for French-speaking Canada, we can also provide myapp_fr_CA.qm and it will be used for fr_CA locales.
The second argument to QTranslator::load() is the directory where we want load() to look for the translation file. In this case, we assume that the translation files are located in the directory given in the qmPath variable.
The Qt libraries contain a few strings that need to be translated. Trolltech provides French, German, and Simplified Chinese translations in Qt's translations directory. A few other languages are provided as well, but these are contributed by Qt users and are not officially supported. The Qt libraries' translation file should also be loaded:
QTranslator qtTranslator; qtTranslator.load("qt_" + QLocale::system().name(), qmPath); app.installTranslator(&qtTranslator);
A QTranslator object can hold only one translation file at a time, so we use a separate QTranslator for Qt's translation. Having just one file per translator is not a problem since we can install as many translators as we need. QCoreApplication will use all of them when searching for a translation.
Some languages, such as Arabic and Hebrew, are written right-to-left instead of left-to-right. In those languages, the whole layout of the application must be reversed, and this is done by calling QApplication::setLayoutDirection(Qt::RightToLeft). The translation files for Qt contain a special marker called "LTR" that tells Qt whether the language is left-to-right or right-to-left, so we normally don't need to call setLayoutDirection() ourselves.
It may prove more convenient for our users if we supply our applications with the translation files embedded in the executable, using Qt's resource system. Not only does this reduce the number of files distributed as part of the product, but it also avoids the risk of translation files getting lost or deleted by accident. Assuming that the .qm files are located in a translations subdirectory in the source tree, we would then have a myapp.qrc file with the following contents:
<RCC> <qresource> <file>translations/myapp_de.qm</file> <file>translations/myapp_fr.qm</file> <file>translations/myapp_zh.qm</file> <file>translations/qt_de.qm</file> <file>translations/qt_fr.qm</file> <file>translations/qt_zh.qm</file> </qresource> </RCC>
The .pro file would contain the following entry:
RESOURCES += myapp.qrc
Finally, in main(), we must specify :/translations as the path for the translation files. The leading colon indicates that the path refers to a resource as opposed to a file in the file system.
We have now covered all that is required to make an application able to operate using translations into other languages. But language and the direction of the writing system are not the only things that vary between countries and cultures. An internationalized program must also take into account the local date and time formats, monetary formats, numeric formats, and string collation order. Qt includes a QLocale class that provides localized numeric and date/time formats. To query other locale-specific information, we can use the standard C++ setlocale() and localeconv() functions.
Some Qt classes and functions adapt their behavior to the locale:
- QString::localeAwareCompare() compares two strings in a locale-dependent manner. It is useful for sorting user-visible items.
- The toString() function provided by QDate, QTime, and QDateTime returns a string in a local format when called with Qt::LocalDate as its argument.
- By default, the QDateEdit and QDateTimeEdit widgets present dates in the local format.
Finally, a translated application may need to use different icons in certain situations rather than the original icons. For example, the left and right arrows on a web browser's Back and Forward buttons should be swapped when dealing with a right-to-left language. We can do this as follows:
if (QApplication::isRightToLeft()) { backAction->setIcon(forwardIcon); forwardAction->setIcon(backIcon); } else { backAction->setIcon(backIcon); forwardAction->setIcon(forwardIcon); }
Icons that contain alphabetic characters very commonly need to be translated. For example, the letter 'I' on a toolbar button associated with a word processor's Italic option should be replaced by a 'C' in Spanish (Cursivo) and by a 'K' in Danish, Dutch, German, Norwegian, and Swedish (Kursiv). Here's a simple way to do it:
if (tr("Italic")[0] == 'C') { italicAction->setIcon(iconC); } else if (tr("Italic")[0] == 'K') { italicAction->setIcon(iconK); } else { italicAction->setIcon(iconI); }
An alternative is to use the resource system's support for multiple locales. In the .qrc file, we can specify a locale for a resource using the lang attribute. For example:
<qresource> <file>italic.png</file> </qresource> <qresource lang="es"> <file alias="italic.png">cursivo.png</file> </qresource> <qresource lang="sv"> <file alias="italic.png">kursiv.png</file> </qresource>
If the user's locale is es (Español), :/italic.png becomes a reference to the cursivo.png image. If the locale is sv (Svenska), the kursiv.png image is used. For other locales, italic.png is used.