I am looking for a relatively minimal example how to dynamically translation. I am creating a tutorial on using the Qt resource system along with UI and TS files. For this purpose I've created a basic UI form:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>widget_form</class>
<widget class="QWidget" name="widget_form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>860</width>
<height>527</height>
</rect>
</property>
<property name="windowTitle">
<string>Get Started</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="btn_import_file">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">image: url(:/icons/files/import);</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_check_file">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">image: url(:/icons/files/check);</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_delete_file">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">image: url(:/icons/files/delete);</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_add_file">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">image: url(:/icons/files/add);</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="cb_change_lang">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPlainTextEdit" name="pte_file_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="Line" name="vline_right">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="btn_csv">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">image: url(:/icons/files/csv);</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_code">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">image: url(:/icons/files/code);</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_json">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">image: url(:/icons/files/json);</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_xml">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">image: url(:/icons/files/xml);</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../resources.qrc"/>
</resources>
<connections/>
</ui>
which I add to a QRC file along with a bunch of images that I use as icons:
<RCC>
<qresource prefix="icons/files">
<file alias="xml">icons/file-formats/icons8-xml-96.png</file>
<file alias="json">icons/file-formats/icons8-json-96.png</file>
<file alias="csv">icons/file-formats/icons8-csv-96.png</file>
<file alias="code">icons/file-formats/icons8-code-96.png</file>
</qresource>
<qresource prefix="icons/files">
<file alias="import">icons/file-ops/icons8-import-96.png</file>
<file alias="delete">icons/file-ops/icons8-delete-file-96.png</file>
<file alias="check">icons/file-ops/icons8-check-file-96.png</file>
<file alias="add">icons/file-ops/icons8-add-file-96.png</file>
</qresource>
<qresource prefix="icons/flags">
<file alias="fr">icons/flags/icons8-france-80.png</file>
<file alias="de">icons/flags/icons8-germany-80.png</file>
<file alias="gb">icons/flags/icons8-great-britain-80.png</file>
<file alias="ru">icons/flags/icons8-russian-federation-80.png</file>
</qresource>
<qresource prefix="forms">
<file alias="main">ui/widget.ui</file>
</qresource>
</RCC>
I manage my project with the following CMakeLists.txt:
cmake_minimum_required(VERSION 3.13)
project(ExampleGetStarted LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE OFF)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
if(CMAKE_VERSION VERSION_LESS "3.7.0")
set(CMAKE_INCLUDE_CURRENT_DIR ON)
endif()
find_package(Qt5
COMPONENTS
Widgets
Xml
XmlPatterns
Concurrent
UiTools
LinguistTools
REQUIRED
)
#find_package(Qt5XmlPatterns REQUIRED)
# TODO https://stackoverflow.com/questions/51217734/how-to-add-qt-translations-to-cmake
set(RESOURCES ${CMAKE_CURRENT_SOURCE_DIR}/resources/resources.qrc)
set(TS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/resources/translations)
qt5_create_translation(TS_FILES ${CMAKE_CURRENT_SOURCE_DIR} ${TS_DIR}/en.ts ${TS_DIR}/de.ts)
#qt5_add_translation(TS_FILES_FINISHED ${TS_DIR}/en.ts ${TS_DIR}/de.ts)
# TODO Convert configure_file to custom post-build action. QM files are initially not available
#configure_file(${QM_FILES} ${CMAKE_BINARY_DIR} COPYONLY)
#qt5_add_translation(QM_FILES ${TS_FILES})
add_executable(example_get_started
example_get_started.cpp
${RESOURCES}
${TS_FILES}
)
target_link_libraries(example_get_started
Qt5::Widgets
Qt5::Xml Qt5::XmlPatterns
Qt5::Concurrent
Qt5::UiTools
#Qt5::LinguistTools
)
# Copy translations to binary directory where executable can access thoses
foreach(TS_FILE ${TS_FILES})
add_custom_command(
TARGET example_get_started POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${TS_FILE}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
COMMENT "Copied ${TS_FILE} to binary directory"
)
endforeach(TS_FILE)
The UI form is loaded with the following code (work in progress):
#include <vector>
#include <utility>
#include <iostream>
#include <QApplication>
#include <QtWidgets>
#include <QtUiTools>
#include <QFile>
#include <QTranslator>
QScopedPointer<QTranslator> translator(new QTranslator());
static QWidget *loadUiFile(QWidget *parent)
{
QFile file(":/forms/main");
file.open(QIODevice::ReadOnly);
QUiLoader loader;
return loader.load(&file, parent);
}
static void retranslate(QString lang_code)
{
std::cout << "Translating to " << lang_code.toStdString() << std::endl;
std::cout << "Looking into directory \"" << QApplication::applicationDirPath().toStdString() << "\"" << std::endl;
if (lang_code == QString("fr"))
{
if (translator->load("fr", QApplication::applicationDirPath()))
{
QApplication::instance()->installTranslator(translator.data());
std::cout << "Switched to " << lang_code.toStdString() << std::endl;
}
else
std::cout << "Unable to load translation for " << lang_code.toStdString() << std::endl;
}
else if (lang_code == QString("de"))
{
if (translator->load("de", QApplication::applicationDirPath()))
{
QApplication::instance()->installTranslator(translator.data());
std::cout << "Switched to " << lang_code.toStdString() << std::endl;
}
else
std::cout << "Unable to load translation for " << lang_code.toStdString() << std::endl;
}
else if (lang_code == QString("ru"))
{
if (translator->load("ru", QApplication::applicationDirPath()))
{
QApplication::instance()->installTranslator(translator.data());
std::cout << "Switched to " << lang_code.toStdString() << std::endl;
}
else
std::cout << "Unable to load translation for " << lang_code.toStdString() << std::endl;
}
else
{
QApplication::instance()->removeTranslator(translator.data());
}
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// Translations need to be created before widgets they affect
/*
if (!translator->load("en_GB.qm"))
std::cout << "Unable to load translation file \"en_GB\"" << std::endl;
app.installTranslator(translator.data());
if (!translator->load("de_DE.qm"))
std::cout << "Unable to load translation file \"de_DE\"" << std::endl;
app.installTranslator(translator.data());
std::cout << translator->language().toStdString() << std::endl;
*/
QWidget *widget = loadUiFile(nullptr);
widget->findChild<QPushButton *>("btn_add_file")->setToolTip(QObject::tr("Create a new file"));
widget->findChild<QPushButton *>("btn_check_file")->setToolTip(QObject::tr("Check the opened file's contents for errors"));
widget->findChild<QPushButton *>("btn_delete_file")->setToolTip(QObject::tr("Deletes the opened file's contents"));
widget->findChild<QPushButton *>("btn_import_file")->setToolTip(QObject::tr("Import existing file"));
QComboBox *cb_change_lang = widget->findChild<QComboBox *>("cb_change_lang");
std::vector<std::pair<QString, QIcon>> langs = {
std::pair<QString, QIcon>(QObject::tr("English (en)"), QIcon(":/icons/flags/gb")),
std::pair<QString, QIcon>(QObject::tr("Deutsch (de)"), QIcon(":/icons/flags/de")),
std::pair<QString, QIcon>(QObject::tr("Français (fr)"), QIcon(":/icons/flags/fr")),
std::pair<QString, QIcon>(QObject::tr("Руский (ru)"), QIcon(":/icons/flags/ru"))};
auto lang_idx = 0;
for (auto lang : langs)
{
cb_change_lang->insertItem(lang_idx, lang.first);
cb_change_lang->setItemIcon(lang_idx, lang.second);
lang_idx++;
}
QRegularExpression regex("\\(([^()]+)\\)");
QObject::connect(cb_change_lang,
qOverload<int>(&QComboBox::currentIndexChanged),
[=](int idx)
{
retranslate(regex.match(langs.at(cb_change_lang->currentIndex()).first).captured(1));
});
std::cout << regex.match(langs.at(cb_change_lang->currentIndex()).first).captured(1).toStdString() << std::endl;
widget->show();
return app.exec();
}
The widget looks like this:
I would like to keep the creation of my widget as it is (UI file loaded in C++ and some extra functionality added to it). The reason why my QTranslator is a global is because of that plus the fact that a translator needs to live as long as QApplication's instance is alive.
The way it's supposed to work (but currently is not) is for the tooltips of each button to be translated whenever the currently selected item inside the QComboBox (top right corner in the screenshot) is changed. I am very new to the translation facilities of Qt and the documentation is not really stellar in this regard (personal opinion here).
Before I started working on the dynamic translation, I loaded one of the QM files inside my main() and it worked - the tooltips were in the language the QM file was made for. Only after adding the new functionality I don't see any change.




