Localized Clock with ID-based Translation | Qt Linguist Manual
Description: The example shows best practices for using Qt's ID-based translation features in CMake and Qt Quick including the handling of plurals in different languages, and localized time formats.

- Qt 6.11
- Qt Linguist Manual
- Qt Linguist Examples
- Localized Clock with ID-based Translation
- Qt 6.11.2 Reference Documentation

# Localized Clock with ID-based Translation

The example shows best practices for using Qt's ID-based translation features in CMake and Qt Quick including the handling of plurals in different languages, and localized time formats.

#### User Interface

The example shows the current time and date in your system's locale and language. Translation for these languages is supported: English, German, French, Spanish, Italian, Japanese, Korean, Portuguese, Arabic, and Chinese. If your desktop is in another language, it falls back to English.

The example also accepts a locale as a command-line argument to test different languages and locales without altering your system.: `localizedClockIdBased --locale en_GB` or `localizedClockIdBased --locale de`

By default, the application shows the time and date in the current locale, but it also provides a button that opens a dialog with a list of time zones. The user can use the dialog to change the time zone of the clock.

##### ID-based Translation

In this example, we use ID-based translation, where translatable texts are identified by a unique ID rather than the traditional context + text combination (see Text ID-based translations). This approach allows translations to be reused across different contexts. We demonstrate this by sharing a translation between Main.qml and the Time Zone Dialog, a QWidget-based form written in C++. Another aspect of ID-based translation is that it separates the text shown in the UI from the developers, making the source code independent of the actual words presented to the user.

In the following two screenshots from the application in English, the two instances of the text "Select time zone: " in the main window (QML) and the dialog (C++) share the same translation by using the same ID.

Screenshot from the application window in English version:

The dialog with a list of time zones (English version):

The main window of the application in German:

The dialog with a list of time zones (German version):

#### Implementation

The application has five parts:

- CMakeLists.txt
- main.cpp
- Time Zone Dialog
- Time Zone Manager
- Main.qml

##### CMakeLists.txt

The CMake file of the application enables Qt's ID-based translation and localization support. Here are the relevant pieces:

`find_package(Qt6 REQUIRED COMPONENTS Core Linguist Qml Quick)`: Finds and links the required Qt 6 modules, including `Linguist`, essential for internationalization. `qt_standard_project_setup()`: Sets up the internationalization system with support for the listed locales. Although the source language is English, an English translation is still required when using ID-based translation. The source code only contains the IDs and does not see the source texts. Hence, we need to set up the project with the English translation, so that qt_add_translations creates a TS file for English; otherwise, they will be missing at runtime.

     qt_standard_project_setup(REQUIRES 6.8
         I18N_TRANSLATED_LANGUAGES de ar ko zh ja fr it es pt en)

`qt_add_translations(...)`: Bundles the functionality of `lupdate` and `lrelease` by generating the translation source files (TS files) in the "i18n" directory using `clock` as the base name, and compiling them into binary `.qm` files if they contain translations.

- Generates one TS file per language listed in `I18N_TRANSLATED_LANGUAGES` of `qt_standard_project_setup`.
- `MERGE_QT_TRANSLATIONS` and `QT_TRANSLATION_CATALOGS qtbase` include the Qt translations in the project. This is necessary to translate the buttons of the `QDialog` widget in the Time Zone Dialog. As the text on those buttons is controlled by QDialog, without including Qt translations those buttons do not get translated (see the translated texts of the dialog in the German screenshots in ID-based Translation).

     qt_add_translations(localizedClockIdBased
         TS_FILE_BASE i18n/clock
         MERGE_QT_TRANSLATIONS
         QT_TRANSLATION_CATALOGS qtbase
         RESOURCE_PREFIX i18n
     )

`qt_add_qml_module(...)`: Adds a QML module under the URI `qtexamples.localizedclock`, includes the Main.qml file, and imports the source and header files of Time Zone Manager into the QML module.

     qt_add_qml_module(localizedClockIdBased
         URI qtexamples.localizedclock
         VERSION 1.0
         QML_FILES
             Main.qml
         RESOURCES dialog.ui
         SOURCES
             timezonemanager.h timezonemanager.cpp
             dialog.h dialog.cpp
     )

##### main.cpp

The starting point of the application. This part is responsible for setting the locale, installing required translations, and loading UI. Below is an explanation of the relevant pieces of code:

Define the locale argument, e.g., `--locale en_US` or `--locale de_DE`:

         QCommandLineParser parser;
         QCommandLineOption localeOption("locale"_L1, "Locale to be used in the user interface"_L1,
                                         "locale"_L1);
         parser.addOption(localeOption);
         parser.addHelpOption();
         parser.process(app);

Parse the arguments, fetch the provided locale, and set the input locale as the default locale of the application:

             QLocale locale(parser.value(localeOption));
             qInfo() << "Setting locale to" << locale.name();
             QLocale::setDefault(locale);

Install the English translation regardless of the locale, to allow incomplete translations for other languages. QTranslator queries translations for texts in the reversed order in which the translations are installed:

         QTranslator enPlurals;
         const auto enPluralsPath = ":/i18n/clock_en.qm"_L1;
         if (!enPlurals.load(enPluralsPath))
             qFatal("Could not load %s!", qUtf8Printable(enPluralsPath));
         app.installTranslator(&enPlurals);

Install a translation according to the given locale:

         QTranslator translation;
         if (QLocale().language() != QLocale::English) {
             if (translation.load(QLocale(), "clock"_L1, "_"_L1, ":/i18n/"_L1)) {
                 qInfo("Loading translation %s",
                       qUtf8Printable(QDir::toNativeSeparators(translation.filePath())));
                 if (!app.installTranslator(&translation))
                     qWarning("Could not install %s!",
                              qUtf8Printable(QDir::toNativeSeparators(translation.filePath())));
             } else {
                 qInfo("Could not load translation to %s. Using English.",
                       qUtf8Printable(QLocale().name()));
             }
         }

As we installed English translation in the previous step, we might end up with two installed translations. Qt uses the most recently installed translation for any overlapping keys. Therefore, the locale-specific translation will take precedence over English, and in case of any missing translations, QTranslator falls back to English.

##### Time Zone Dialog

This class is a C++ QWidget-based dialog (QDialog) that shows a QComboBox containing a list of time zones. Below is an explanation of the code:

Enable ID-based translation in the UI form (dialog.ui):

     <ui version="4.0" idbasedtr="true">

Set the title with ID-based translation (dialog.ui). Here, "title" is the unique ID of the translation:

     <property name="windowTitle">
         <string id="title">Time Zone</string>
     </property>

Add a label with ID-based translation (dialog.ui), with the ID "timezonelabel":

     <widget class="QLabel" name="label">
     ...
     <property name="text">
     <string id="timezonelabel">Select time zone</string>
     </property>
     ...
     </widget>

##### Time Zone Manager

A `QML_SINGLETON` class in `C++` responsible for handling time zone changes.

Upon selecting a time zone by the user, the instance of `TimeZoneManager` remembers the chosen time zone:

             connect(m_dialog.get(), &Dialog::timeZoneSelected, this, &TimeZoneManager::setTimeZone);

Updating the time zone emits a TimeZoneManager::timeZoneChanged signal:

             connect(m_dialog.get(), &Dialog::timeZoneSelected, this, &TimeZoneManager::setTimeZone);
         }
         m_dialog->show();
     }

`TimeZoneManager::currentTimeZoneOffsetMs()` is marked with Q_INVOKABLE and returns the time offset of the selected time zone. Since the `TimeZoneManager` class is declared with QML_ELEMENT and QML_SINGLETON, the method can be directly accessed from QML to update the presented time; also, see Main.qml.

     qint64 TimeZoneManager::currentTimeZoneOffsetMs()
     {
         const QTimeZone tz(m_timeZone.toLatin1());
         if (!tz.isValid())
             return 0;
         const QDateTime nowUtc = QDateTime::currentDateTimeUtc();

         const int targetOffset = tz.offsetFromUtc(nowUtc);
         const int systemOffset = QTimeZone::systemTimeZone().offsetFromUtc(nowUtc);

         return (targetOffset - systemOffset) * 1000;
     }

##### Main.qml

The main QML file defines the application's user interface. The UI presents time, date, current time zone, and a counter for seconds. It also provides a button that opens a Time Zone Dialog to change the time zone. Below is an explanation of the relevant pieces of code:

Define the window and set its title using qsTrId() for ID-based translation. The text for the source language is specified using the meta string notation `//%` (see Text ID-based translations). lupdate parses the meta strings and writes the defined source texts to the TS file. As the source text is specified using meta strings and in the form of a comment, they are not visible to the application at runtime. Hence, when the application is loaded in the English locale, even though the source language is in English, it still needs to install the English translation to present the English texts. Otherwise, the raw ID "Main-Digital-Clock" is shown. This is also why we specified "en" in `I18N_TRANSLATED_LANGUAGES` by the setup in CMakeLists.txt.

     //% "Digital Clock"
     title: qsTrId("Main-Digital-Clock")

Show the number of seconds using qsTrId() with plural support. Again here the text in the source language is specified using the `//%` meta string. The plural form is enabled by using the special notation "%n" in the source text in the meta string (see Handle Plural Forms). Depending on the value of n, the translation function returns a different translation, with the correct grammatical number for the target language. For instance in English, if the value of `root.seconds` is larger than one, the plural form is used, otherwise the singular form is used. In Translation Rules for Plural Forms you find the plural rules for different languages.

     //% "%n second(s)"
     text: qsTrId("Main-n-second-s", root.seconds)

Display the currently selected time zone using ID-based translation, with `//% "Time zone: "` specifying the text in the source language:

     //% "Time zone: "
     text: qsTrId("timezone") + TimeZoneManager.timeZone;

A Button to open the Time Zone Dialog. The text on the button is specified using qsTrId() for ID-based translation. In this case, the source text is not defined by meta strings anymore since this is a reuse of the ID "timezonelabel", which was previously used in the dialog.ui document (see Time Zone Dialog). In ID-based translation, it suffices to specify the source text per ID only once in the project:

             Button {
                 text: qsTrId("timezonelabel")
                 onClicked: TimeZoneManager.openDialog()
                 Layout.alignment: Qt.AlignHCenter
             }

Upon changing the time zone and receiving the `TimeZoneManager::timeZoneChanged()` signal (see Time Zone Manager), update the `diff` variable with the time offset of the selected time zone:

         Connections {
             target: TimeZoneManager
             function onTimeZoneChanged() {
                 root.diff = TimeZoneManager.currentTimeZoneOffsetMs();
             }
         }

Declare a Timer that triggers every second and updates the time, date, and seconds properties. The timer calculates the time by adding the current time to the time offset of the selected time zone:

         Timer {
             interval: 1000
             running: true
             repeat: true
             triggeredOnStart: true

             onTriggered: {
                 const now = new Date(new Date().getTime() + root.diff);
                 const locale = Qt.locale();
                 root.time = now.toLocaleTimeString(locale, Locale.ShortFormat);
                 root.date = now.toLocaleDateString(locale);
                 root.seconds = now.getSeconds();
             }
         }

The locale affects how dates and times are displayed. These are formatted according to the current locale's country conventions. For example, a German locale results in 24-hour time and DD.MM.YYYY date format, while a US locale uses a 12-hour clock and MM/DD/YYYY date format.

Example project @ code.qt.io