basysKom AnwendungsEntwicklung

Translating Qt Applications
Essential Summary
Translating a Qt application, can be a daunting task. This is an overview from Qt 5 to Qt 6 and what new functionality Qt 6.7 brings.
Professionelle Entwicklung Ihrer HMI

Sie benötigen ein Geräte-HMI, Ihnen fehlt es aber an Zeit oder speziellem HMI-Know-How?​

Warum Dienstleistungen von basysKom?

Von der Konzeption über die Implementierung bis zum Testen unterstützen wir Sie in der Entwicklung Ihrer individuellen HMI. Unsere Services umfassen zudem die Technische Beratung, individuelle Trainings, Coaching, Verstärkung Ihrer Entwicklungsteams bis hin zur vollständigen Auftragsabwicklung im gesamten Lebenszyklus Ihres Produktes.

Translating Qt Applications

Translating a Qt application, executing the right tool at the right time, can be a daunting task. This blog post gives an overview of the differences between what it was like during the Qt 5 times, how Qt 6 improved upon it and what new functionality Qt 6.7 brings.

Generally, there’s two tools involved: lupdate and lrelease.

  • lupdate, extracts translatable strings from your source code by scanning it for the relevant keywords (qsTr, tr, QT_TR_NOOP and others) and places them in a `.ts` file, either creating a new one or updating an existing one. Those `.ts` files are XML files that can be edited in Qt Linguist or sent to a translation service company.
  • lrelease converts them into binary `.qm` files that get bundled with and loaded by the application.

The Qt 5.15 way

Qt has always provided CMake API for dealing with `lupdate` and `lrelease` in the `LinguistTools` package. In Qt 5 days you typically found yourself using:

  • `qt_create_translation` runs `lupdate` and creates or updates the `.ts` files
  • `qt_add_translation` runs `lrelease` and converts the `.ts` files to `.qm` files for consumption by the application

However, it was your responsibility to run the right commands at the proper time. You probably don’t want to update translations during every build to avoid cluttering the translation files with temporary changes. Therefore, you either had to add a custom build target or add a CMake option and build with something custom like `-UPDATE_TS_FILES=ON`. As was custom at the time, both CMake commands return the list of `.qm` files in a variable that you then have to add to your project sources and/or application resources. Additionally, you had to manually take care of the list of languages you wanted to support.

The Qt 6 way

Enter Qt 6: CMake has become the default and recommended build system generator for Qt projects and building Qt itself. Therefore, Qt has a vested interest in providing good CMake API for all its features and `LinguistTools` is no exception.

Note: There are changes on how translations are added to cmake within the Qt6 Series. We will discuss those changes down below. For a start we will describe on how it is done using Qt6.7+.

Qt 6.2 added a new `qt_add_translations` command. Note the plural _s_. Keep this in mind, since the old Qt5 command is still available.
The new qt_add_translations combines the previous `create` and `add` commands and adds build targets for both `lupdate` and `lrelease`.

A `<target>_lupdate` naturally runs `lupdate` while `<target>_lrelease` runs `release`.

The latter is actually built by default with your project. Additionally, a global `update_translations` and `release_translations` target is created for all translations within the project. Their names can be adjusted using the `QT_GLOBAL_LUPDATE_TARGET` and `QT_GLOBAL_LRELEASE_TARGET` variables.

In order to specify which languages your application supports, either set the `QT_I18N_TRANSLATED_LANGUAGES` variable, or use `I18N_TRANSLATED_LANGUAGES` argument of `qt_standard_project_setup`. The source language defaults to “en” but can be configured using `(QT_)I18N_SOURCE_LANGUAGE`.

Even if you merely migrate an application to the new way and have existing `.ts` files already, you want to set this variable since some platforms and distribution methods require the application to report its supported language in its metadata.


set(QT_I18N_TRANSLATED_LANGUAGES de fr)
add_executable(qstrtest main.cpp)
qt_add_translation(qstrtest) 

The translations are by default added to the Qt resource system in the `/i18n` prefix with the files named after your target. Therefore, from your `main()` load the translations like so:

QTranslator translator;
if (translator.load(QLocale(), "qstrtest"_L1, "_"_L1, ":/i18n"_L1)) {
    QCoreApplication::installTranslator(&translator);
} 

By the way, please use the `QTranslator::load` overload that takes a proper `QLocale`, don’t just hardcode a look-up based on the locale’s name. A user might want to use their home locale (24 hour clock, currency, and all) but with an English user interface.

And that’s it.

Build the `update_translations` target:

cmake --build . --target update_translations 

Of course this sounds too good to be true, right?

Customizing translations

For starters, you’ll find that `lupdate` blatantly placed the ts files, i.e. `qstrtest_de.ts` and `qstrtest_fr.ts` files alongside your `main.cpp`. If you want them in a dedicated folder instead, set the `TS_FILES_DIR` parameter of `qt_add_translations`. The default resource prefix can be changed using `RESOURCE_PREFIX`.

In case you don’t want to automatically add the `.qm` files to the application resources or you need more control on how they’re added, set `QM_FILES_OUTPUT_VARIABLE`. This disables automatic resource handling and instead writes the list of generated `.qm` files to that variable. From there you can postprocess the files any way you like, e.g. install them into a specific directory or add them to your existing resource file.

If you also need to adjust where the `.qm` files are created, set the `OUTPUT_LOCATION` file property using `set_source_files_properties`. Sadly, this needs to be done on each individual `.ts` file, defying the automatic determination of `.ts` file names based on the supported languages. On top of that, the property has to be set _before_ calling `qt_add_translations`!

Additional command-line arguments for `lupdate` and `lrelease` can be specified using the `LUPDATE_OPTIONS` and `LRELEASE_OPTIONS` arguments, respectively. For example, set `LRELEASE_OPTIONS  -idbased` if your application uses ID-based translations.

 

Important differences in Qt lower than version Qt6.7

The biggest showstopper of the snippet above, however, is that it only works like this from Qt 6.7 onwards. When you are targeting Qt 6.5 LTS you need to do things a litte bit differently.

In Qt 6.5 LTS you have to:

  • specify the `.ts` files manually using the `TS_FILES` option
  • in case the target you need to attach your qm files at is not your build target, you have to first create a custom target, add your translations against it and configure dependencies. From Qt6.7 onward you can set the output target to be not your build target.

Luckily, like with most recent improvements in Qt, be it in QML or in our case CMake, the new features are opt-in and a Qt6.5 LTS setup will continue to work as you upgrade to the upcoming Qt 6.8 LTS.

Comparison Qt5.15, Qt6.5, Qt6.7

Let’s compare the syntax you would use for a project built with Qt 5.15, 6.5, and 6.7, respectively. Assume the project was originally written against Qt 5.15 and should be ported to modern Qt CMake infrastructure. This should ideally be done without having to shuffle translation files around and without touching the C++ code that loads them.

Therefore, the location and name of the `.ts` files in the source tree must remain the same as well as the location within the application’s resources.

In our example, the `.ts` files will be

  • `translations/app_de.ts` and
  • `translations/app_fr.ts`,

loaded using: `translator.load(QLocale(), QStringLiteral("app"), QStringLiteral("_"), QStringLiteral(":/translations"));`.

 

Qt5.15

find_package(Qt5 REQUIRED COMPONENTS Core LinguistTools)

option(UPDATE_TS_FILES "Update .ts files" OFF)
set(LANGUAGES de fr)

add_executable(qstrtest main.cpp)

foreach(LANGUAGE ${LANGUAGES})
    list(APPEND TS_FILES translations/app_${LANGUAGE}.ts)
endforeach()

if (UPDATE_TS_FILES)
    qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES} OPTIONS -locations none)
else()
    qt_add_translation(QM_FILES ${TS_FILES})
endif()

set(TRANSLATIONS_QRC_FILE ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc)

file(WRITE ${TRANSLATIONS_QRC_FILE} "<!DOCTYPE RCC>\n<RCC version=\"1.0\">\n\t<qresource prefix=\"/translations\">\n")
foreach(QM_FILE ${QM_FILES})
    get_filename_component(QM_FILE_NAME ${QM_FILE} NAME)
    file(APPEND ${TRANSLATIONS_QRC_FILE} "\t\t<file alias=\"${QM_FILE_NAME}\">${QM_FILE}</file>\n")
endforeach()
file(APPEND ${TRANSLATIONS_QRC_FILE} "\t</qresource>\n</RCC>")

target_sources(qstrtest PRIVATE ${TRANSLATIONS_QRC_FILE} ${QM_FILES}) 

Awful, isn’t it? Sure, we could hardcode the `.qrc` file or use `configure_file` for it but if we want the flexibility of easily adding new languages, this is what we ought to do.

Qt6.5

find_package(Qt6 REQUIRED COMPONENTS Core LinguistTools)

qt_standard_project_setup(REQUIRES 6.5)

qt_add_executable(qstrtest main.cpp)

set(LANGUAGES de fr)
foreach(LANGUAGE ${LANGUAGES})
    list(APPEND TS_FILES translations/app_${LANGUAGE}.ts)
endforeach()

qt_add_translations(qstrtest
    TS_FILES ${TS_FILES}
    RESOURCE_PREFIX translations
    LUPDATE_OPTIONS -locations none
) 

Qt 6.5, as you can clearly tell, is a lot nicer. You still need to collect the list of `.ts` files yourself but other than that it’s pretty much automagic. The only key difference is that instead of building the project with `-DUPDATE_TS_FILES=ON` you just build the `update_translations` target as needed

Qt6.7

ind_package(Qt6 REQUIRED COMPONENTS Core LinguistTools)

qt_standard_project_setup(
    REQUIRES 6.7
    I18N_TRANSLATED_LANGUAGES de fr
)

qt_add_executable(qstrtest main.cpp)

qt_add_translations(qstrtest
    TS_FILE_BASE app
    TS_FILE_DIR translations
    RESOURCE_PREFIX translations
    LUPDATE_OPTIONS -locations none
) 

Qt 6.7 takes it one step further: you specify the list of supported languages and what file name format the resulting files should have. You can continue to use `TS_FILES` and provide an explicit list if you prefer.

There’s a lot more you can control with `qt_add_translations`, such as excluding certain files and folders. If it’s too much magic for your taste, there’s also more aptly named individual qt_add_lupdate  and qt_add_lrelease commands.

Nevertheless, this should give you a good idea of what it takes to port your Qt 5 translation setup to modern Qt CMake infrastructure.

Picture of Kai Uwe Broulik

Kai Uwe Broulik

Kai Uwe Broulik is Software Architect at basysKom where he designs embedded HMI applications based on Qt with C++ and QML. He also trains customers on how to use Qt efficiently. With his more than ten years of experience in Qt he has successfully deployed Qt applications to a variety of platforms, such as mobile phones, desktop environments, as well as automotive and other embedded devices.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Weitere Blogartikel

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy