Declaration of VAR

and some other stuff

How to translate a QML application

2017-01-04 10:33:39 +0100

2017-01-04 10:33:39 +0100 | Comments

…and dynamically switch between languages at runtime.

Ideally, it would be nice if you started with the following articles from Qt’s documentation:

But no doubt, you don’t have time for this, so here’s a crash-course in my interpretation for you:

<crash-course>
All the UI strings in your code should be surrounded by tr() function. And then you need to feed your source code to the lupdate tool, so it will parse all the strings into a separate translation file, which can be translated with Qt Linguist tool. After that you can use those translation files to switch between languages in your Qt application.
</crash-course>

Although, in case of QML application it is a bit more complicated. First thing - instead of of tr() you need to use qsTr() function, and second - you need to do quite a trick for QQmlEngine to reevaluate text and apply corresponding translation. I discovered all this in the article at Qt’s wiki, but I didn’t like some parts of the article (for instance, the way of loading the corresponding translation), so I decided to create my own tutorial.

Ok, let’s start right away with the trick. You need to create a class and then add an object of that class into QmlEngine’s context.

trans.h:

#ifndef TRANS_H
#define TRANS_H

#include <QObject>
#include <QTranslator>

class Trans : public QObject
{
    Q_OBJECT
    // that's the "magic" string for the trick
    Q_PROPERTY(QString emptyString READ getEmptyString NOTIFY languageChanged)

public:
    Trans();
    QString getEmptyString();
    Q_INVOKABLE void selectLanguage(QString language);

signals:
    void languageChanged();

private:
    QTranslator *translator;
};

#endif // TRANS_H

trans.cpp:

#include "trans.h"
#include <QDebug>
#include <QGuiApplication>
#include <QDir>

Trans::Trans()
{
    translator = new QTranslator(this);
}

QString Trans::getEmptyString()
{
    return QString();
}

void Trans::selectLanguage(QString language)
{
    // working folder
    QDir dir = QDir(qApp->applicationDirPath()).absolutePath();
//    #ifdef Q_OS_MACOS // crutch for Mac OS
//    dir.cdUp();
//    dir.cdUp();
//    dir.cdUp();
//    #endif
//    qDebug() << dir.path();

    if (!translator->load(
                // for example, in case of "ru" language the file would be
                // translating-qml_ru.qm
                // extension is set automatically
                QString("translating-qml_%1").arg(language),
                // look for the file in i18n folder within working directory
                QString("%1/i18n").arg(dir.path())
                )
            )
    {
        qDebug() << "Failed to load translation file, falling back to English";
    }
    // it's a global thing, we can use it anywhere (after #including <QGuiApplication>)
    qApp->installTranslator(translator);

    emit languageChanged();
}

main.cpp:

#include "trans.h"

// ...

// object of our class with "magic" property for translation
Trans trans;

QQmlApplicationEngine engine;
// make this object available from QML side
engine.rootContext()->setContextProperty("trans", &trans);
engine.load(QUrl(QLatin1String("qrc:/main.qml")));

// ...

That’s it. Now we can use it for strings in our UI. For example I have this in my main.qml:

Button {
    text: qsTr("Simple button") + trans.emptyString
    onClicked: console.log(qsTr("Some output into console") + trans.emptyString)

    ToolTip.visible: pressed
    ToolTip.text: qsTr("Some tooltip") + trans.emptyString
}

As you can see, every string is not only processed by qsTr() function, but also has this + trans.emptyString expression. That’s the trick itself - it forces QmlEngine to re-evaluate the text, and without this expression it won’t happen. It looks like a dirty trick, and I consider it dirty as well, but apparently so far that’s the only way to do the dynamic language switching in QML application, because related bug was reported in motherfucking 2010 and has not yet been fixed - that’s more than 6 (six) years for the moment, for fuck’s sake.

Ok, save main.qml. Now we need to generate a translation file. For that purpose there is an utility lupdate. Create a i18n folder in your project directory and execute the following command:

/path/to/Qt/5.7/clang_64/bin/lupdate main.qml -ts i18n/translating-qml_ru.ts

By the _ru suffix it will understand, that this translation file is meant for the Russian language. The same way we can create translation files for Norwegian and German languages:

/path/to/Qt/5.7/clang_64/bin/lupdate main.qml -ts i18n/translating-qml_no.ts

and

/path/to/Qt/5.7/clang_64/bin/lupdate main.qml -ts i18n/translating-qml_de.ts

Of course, we don’t want to do this manually for each language and especially if we have more than just main.qml that requires translation. So let’s add some stuff into the .pro file:

# list of source files containing strings for translation
lupdate_only { # that way those files will be skipped by C++ compiler
    SOURCES = *.qml # \
              # pages/*.qml
}

# list of language files that will store translated strings for every language we want
# create i18n directory first, if you don't have it
TRANSLATIONS = i18n/translating-qml_de.ts \
               i18n/translating-qml_no.ts \
               i18n/translating-qml_ru.ts

As you could’ve noticed, we didn’t create a translation file for English language. But that’s okay, because it is the default language of the UI, so when Translator will fail to load translating-qml_en.qm, it will just fall back to the “hardcoded” English.

Now we can run lupdate just once for all listed files and generate translation files for all required languages:

/path/to/Qt/5.7/clang_64/bin/lupdate translating-qml.pro

Okay, we got .ts files which simply are just XML files, so you can easily open them in any text editor. But they are supposed to be processed by the Qt Linguist tool, so open them there:

The translation process goes like this:

  1. Select string to translate;
  2. Write the translation;
  3. Mark this string as translated.

After you’re done with all strings, you can save and “release” the translation by pressing corresponding item in the File menu. The translation will be “compiled” into .qm format. Or you can just translate and save all your .ts files and then use lrelease tool for batch “compiling”:

/path/to/Qt/5.7/clang_64/bin/lrelease translating-qml.pro

Now back to code. You need some button or menu item that will trigger selectLanguage() function with corresponding language. For example:

onClicked: {
    onClicked: trans.selectLanguage("ru");
}

After you’ll have some UI with translations ready to test, you need to deploy the .qm files. Move them into the folder with your application binaries (the build folder)). In our case, it is again the i18n folder as we specified in trans.cpp, for example: build-translating-qml-Desktop_Qt_5_7_1_clang_64bit-Debug/i18n/.

For Mac OS it’s a bit different, because there applications are bundled into the .app package, so you need to open Package Contents and place i18n folder with .qm files into the translating-qml.app/Contents/MacOS/i18n:

That’s it. You can now run the application and try to click the button / menu to switch the current language of UI without restarting the application.

In case you’ve (most probably) got lost in my tutorial, here is a demo application with sources:

If you need to go deeper, check out the following links:



[30.05.2018] Update: QQmlEngine::retranslate()

…because related bug was reported in motherfucking 2010 and has not yet been fixed

In Qt 5.10 they actually did fix it. Now there is a special QQmlEngine::retranslate() function that does the job, so there is no need in this magic empty string anymore:

...
qApp->installTranslator(_translator);
_engine->retranslate();

emit languageChanged();
...

I’ve updated my demo application.