Revisited i18n with CMake
January 31, 2024 by Jörg Bornemann | Comments
With Qt 6.2, we introduced a new CMake API to handle internationalization (i18n) of Qt-based projects: qt_add_translations
, qt_add_lupdate
and qt_add_lrelease
. These functions have shortcomings that we address in the upcoming Qt 6.7 release.
Update: In the Qt 6.7 API review we've renamed some parts of the i18n CMake API. This post has been updated accordingly.
What's the problem?
The main entry point to i18n with CMake is qt_add_translations
. The function takes a CMake target as the first parameter. Source files from that target are then passed to lupdate, producing a .ts file.
Now, projects usually have more than one target. We didn't have a good way to pass multiple targets to qt_add_translations
or qt_add_lupdate
so far. Even if you created separate .ts files per target, there is no convenient way to merge the resulting .qm files. The lconvert tool can do that, but you'll have to do the setup on CMake level.
Then, there might be sources in a target that you don't want to pass to lupdate. You want to mark sources as "I don't want this to contribute to my project's .ts files." Our i18n functions didn't offer a way to exclude sources.
The only reliable workaround for these shortcomings was to explicitly pass the list of source files to qt_add_translations
/ qt_add_lupdate
.
Further, on iOS, projects need to announce what languages they support. We had code that extracted the languages from the .ts files to write it into the Info.plist file. That works, but it's a bit fragile at the edges.
The revisited CMake i18n commands
The target-based view of i18n is way too fine-grained. We need a project-level view that allows us to collect translatable strings in the sources of the whole project. And there needs to be a way to exclude parts of the source tree conveniently.
QMake has a project-wide view with its TRANSLATIONS
variable, and TR_EXCLUDE
is used to exclude sources. Projects that use gettext
usually operate directly on the source tree. With Qt 6.7, we'll offer a project-wide view for qt_add_translations
too. We've kept compatibility with the "one target API" to avoid breaking existing projects.
Let's consider a medium-size project - a clone of the game classic frogger. The project consists of several parts:
frogger
, the main executable targetgame_logic
, a library target with the meat of the game logicjump_sim
, a third-party public domain library to realistically simulate the frog jumps- a bunch of tests
The top-level project file looks like this:
cmake_minimum_required(VERSION 3.28)
project(frogger)
find_package(Qt6 COMPONENTS OpenGLWidgets)
qt_standard_project_setup()
add_subdirectory(3rdparty) # adds target jump_sim
qt_add_library(game_logic src/game_logic/stuff.cpp ...)
target_link_libraries(game_logic PRIVATE jump_sim)
qt_add_executable(frogger src/frogger/main.cpp ...)
target_link_libraries(frogger PRIVATE game_logic)
add_subdirectory(tests) # adds several targets
Our project has Norwegian and German translations, so we adjust the setup call as follows:
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES nb de)
Somewhere after the creation of the frogger
target, we call qt_add_translations
.
qt_add_translations(frogger)
And that's it! This will
- collect all source files from all targets of the project
- create
frogger_nb.ts
andfrogger_de.ts
from the project name and the list of languages we passed in theqt_standard_project_setup
call - create
frogger_en.ts
that contains only plural forms. See the explanation of plural forms below. - create an
update_translations
target to extract the translatable strings from the collected source files - create a
release_translations
target to create the .qm files from the .ts files - create a Qt resource that contains the .qm files and embed it into the
frogger
target - when building for iOS, the app will contain the information that frogger supports the languages English, Norwegian and German
The example above shows the most straightforward use of qt_add_translations
. Customizing various aspects of the automatisms or explicitly specifying targets, sources or .ts files is possible. To show how to do that is out of the scope of this post. Please take a look at the documentation if you're interested in more details.
Excluding targets and directories
As it happens, the jump_sim
target and the tests of our project contain translatable strings that we don't need in frogger's .qm files.
To exclude the jump_sim
target from translation, we set the target property QT_EXCLUDE_FROM_TRANSLATION to ON:
set_property(TARGET jump_sim PROPERTY QT_EXCLUDE_FROM_TRANSLATION ON)
For the tests we want to exclude every target below the tests directory. To do that, we set the directory property QT_EXCLUDE_FROM_TRANSLATIONS
in tests/CMakeLists.txt
to ON
.
# tests/CMakeLists.txt
set_directory_properties(PROPERTIES QT_EXCLUDE_FROM_TRANSLATION ON)
With this setup, we don't pass source files from jump_sim
or the tests.
Specifying source targets explicitly
For this simple project, it would be actually easier just to say that the translatable source files are in the two targets, frogger
and game_logic
. Instead of excluding parts of the project, we could call qt_add_translations
like this:
qt_add_translations(frogger
SOURCE_TARGETS frogger game_logic
)
Remember to adjust the SOURCE_TARGETS
list if you extend your project with more targets containing translatable strings! For more complex projects, automatically collecting targets and excluding unwanted parts would be advisable.
Handling plural forms in the source language
In the main game screen we display how many frogs already reached home. It's a string like this:
int numberOfFrogsAtHome = countFrogsAtHome();
tr("%n frog(s) are home", "", numberOfFrogsAtHome);
This translatable string is a plural form, and our Norwegian and German translations will display different strings depending on the number of frogs (e.g. "1 Frosch" vs. "2 Frösche" in German). It would also be nice to display "1 frog" and "2 frogs" in English. To do that, we need to create a translation from "source code English" to "human English" that only contains the plural forms.
The language our untranslated source strings are written in is called the source language of the project. Another commonly used term is development language. And we denote that fact in qt_standard_project_setup
like so:
qt_standard_project_setup(
I18N_SOURCE_LANGUAGE en
I18N_TRANSLATED_LANGUAGES nb de
)
The qt_add_translations
command will automatically create a frogger_en.ts
file that only contains plural form strings. We fire up Qt Linguist to "translate" the handful of plural forms, and now frogger's game progress display can show "1 frog is home" and "2 frogs are home".
If your source language is English, you don't have to specify I18N_SOURCE_LANGUAGE
, because that's the default. You can prevent the creation of a plurals-only file by passing NO_GENERATE_PLURALS_TS_FILE
to qt_add_translations
.
Conclusion
The full example listing looks like this:
cmake_minimum_required(VERSION 3.28)
project(frogger)
find_package(Qt6 COMPONENTS OpenGLWidgets)
qt_standard_project_setup(
I18N_TRANSLATED_LANGUAGES nb de
)
add_subdirectory(3rdparty) # adds target jump_sim
set_property(TARGET jump_sim PROPERTY QT_EXCLUDE_FROM_TRANSLATION ON)
qt_add_library(game_logic src/game_logic/stuff.cpp ...)
target_link_libraries(game_logic PRIVATE jump_sim)
qt_add_executable(frogger src/frogger/main.cpp ...)
target_link_libraries(frogger PRIVATE game_logic)
# in tests/CMakeLists.txt we have
# set_directory_properties(PROPERTIES QT_EXCLUDE_FROM_TRANSLATION ON)
add_subdirectory(tests) # adds several targets
qt_add_translations()
We've discussed the shortcomings of the i18n CMake API we introduced in Qt 6.2 and how we addressed this with the revisited i18n CMake API in Qt 6.7. The key takeaways are
- We now have a project-wide view of translations instead of a single-target view.
- It's possible (and advisable) to specify the supported languages on the project level.
qt_add_translations
can automatically collect targets from which source files are used as input for lupdate.- Targets and directories can be excluded from this automatic collection process.
- If no automatic target collection is desired, targets can be explicitly passed.
qt_add_translations
can automatically generate the names of .ts files. This can also be turned off.- There's now direct support for creating a plural-form .ts file for the native language of the project.
Please play with this new API. We're happy to receive your feedback.
Blog Topics:
Comments
Subscribe to our newsletter
Subscribe Newsletter
Try Qt 6.8 Now!
Download the latest release here: www.qt.io/download.
Qt 6.8 release focuses on technology trends like spatial computing & XR, complex data visualization in 2D & 3D, and ARM-based development for desktop.
We're Hiring
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.