2

I am currently evaluating the C++ framework Qt v5.4.1. Currently I am trying to understand and apply Internationalization with Qt.

I successfully implemented switching of the human language at runtime via the help of the article How to create a multi lingual application that can switch the language at runtime? and I know how-to work with Qt Linguist Translation source (.ts) files and how to generate Qt Linguist Message (.qm) files. I use the build system CMake to automate the generation and it works very nice.

In my project the translations are loaded from a Qt Resource Collection (.qrc) file compiled into the application. I know how-to translate "static" strings via the member functions QObject::tr() and QObject::translate().

Now comes the tricky part: I want to add languages while my application evolves. Currently I have the following two .ts files:

  • foo_ui_de_DE.ts
  • foo_ui_en_US.ts

These are compiled via lrelease into the following two .qm files by the build process:

  • foo_ui_de_DE.qm
  • foo_ui_en_US.qm

The build process automatically generates a .qrc file translations.qrc and compiles that file into the executable via rcc.

Relevant source code from the declaration file (.h):

#include <QMainWindow>
#include <QLocale>
#include <QString>
#include <QTranslator>

class MainWindow : public QMainWindow {
  Q_OBJECT

 public:
  /**
   * Initializes a new instance of the MainWindow class with the given parent.
   *
   * @param parent The parent.
   */
  explicit MainWindow(QWidget* parent = 0);

 private slots:
  /**
   * Loads a language by the given language shortcut (e.g. `de_DE`, `en_US`).
   */
  void LoadLanguage(QLocale const& kLocale);

  void SwitchTranslator(QTranslator& translator,
                        QString const& kLocale,
                        QString const& kFilename);

  /**
   * Creates the language menu dynamically.
   */
  void CreateLanguageMenu();

 protected:
  /**
   * Handler which is activated when a new translator is loaded or the system
   * language is changed.
   */
  void changeEvent(QEvent* event);

 protected slots:
  /**
   * Slot which is called by the language menu actions.
   */
  void slotLanguageChanged(QAction* action);

 private:
  /**
   * The translations for this application.
   */
  QTranslator translator_;

  /**
   * The translations for the Qt Widgets used in this application.
   */
  QTranslator qt_translator_;

  /**
   * Contains the currently loaded locale.
   */
  QLocale locale_;

  /**
   * The main window of the application.
   */
  Ui::MainWindow* ui_;
};

Relevant source code from the definition file (.cc):

#include <QLibraryInfo>

#include "main_window.h"

#include "ui_main_window.h"

MainWindow::MainWindow(QWidget* the_parent)
    : QMainWindow{the_parent},
      ui_{new Ui::MainWindow} {
  ui_->setupUi(this);
  CreateLanguageMenu();
}

MainWindow::~MainWindow() {
  delete ui_;
}

void MainWindow::LoadLanguage(QLocale const& kNewLocale) {
  QLocale::setDefault(kNewLocale);
  QString const kLanguageName{QLocale::languageToString(kNewLocale.language())};

  SwitchTranslator(translator_, "qt_" + kNewLocale.bcp47Name(),
                   QLibraryInfo::location(QLibraryInfo::TranslationsPath));
  SwitchTranslator(qt_translator_,
                   qApp->applicationName() + '_' + kNewLocale.name(),
                   ":/translations");
  statusBar()->showMessage(
      tr("Language changed to %1").arg(kLanguageName));
  locale_ = kNewLocale;
}

void MainWindow::SwitchTranslator(QTranslator& translator,
                                  QString const& kLocale,
                                  QString const& kFilename) {
  qApp->removeTranslator(&translator);

  if (translator.load(kLocale, kFilename)) {
    qApp->installTranslator(&translator);
  }
}

void MainWindow::CreateLanguageMenu() {
  // TODO(wolters): This is not optimal, since it does not work automatically
  // with the .qm files added as a resource to the application.
  //: Translation for the human language German.
  QT_TR_NOOP("German");
  //: Translation for the human language English.
  QT_TR_NOOP("English");

  QActionGroup* language_group{new QActionGroup(ui_->menuLanguage)};
  language_group->setExclusive(true);

  connect(language_group, SIGNAL(triggered(QAction*)), this,
          SLOT(slotLanguageChanged(QAction*)));

  QLocale const kDefaultLocale{QLocale::system()};
  QDir const kDirectory{QApplication::applicationDirPath() + "/.."};
  QStringList const kFileNames{kDirectory.entryList(QStringList("*.qm"))};

  for (QString const& kFileName : kFileNames) {
    QLocale const kLocale{QFileInfo{kFileName}.completeBaseName().replace(
        qApp->applicationName() + "_", "")};
    QString const kCountryCode{
        kLocale.name().toLower().mid(kLocale.name().lastIndexOf('_') + 1)};
    QIcon const kIcon{":/icons/flags/" + kCountryCode + ".png"};
    QAction* action{new QAction{
        kIcon,
        // TODO(wolters): This does not work.
        tr(QLocale::languageToString(kLocale.language()).toStdString().c_str()),
        this}};
    action->setCheckable(true);
    action->setData(kLocale);

    ui_->menuLanguage->addAction(action);
    language_group->addAction(action);

    if (kDefaultLocale == kLocale) {
      action->setChecked(true);
    }
  }
}

void MainWindow::changeEvent(QEvent* the_event) {
  if (nullptr != the_event) {
    switch (the_event->type()) {
      // QEvent::LanguageChange is send if a translator is loaded.
      case QEvent::LanguageChange:
        ui_->retranslateUi(this);
        break;
      // QEvent::LocaleChange is send, if the system language changes.
      case QEvent::LocaleChange:
        LoadLanguage(QLocale::system());
        break;
      default:
        break;
    }
  }

  QMainWindow::changeEvent(the_event);
}

void MainWindow::slotLanguageChanged(QAction* action) {
  if (nullptr != action) {
    LoadLanguage(qvariant_cast<QLocale>(action->data()));
  }
}

The source code already describes the problem I have in the comments.

  1. If the application is started, the current system locale is used to re-translate the UI. This works and I can see that the items in the menu Languages appear in the German human language (I've translated both items in the .ts files). But when I switch the language via the menu from German to English, both item labels do not get translated.
  2. The approach is in general not optimal, since I do not want to modify the source code (QT_TR_NOOP above), if a new human language is added to the application. The optimal work flow would be:
    1. Add all supported languages to all .ts files in their proper language.
    2. Dynamically translate the menu items.

I think I am misunderstanding something, but I couldn't found a solution while searching the WWW for a while.

Update 2015-04-01: I think I use the wrong approach. The important part is that the Languages menu is created dynamically in the member function CreateLanguageMenu(). I need an answer to the question how-to translate the dynamically created menu items. So it's all about the line QAction* action{new QAction{kIcon, tr(QLocale::languageToString(kLocale.language()).toStdString().c_str()), this}}; in that function. I need some kind of lookup-functionality available at compile time I think...

Florian Wolters
  • 3,820
  • 5
  • 35
  • 55
  • How do you switch the language? Post the code. – svlasov Mar 31 '15 at 19:45
  • I've added all relevant code to the question. – Florian Wolters Mar 31 '15 at 19:57
  • Where `translator_` and `qt_translator_` are coming from? Also, it would be great if you make a self-contained compilable example. – svlasov Mar 31 '15 at 20:10
  • I've updated the question with the relevant source code from the header file. `translator_` and `qt_translator_` are declared there. But I don't think that's even relevant. I've clarified the problem in the question a bit. I cannot provide a full example at the moment. Maybe I can do it in eight hours. – Florian Wolters Apr 01 '15 at 07:08
  • Just an aside - I feel uncomfortable when I see `":/icons/flags/" + kCountryCode + ".png"`, because there's nothing like a one-to-one correspondence between countries and languages. There's plenty of insightful reading to be found online on this topic, so I won't elaborate further in this short comment. – Toby Speight Feb 10 '17 at 11:33

1 Answers1

1

As you already mentioned you need a lookup-functionality but in real time. I suggest such hack: when creating QAction objects use object name as Language identifer

QT_TR_NOOP("LANG_ENG")
QAction* langAction = ...;
langAction->setObjectName("LANG_ENG");

on Language Change event call some method to retreanslate this actions

void retranslateLangActions()
{
    QList<QAction*> widgetActions = this->findChildren<QAction*>();
    foreach(QAction* act, widgetActions) // qt foreach macro
    {
        QString objName = act->objectName();
        if (objName.startsWith("LANG_"))
        {
            act->setText(tr(objName.toStdString().c_str()));
        }
    }
}
Ruslan F.
  • 5,498
  • 3
  • 23
  • 42
  • Thank you for your reply. That is indeed a workaround, but not the final solution. I see one major problem: It does not allow to automatically add translations for menu items if new .qm files are added, since a new `QT_TRANSLATE_NOOP` has to be added to the application. But I don't see a way to avoid that, maybe I should move the `QT_TRANSLATE_NOOP` macro calls to a separate .h file and generate that file via the build system. Also, why isn't it possible to find all actions via `ui_->menuLanguage->->findChildren()`? If that would be possible, I could get rid of that ugly prefix. – Florian Wolters Apr 01 '15 at 11:29
  • One note to your code: It must be `for (QAction* act : findChildren()) {` and `act->setText(tr(objName.toStdString().c_str()));` – Florian Wolters Apr 01 '15 at 11:31
  • 1
    @Florian Wolters this is kind of a pseudo code... so if you want c++ i'll fix it a bit. ui_->menuLanguage->->findChildren() will work if you set ui_->menuLanguage as parrent in ctor – Ruslan F. Apr 01 '15 at 11:34
  • 1
    The last argument of QAction Ctor is a parrent and you put there a MianWindow ptr (this). So my code is based on yours. – Ruslan F. Apr 01 '15 at 11:39
  • Just to clarify for others, @Ruslan F. means the constructor of `QAction`. I made a mistake while creating the `QAction` objects, since the last argument was `this`. Instead `language_group` should be used, then `ui_>menuLanguage->->findChildren()` works. – Florian Wolters Apr 01 '15 at 12:20
  • 1
    I've made it work without using `QAction::objectName()` and with dynamic generation of a header file which declares all required dynamic translations in a free function. The header is included in my `main.cc` and the free function is called. It works and I do not need to modify the source code if adding a new language, but it feels like a hack for sure! So I am open for a cleaner solution. But at the moment it is *Good Enough* (TM). – Florian Wolters Apr 01 '15 at 13:59