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 while working with the translation system in Qt, as well as share ways to deal with these inconveniences.

To begin with, briefly recall how the translation system in Qt works.

First of all, the developer, when writing code, wraps a string, which must be translated into various languages, in one of the special functions:

tr("Push me", "button text"); //Второй параметр - комментарий.
QCoreApplication::translate("console", "Enter a number"); //Первый параметр - контекст.

Next, the project file indicates the files in which the translator will carry out, in fact, the translation itself:

TRANSLATIONS += translations/myapp_ru.ts //Файлов может быть больше в зависимости от количества целевых языков.

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

Finally, when all the lines are translated, the lrelease utility starts , converting 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);

That's it, our lines will be displayed in the desired language.

Inconvenience 1: translation storage


So, well, we translated the line, when the application started, we downloaded the translation file, the text in the desired language appeared in our text box (or somewhere else). Indeed, in such a trivial case, more is not necessary. However, consider the following example. Suppose we have a console application where the processing of user-entered commands is implemented. New commands can be added by setting a handler function, for example, like this:

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

Everything is fine, but it would be nice when entering, say “help command ”, to give help on the corresponding command command . We will do:

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

Feel the catch? Yes, at first everything will be fine:

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

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

There are several solutions to this problem. I will give a few, including the one that seems to me the most natural and convenient.

Solution 1: factory

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

typedef QString(*HelpFactoryFunction)(void);
QMap 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 carried out each time a help is called, so when you change the language, the help will be displayed translated into this new language. Is this a beautiful decision? Everyone thinks differently, but I think not.

Solution 2: QT_TRANSLATE_NOOP3

In the header file there is such a macro - QT_TRANSLATE_NOOP3 . It marks the string wrapped in it for 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 tr and translate functions .

Needless to say, the code is cumbersome and ugly? I think not. In addition, difficulties arise in passing such a structure as a parameter to a function. The code:

typedef struct { const char *source; const char *comment; } TranslateNoop3;
QMap 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"));

The fact that for translation without comment another macro is used (and another structure) - QT_TRANSLATE_NOOP - I am completely silent. But you would have to block the installHelp overload and turn one structure into another. Disgusting. We leave this to the conscience of the Qt developers.

Solution 3: a self-written wrapper class

In a way, my solution is an improved version of QT_TRANSLATE_NOOP3 . I suggest you look 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;
    t.n = 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", t.n);
    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();
    t.n = m.value("n", -1).toInt();
    return stream;
}


I used the interesting property of lupdate : it doesn’t matter in which namespace the translate function is located , the main thing is that it has exactly the same name, as well as the order of the arguments and their type are as in QCoreApplication :: translate . In this case, the lines wrapped in any translate function will be marked for translation and added to the .ts file.

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

Here is a help example using Translation :

QMap 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 the factory, and prettier than QT_TRANSLATE_NOOP3 , doesn't it?

Inconvenience 2: translation without inheritance


The second inconvenience that I encountered in Qt is the inability to dynamically translate an interface without inheriting at least one class. Let's look at an example right away:

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 a different language. The name QWidget should change, but for this we need to take some additional steps. 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 a new QTranslator method is called changeEvent , and, in our case, setWindowTitle . Simply? Enough. Conveniently? I believe that it’s not always (in particular, when it is necessary to fence such a garden only for the sake of transfers).

Solution 2: transfer from the outside

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

Solution 3: another bike another wrapper

The idea is simple: let’s use such a convenient Qt tool as a meta-object system (it means that signals and slots belong to the same place). We will write a class to which we will pass a pointer to the target object, as well as a translation object from the first part of the article - Translator . In addition, we specify what property ( property ) to write the translation, or a slot to pass as an argument. So, less words, more work:
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, translation, as well as 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's what it looks like 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 removing it - DynamicTranslator will be removed along with QWidget .

Instead of a conclusion


Of course, the considered methods of dealing with the inconvenience of translations are not the only ones, and even more so - the only true ones. For example, in large enough applications, third-party translation tools can generally be used instead of those that Qt provides (for example, you can store all texts in files and specify only identifiers 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. However, the solutions presented here can save a little time and lines of code, as well as 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.

Also popular now: