From 68a5837a65706c6208131eee384e237762b3f833 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Fri, 28 Jan 2022 12:11:04 -0500
Subject: [PATCH] settings: add a settings to change the language of the app

Move installTranslator into the settings manager and add a settings
in the SystemSettings to be able to dynamically change the language
of jami-qt.

https://git.jami.net/savoirfairelinux/jami-project/-/issues/1342

Change-Id: I4f720fa50d5e313356dbdf1b8acb4e98d74401e4
---
 src/appsettingsmanager.cpp                    | 75 +++++++++++++++++++
 src/appsettingsmanager.h                      | 11 ++-
 src/constant/JamiStrings.qml                  |  1 +
 src/mainapplication.cpp                       | 58 ++------------
 src/mainapplication.h                         |  4 +-
 .../components/SystemSettings.qml             | 29 +++++++
 src/utilsadapter.cpp                          | 39 +++++++++-
 src/utilsadapter.h                            |  1 +
 8 files changed, 164 insertions(+), 54 deletions(-)

diff --git a/src/appsettingsmanager.cpp b/src/appsettingsmanager.cpp
index 614a027c8..b115dd3e1 100644
--- a/src/appsettingsmanager.cpp
+++ b/src/appsettingsmanager.cpp
@@ -20,6 +20,11 @@
 
 #include "appsettingsmanager.h"
 
+#include <QCoreApplication>
+#include <QLibraryInfo>
+
+#include <locale.h>
+
 const QString defaultDownloadPath = QStandardPaths::writableLocation(
     QStandardPaths::DownloadLocation);
 
@@ -51,3 +56,73 @@ AppSettingsManager::setValue(const Settings::Key key, const QVariant& value)
 {
     settings_->setValue(Settings::toString(key), value);
 }
+
+void
+AppSettingsManager::loadTranslations()
+{
+#if defined(Q_OS_LINUX) && defined(JAMI_INSTALL_PREFIX)
+    QString appDir = JAMI_INSTALL_PREFIX;
+#else
+    QString appDir = qApp->applicationDirPath() + QDir::separator() + "share";
+#endif
+
+    // Remove previously installed translators
+    for (auto* tr : installedTr_)
+        qApp->removeTranslator(tr);
+    installedTr_.clear();
+
+    auto pref = getValue(Settings::Key::LANG).toString();
+
+    QString locale_name = pref == "SYSTEM" ? QLocale::system().name() : pref;
+    qDebug() << QString("Using locale: %1").arg(locale_name);
+    QString locale_lang = locale_name.split('_')[0];
+
+    QTranslator* qtTranslator_lang = new QTranslator(qApp);
+    QTranslator* qtTranslator_name = new QTranslator(qApp);
+    if (locale_name != locale_lang) {
+        if (qtTranslator_lang->load("qt_" + locale_lang,
+                                    QLibraryInfo::path(QLibraryInfo::TranslationsPath)))
+            qApp->installTranslator(qtTranslator_lang);
+        installedTr_.append(qtTranslator_lang);
+    }
+
+    if (qtTranslator_name->load("qt_" + locale_name,
+                                QLibraryInfo::path(QLibraryInfo::TranslationsPath))) {
+        qApp->installTranslator(qtTranslator_name);
+        installedTr_.append(qtTranslator_name);
+    }
+
+    QTranslator* lrcTranslator_lang = new QTranslator(qApp);
+    QTranslator* lrcTranslator_name = new QTranslator(qApp);
+    if (locale_name != locale_lang) {
+        if (lrcTranslator_lang->load(appDir + QDir::separator() + "libringclient" + QDir::separator()
+                                     + "translations" + QDir::separator() + "lrc_" + locale_lang)) {
+            qApp->installTranslator(lrcTranslator_lang);
+            installedTr_.append(lrcTranslator_lang);
+        }
+    }
+    if (lrcTranslator_name->load(appDir + QDir::separator() + "libringclient" + QDir::separator()
+                                 + "translations" + QDir::separator() + "lrc_" + locale_name)) {
+        qApp->installTranslator(lrcTranslator_name);
+        installedTr_.append(lrcTranslator_name);
+    }
+
+    QTranslator* mainTranslator_lang = new QTranslator(qApp);
+    QTranslator* mainTranslator_name = new QTranslator(qApp);
+    if (locale_name != locale_lang) {
+        if (mainTranslator_lang->load(appDir + QDir::separator() + "ring" + QDir::separator()
+                                      + "translations" + QDir::separator() + "ring_client_windows_"
+                                      + locale_lang)) {
+            qApp->installTranslator(mainTranslator_lang);
+            installedTr_.append(mainTranslator_lang);
+        }
+    }
+    if (mainTranslator_name->load(appDir + QDir::separator() + "ring" + QDir::separator()
+                                  + "translations" + QDir::separator() + "ring_client_windows_"
+                                  + locale_name)) {
+        qApp->installTranslator(mainTranslator_name);
+        installedTr_.append(mainTranslator_name);
+    }
+
+    Q_EMIT retranslate();
+}
\ No newline at end of file
diff --git a/src/appsettingsmanager.h b/src/appsettingsmanager.h
index f316d9b74..0ea1c6bba 100644
--- a/src/appsettingsmanager.h
+++ b/src/appsettingsmanager.h
@@ -28,6 +28,8 @@
 #include <QStandardPaths>
 #include <QWindow> // for QWindow::AutomaticVisibility
 
+#include <QTranslator>
+
 extern const QString defaultDownloadPath;
 
 // clang-format off
@@ -47,7 +49,8 @@ extern const QString defaultDownloadPath;
     X(ShowChatviewHorizontally, true) \
     X(NeverShowMeAgain, false) \
     X(WindowGeometry, QRectF(qQNaN(), qQNaN(), 0., 0.)) \
-    X(WindowState, QWindow::AutomaticVisibility)
+    X(WindowState, QWindow::AutomaticVisibility) \
+    X(LANG, "SYSTEM")
 
 /*
  * A class to expose settings keys in both c++ and QML.
@@ -102,6 +105,12 @@ public:
     Q_INVOKABLE QVariant getValue(const Settings::Key key);
     Q_INVOKABLE void setValue(const Settings::Key key, const QVariant& value);
 
+    void loadTranslations();
+
+Q_SIGNALS:
+    void retranslate();
+
 private:
     QSettings* settings_;
+    QVector<QTranslator*> installedTr_ {};
 };
diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml
index 2262e59a3..8080aca88 100644
--- a/src/constant/JamiStrings.qml
+++ b/src/constant/JamiStrings.qml
@@ -376,6 +376,7 @@ Item {
     property string enableTypingIndicator: qsTr("Enable typing indicators")
     property string displayHyperlinkPreviews: qsTr("Display hyperlink previews in the chatview")
     property string chatviewPositionInCall: qsTr("Chatview's position in calls")
+    property string language: qsTr("User interface language")
     property string bottomOpt: qsTr("Bottom")
     property string rightOpt: qsTr("Right")
 
diff --git a/src/mainapplication.cpp b/src/mainapplication.cpp
index d515cd8fc..099781954 100644
--- a/src/mainapplication.cpp
+++ b/src/mainapplication.cpp
@@ -148,6 +148,10 @@ MainApplication::MainApplication(int& argc, char** argv)
     : QApplication(argc, argv)
 {
     QObject::connect(this, &QApplication::aboutToQuit, [this] { cleanup(); });
+    QObject::connect(settingsManager_.get(),
+                     &AppSettingsManager::retranslate,
+                     this,
+                     &MainApplication::retranslate);
 }
 
 MainApplication::~MainApplication()
@@ -181,7 +185,7 @@ MainApplication::init()
     }
 
     Utils::removeOldVersions();
-    loadTranslations();
+    settingsManager_->loadTranslations();
     setApplicationFont();
 
 #if defined _MSC_VER
@@ -262,57 +266,9 @@ MainApplication::restoreApp()
 }
 
 void
-MainApplication::loadTranslations()
+MainApplication::retranslate()
 {
-#if defined(Q_OS_LINUX) && defined(JAMI_INSTALL_PREFIX)
-    QString appDir = JAMI_INSTALL_PREFIX;
-#else
-    QString appDir = qApp->applicationDirPath() + QDir::separator() + "share";
-#endif
-
-    QString locale_name = QLocale::system().name();
-    QString locale_lang = locale_name.split('_')[0];
-
-    QTranslator* qtTranslator_lang = new QTranslator(this);
-    QTranslator* qtTranslator_name = new QTranslator(this);
-    if (locale_name != locale_lang) {
-        if (qtTranslator_lang->load("qt_" + locale_lang,
-                                    QLibraryInfo::path(QLibraryInfo::TranslationsPath)))
-            installTranslator(qtTranslator_lang);
-    }
-
-    if (qtTranslator_name->load("qt_" + locale_name,
-                                QLibraryInfo::path(QLibraryInfo::TranslationsPath))) {
-        installTranslator(qtTranslator_name);
-    }
-
-    QTranslator* lrcTranslator_lang = new QTranslator(this);
-    QTranslator* lrcTranslator_name = new QTranslator(this);
-    if (locale_name != locale_lang) {
-        if (lrcTranslator_lang->load(appDir + QDir::separator() + "libringclient" + QDir::separator()
-                                     + "translations" + QDir::separator() + "lrc_" + locale_lang)) {
-            installTranslator(lrcTranslator_lang);
-        }
-    }
-    if (lrcTranslator_name->load(appDir + QDir::separator() + "libringclient" + QDir::separator()
-                                 + "translations" + QDir::separator() + "lrc_" + locale_name)) {
-        installTranslator(lrcTranslator_name);
-    }
-
-    QTranslator* mainTranslator_lang = new QTranslator(this);
-    QTranslator* mainTranslator_name = new QTranslator(this);
-    if (locale_name != locale_lang) {
-        if (mainTranslator_lang->load(appDir + QDir::separator() + "ring" + QDir::separator()
-                                      + "translations" + QDir::separator() + "ring_client_windows_"
-                                      + locale_lang)) {
-            installTranslator(mainTranslator_lang);
-        }
-    }
-    if (mainTranslator_name->load(appDir + QDir::separator() + "ring" + QDir::separator()
-                                  + "translations" + QDir::separator() + "ring_client_windows_"
-                                  + locale_name)) {
-        installTranslator(mainTranslator_name);
-    }
+    engine_->retranslate();
 }
 
 void
diff --git a/src/mainapplication.h b/src/mainapplication.h
index 53a1ceb07..62eb26f1d 100644
--- a/src/mainapplication.h
+++ b/src/mainapplication.h
@@ -72,7 +72,6 @@ private:
     void vsConsoleDebug();
     void fileDebug(QFile* debugFile);
 
-    void loadTranslations();
     void initLrc(const QString& downloadUrl, ConnectivityMonitor* cm, bool logDaemon);
     const QVariantMap parseArguments();
     void setApplicationFont();
@@ -80,6 +79,9 @@ private:
     void initSystray();
     void cleanup();
 
+public Q_SLOTS:
+    void retranslate();
+
 private:
     QScopedPointer<QFile> debugFile_;
     QScopedPointer<QQmlApplicationEngine> engine_;
diff --git a/src/settingsview/components/SystemSettings.qml b/src/settingsview/components/SystemSettings.qml
index 952d24fdd..1e48934ff 100644
--- a/src/settingsview/components/SystemSettings.qml
+++ b/src/settingsview/components/SystemSettings.qml
@@ -160,4 +160,33 @@ ColumnLayout {
             onClicked: downloadPathDialog.open()
         }
     }
+
+    SettingsComboBox {
+        id: langComboBoxSetting
+
+        Layout.fillWidth: true
+        Layout.preferredHeight: JamiTheme.preferredFieldHeight
+        Layout.leftMargin: JamiTheme.preferredMarginSize
+
+        labelText: JamiStrings.language
+        fontPointSize: JamiTheme.settingsFontSize
+        comboModel: ListModel {
+            Component.onCompleted: {
+                var supported = UtilsAdapter.supportedLang();
+                var keys = Object.keys(supported);
+                var currentKey = UtilsAdapter.getAppValue(Settings.Key.LANG);
+                for (var i = 0 ; i < keys.length ; ++i) {
+                    append({ textDisplay: supported[keys[i]], id: keys[i] })
+                    if (keys[i] == currentKey)
+                        langComboBoxSetting.modelIndex = i
+                }
+            }
+        }
+        widthOfComboBox: itemWidth
+        role: "textDisplay"
+
+        onActivated: {
+            UtilsAdapter.setAppValue(Settings.Key.LANG, comboModel.get(modelIndex).id)
+        }
+    }
 }
diff --git a/src/utilsadapter.cpp b/src/utilsadapter.cpp
index 46614a23b..274a90cc4 100644
--- a/src/utilsadapter.cpp
+++ b/src/utilsadapter.cpp
@@ -33,6 +33,7 @@
 #include <QApplication>
 #include <QClipboard>
 #include <QFileInfo>
+#include <QRegExp>
 
 UtilsAdapter::UtilsAdapter(AppSettingsManager* settingsManager,
                            SystemTray* systemTray,
@@ -338,6 +339,9 @@ void
 UtilsAdapter::setAppValue(const Settings::Key key, const QVariant& value)
 {
     settingsManager_->setValue(key, value);
+    // If we change the lang preference, reload the translations
+    if (key == Settings::Key::LANG)
+        settingsManager_->loadTranslations();
 }
 
 QString
@@ -411,6 +415,39 @@ UtilsAdapter::clearInteractionsCache(const QString& accountId, const QString& co
             auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
             auto& convModel = accInfo.conversationModel;
             convModel->clearInteractionsCache(convId);
-        } catch (...) {}
+        } catch (...) {
+        }
+    }
+}
+
+QVariantMap
+UtilsAdapter::supportedLang()
+{
+#if defined(Q_OS_LINUX) && defined(JAMI_INSTALL_PREFIX)
+    QString appDir = JAMI_INSTALL_PREFIX;
+#else
+    QString appDir = qApp->applicationDirPath() + QDir::separator() + "share";
+#endif
+    auto trDir = QDir(appDir + QDir::separator() + "ring" + QDir::separator() + "translations");
+    QStringList trFiles = trDir.entryList(QStringList() << "ring_client_windows_*.qm", QDir::Files);
+    QVariantMap result;
+    result["SYSTEM"] = tr("System");
+    // Get available locales
+    QRegExp regex("ring_client_windows_(.*).qm");
+    QSet<QString> nativeNames;
+    for (const auto& f : trFiles) {
+        auto match = regex.indexIn(f);
+        if (regex.capturedTexts().size() == 2) {
+            const auto& l = regex.capturedTexts()[1];
+            auto nativeName = QLocale(l).nativeLanguageName();
+            if (nativeName.isEmpty()) // If a locale doesn't have any nativeLanguageName, ignore it.
+                continue;
+            // Avoid to show potential duplicates.
+            if (!nativeNames.contains(nativeName)) {
+                result[l] = nativeName;
+                nativeNames.insert(nativeName);
+            }
+        }
     }
+    return result;
 }
diff --git a/src/utilsadapter.h b/src/utilsadapter.h
index 2b5f60d4d..a1a5eb1d8 100644
--- a/src/utilsadapter.h
+++ b/src/utilsadapter.h
@@ -90,6 +90,7 @@ public:
     Q_INVOKABLE void setDownloadPath(QString dir);
     Q_INVOKABLE void monitor(const bool& continuous);
     Q_INVOKABLE void clearInteractionsCache(const QString& accountId, const QString& convUid);
+    Q_INVOKABLE QVariantMap supportedLang();
 
 Q_SIGNALS:
     void debugMessageReceived(const QString& message);
-- 
GitLab