📜 ⬆️ ⬇️

Disadvantages when working with translations in Qt and ways to deal with them

In this article I would like to talk about some of the inconveniences that I encountered when working with the translation system in Qt, and also to share ways to combat these inconveniences.

First, let me briefly remind you how the translation system in Qt works.

First of all, the developer, while writing the code, wraps the string that should be translated into different languages ​​into one of the special functions:

tr("Push me", "button text"); //  - . QCoreApplication::translate("console", "Enter a number"); //  - . 

Further, in the project file, the files are indicated, in which the translator will perform, in fact, the translation itself:
')
 TRANSLATIONS += translations/myapp_ru.ts //         . 

Then, the lupdate utility is launched , which creates (or updates) the source files of the translations (regular XML files), after which the translator can work with them with the help of a special tool - Qt Linguist. The lines wrapped in the tr and translate functions will be processed by the utility and added to the .ts files.

Finally, when all strings are translated, the lrelease utility is launched , turning the source translation files (.ts) into .qm files that have a special binary format. Now it remains only to add the following code to the application:

 QTranslator *t = new QTranslator; t->load("/path/to/translations/myapp_ru.qm"); QApplication::installTranslator(t); 

Everything, our lines will be displayed in the desired language.

Inconvenience 1: storage of translations


So, well, we translated the string, when the application was launched, we downloaded the translation file, in our text field (or somewhere else) the text appeared in the desired language. Indeed, in such a trivial case, more is not needed. However, consider the following example. Suppose we have a console application that implements the processing of user-entered commands. New commands can be added by installing a handler function, for example, like this:

 typedef bool (*HandlerFunction)(const QStringList &arguments); QMap<QString, HandlerFunction> handlerMap; void installHandler(const QString &command, HandlerFunction f) { handlerMap.insert(command, f); } 

Everything is fine, but it would be nice if you type, say «help command » to issue information on the corresponding command command . We will do:

 QMap<QString, QString> helpMap; void installHelp(const QString &command, const QString &help) { helpMap.insert(command, help); } 

Feel trick? Yes, at first everything will be fine:

 installHelp("mycommand", tr("Does some cool stuff")); 

If QTranslator was set in advance, then we get the translated string. But what if the user decides to change the language (in other words, another translation file will be loaded)? The line will remain the same.

This problem has several solutions. I will give a few, including the one that seems to me the most natural and comfortable.

Solution 1: Factory

You can replace the string with a factory function that will return the string:

 typedef QString(*HelpFactoryFunction)(void); QMap<QString, HelpFactoryFunction> helpMap; void installHelp(const QString &command, HelpFactoryFunction f) { helpMap.insert(command, f); } 

The factory function and its application may look like this:

 QString myHelpFactory() { return tr("Does some cool stuff"); } installHelp("mycommand", &myHelpFactory); 

Does this solve the problem? Yes, the translation will be made every time the help is called, so when the language changes, the help will be shown translated into this new language. Is this a beautiful solution? Everyone thinks differently, but I think not.

Solution 2: QT_TRANSLATE_NOOP3

The <QtGlobal> header file contains such a macro, QT_TRANSLATE_NOOP3 . It marks the string wrapped in it to the translation and returns an anonymous structure (struct) containing this string (in untranslated form), as well as a comment. In the future, the created structure can be used in the functions tr and translate .

Do I have to say that the code is cumbersome and ugly? I think not. In addition, difficulties arise with the transfer of such a structure as a parameter of the function. Code:

 typedef struct { const char *source; const char *comment; } TranslateNoop3; QMap<QString, TranslateNoop3> helpMap; void installHelp(const QString &command, const TranslateNoop3 &t) { helpMap.insert(command, t); } 

Using:

 installHelp("mycommand", QT_TRANSLATE_NOOP3("context", "Does some cool stuff", "help")); 

That another macro (and another structure), QT_TRANSLATE_NOOP, is used for translation without a comment — I am completely silent. But you would have to fence installHelp overload and turn one structure into another. Disgusting. Let's leave it to the conscience of the Qt developers.

Solution 3: self-written class wrapper

In a sense, my solution is an improved version of QT_TRANSLATE_NOOP3 . I suggest looking at the code right away:
translation.h
 class Translation { private: QString context; QString disambiguation; int n; QString sourceText; public: explicit Translation(); Translation(const Translation &other); public: static Translation translate(const char *context, const char *sourceText, const char *disambiguation = 0, int n = -1); public: QString translate() const; public: Translation &operator =(const Translation &other); operator QString() const; operator QVariant() const; public: friend QDataStream &operator <<(QDataStream &stream, const Translation &t); friend QDataStream &operator >>(QDataStream &stream, Translation &t); }; Q_DECLARE_METATYPE(Translation) 


translation.cpp
 Translation::Translation() { n = -1; } Translation::Translation(const Translation &other) { *this = other; } Translation Translation::translate(const char *context, const char *sourceText, const char *disambiguation, int n) { if (n < 0) n = -1; Translation t; t.context = context; t.sourceText = sourceText; t.disambiguation = disambiguation; tn = n; return t; } QString Translation::translate() const { return QCoreApplication::translate(context.toUtf8().constData(), sourceText.toUtf8().constData(), disambiguation.toUtf8().constData(), n); } Translation &Translation::operator =(const Translation &other) { context = other.context; sourceText = other.sourceText; disambiguation = other.disambiguation; n = other.n; return *this; } Translation::operator QString() const { return translate(); } Translation::operator QVariant() const { return QVariant::fromValue(*this); } QDataStream &operator <<(QDataStream &stream, const Translation &t) { QVariantMap m; m.insert("context", t.context); m.insert("source_text", t.sourceText); m.insert("disambiguation", t.disambiguation); m.insert("n", tn); stream << m; return stream; } QDataStream &operator >>(QDataStream &stream, Translation &t) { QVariantMap m; stream >> m; t.context = m.value("context").toString(); t.sourceText = m.value("source_text").toString(); t.disambiguation = m.value("disambiguation").toString(); tn = m.value("n", -1).toInt(); return stream; } 


I used the interesting property of lupdate : it does not matter in which namespace the translate function is located, the main thing is that it has exactly such a name, and also that the order of the arguments and their type are as in QCoreApplication :: translate . In this case, lines wrapped in any translate function will be marked with the translation and added to the .ts file.

Then it remains for the small: we implement our static translate method so that it creates an instance of the Translation class, which is in fact a more convenient analogue of the anonymous structure that QT_TRANSLATE_NOOP3 returns. We also add another translate method, but no longer static. It simply calls inside QCoreApplication :: translate , passing as parameters the context, the source string, and the comment that was specified when calling the static Translation :: translate method. We add methods for copying and (de) serializing, and we get a convenient container for storing translations. I will not describe the rest of the class methods, since they are not directly related to the problem being solved and are trivial for developers familiar with C ++ and Qt, for whom this article is intended.

Here is how an example would look like with help using Translation :

 QMap<QString, Translation> helpMap; void installHelp(const QString &command, const Translation &help) { helpMap.insert(command, help); } installHelp("mycommand", Translation::translate("context", "Do some cool stuff")); 

It looks more natural than a factory, and more beautiful than QT_TRANSLATE_NOOP3 , isn't it?

Disadvantage 2: translation without inheritance


The second disadvantage I encountered in Qt is the impossibility of dynamically translating an interface without inheriting at least one class. Consider immediately an example:

 int main(int argc, char **argv) { QApplication app(argc, argv); QTranslator *t = new QTranslator; t->load("/path/to/translations/myapp_ru.qm"); QApplication::installTranslator(t); QWidget *w = new QWidget; w->setWindowTitle(QApplication::translate("main", "Cool widget")); w->show(); LanguageSettingsWidget *lw = new LanguageSettingsWidget; lw->show(); int ret = app.exec(); delete w; return ret; } 

As you can see from the example, we load the translation file, create a QWidget and set its name. But suddenly the user decided to use LanguageSettingsWidget and chose another language. The name of QWidget should change, but for this we need to take some additional actions. Again, there are several options.

Solution 1: Inheritance

You can inherit from QWidget and override one of the virtual methods:

 class MyWidget : public QWidget { protected: void changeEvent(QEvent *e) { if (e->type() != QEvent::LanguageChange) return; setWindowTitle(tr("Cool widget")); } }; 

In this case, when installing a new QTranslator, the changeEvent method will be called, and, in our case, setWindowTitle . Simply? Enough. Conveniently? I believe that it is not always (in particular, when it is only for the sake of transfers that you have to make such a garden).

Solution 2: external translation

You can also pass a pointer to this class to another class, which is already inherited from QWidget , and call the appropriate method there. I will not give the code - it is obvious and differs little from the previous example. Let me just say that this is definitely a bad way - the less classes know about each other, the better.

Solution 3: one more bike another wrapper

The idea is simple: let us use Qt, such a convenient tool, as a meta-object system (it is understood that the signals and slots belong here). Let's write a class to which we will pass a pointer to the target object, as well as the translation object from the first part of the article - Translator . In addition, we indicate which property to write the translation in, or to which slot to pass as an argument. So fewer words, more deeds:
dynamictranslator.h
 class DynamicTranslator : public QObject { Q_OBJECT private: QByteArray targetPropertyName; QByteArray targetSlotName; Translation translation; public: explicit DynamicTranslator(QObject *parent, const QByteArray &targetPropertyName, const Translation &t); explicit DynamicTranslator(QObject *parent, const Translation &t, const QByteArray &targetSlotName); protected: bool event(QEvent *e); private: Q_DISABLE_COPY(DynamicTranslator) }; 


dynamictranslator.cpp
 DynamicTranslator::DynamicTranslator(QObject *parent, const QByteArray &targetPropertyName, const Translation &t) : QObject(parent) { this->targetPropertyName = targetPropertyName; translation = t; } DynamicTranslator::DynamicTranslator(QObject *parent, const Translation &t, const QByteArray &targetSlotName) : QObject(parent) { this->targetSlotName = targetSlotName; translation = t; } bool DynamicTranslator::event(QEvent *e) { if (e->type() != QEvent::LanguageChange) return false; QObject *target = parent(); if (!target) return false; if (!targetPropertyName.isEmpty()) target->setProperty(targetPropertyName.constData(), translation.translate()); else if (!targetSlotName.isEmpty()) QMetaObject::invokeMethod(target, targetSlotName.constData(), Q_ARG(QString, translation.translate())); return false; } 


What is going on here? When creating an instance of the DynamicTranslator class , we specify the target object, the translation, and also the name of the slot (for example, setWindowTitle ) or the name of the property ( windowTitle ). Our DynamicTranslator, with each change of language, either calls the corresponding slot using QMetaObject , or sets the desired property using setProperty . Here is how it looks in practice:

 int main(int argc, char **argv) { QApplication app(argc, argv); QTranslator *t = new QTranslator; t->load("/path/to/translations/myapp_ru.qm"); QApplication::installTranslator(t); QWidget *w = new QWidget; Translation t = Translation::translate("main", "Cool widget"); w->setWindowTitle(t); new DynamicTranslator(w, "windowTitle", t); w->show(); LanguageSettingsWidget *lw = new LanguageSettingsWidget; lw->show(); int ret = app.exec(); delete w; return ret; } 

Due to the fact that the widget w is the parent of our DynamicTranslator , there is no need to worry about deleting it - DynamicTranslator will be removed along with QWidget .

Instead of conclusion


Of course, the considered ways of dealing with the inconvenience of translations are not the only, and even more so - the only true ones. For example, in large enough applications, third-party translation tools can be used instead of those provided by Qt (for example, all texts can be stored in files, and only identifiers can be specified in the code). Again, in large applications, a couple dozen extra lines (in the case of inheritance or writing a factory function) will not make the weather. Nevertheless, the solutions given here can save a little time and lines of code, and also make this code more natural.

Criticism and alternative solutions are always welcome - let's write more correct, beautiful and clear code together. Thank you for attention.

Source: https://habr.com/ru/post/247207/


All Articles