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.