[office/crow-translate] src: Add LibreTranslate API key and a Mozhi/direct API toggle

Maciej Bonin <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit dfad0cb8a990c7a3eceb259c60a64f41e709c624 by Maciej Bonin, on behalf of Mauritius Clemens.
Committed on 16/08/2026 at 14:32.
Pushed by pillowtrucker into branch 'master'.

Add LibreTranslate API key and a Mozhi/direct API toggle

The Mozhi provider gains an optional API key and a toggle between the
Mozhi-shaped API (the aggregator's GET /api/translate) and the direct
LibreTranslate REST API (POST /translate with q/source/target/format/api_key),
so self-hosted LibreTranslate instances work while the default Mozhi behavior
is preserved. The key and toggle are threaded through the provider,
ProviderOptionsManager and AppSettings, and exposed as a password field plus a
checkbox in the Mozhi settings tab.

M  +60   -0    src/onlinetranslator.cpp
M  +32   -0    src/onlinetranslator.h
M  +2    -0    src/provideroptionsmanager.cpp
M  +20   -0    src/settings/appsettings.cpp
M  +4    -0    src/settings/appsettings.h
M  +4    -0    src/settings/settingsdialog.cpp
M  +71   -33   src/settings/settingsdialog.ui
M  +15   -1    src/translator/mozhitranslationprovider.cpp

https://invent.kde.org/office/crow-translate/-/commit/dfad0cb8a990c7a3eceb259c60a64f41e709c624

diff --git a/src/onlinetranslator.cpp b/src/onlinetranslator.cpp
index 1f1771d1..597e1e6e 100644
--- a/src/onlinetranslator.cpp
+++ b/src/onlinetranslator.cpp
@@ -495,6 +495,26 @@ void OnlineTranslator::setInstance(QString url)
     m_instance = qMove(url);
 }
 
+const QString &OnlineTranslator::apiKey()
+{
+    return m_apiKey;
+}
+
+void OnlineTranslator::setApiKey(QString apiKey)
+{
+    m_apiKey = qMove(apiKey);
+}
+
+bool OnlineTranslator::isDirect() const
+{
+    return m_direct;
+}
+
+void OnlineTranslator::setDirect(bool direct)
+{
+    m_direct = direct;
+}
+
 QString OnlineTranslator::languageName(Language lang)
 {
     switch (lang) {
@@ -1260,6 +1280,27 @@ void OnlineTranslator::requestTranslate()
 {
     const QString sourceText = sender()->property(s_textProperty).toString();
 
+    // LibreTranslate speaks its own REST API (POST /translate), not the Mozhi
+    // aggregator's GET /api/translate; this is what lets a self-hosted instance
+    // and its API key be used directly.
+    if (m_engine == LibreTranslate && m_direct) {
+        QUrl url(m_instance + QStringLiteral("/translate"));
+
+        QJsonObject body;
+        body.insert(QStringLiteral("q"), sourceText);
+        body.insert(QStringLiteral("source"), m_sourceLang == Auto ? QStringLiteral("auto") : languageApiCode(m_engine, m_sourceLang));
+        body.insert(QStringLiteral("target"), languageApiCode(m_engine, m_translationLang));
+        body.insert(QStringLiteral("format"), QStringLiteral("text"));
+        if (!m_apiKey.isEmpty())
+            body.insert(QStringLiteral("api_key"), m_apiKey);
+
+        QNetworkRequest request(url);
+        request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
+        request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
+        m_currentReply = m_networkManager->post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
+        return;
+    }
+
     // Generate API url
     QUrl url(m_instance + "/api/translate");
     url.setQuery(QStringLiteral("engine=%1&from=%2&to=%3&text=%4").arg(QString(QMetaEnum::fromType<OnlineTranslator::Engine>().valueToKey(m_engine)).toLower(), languageApiCode(m_engine, m_sourceLang), languageApiCode(m_engine, m_translationLang), QUrl::toPercentEncoding(sourceText)));
@@ -1287,6 +1328,25 @@ void OnlineTranslator::parseTranslate()
     m_jsonResponse = QJsonDocument::fromJson(m_currentReply->readAll());
     const QJsonObject jsonData = m_jsonResponse.object();
 
+    // LibreTranslate returns { "translatedText", "detectedLanguage": { "language" } }
+    // instead of Mozhi's { "translated-text", "detected" }.
+    if (m_engine == LibreTranslate && m_direct) {
+        if (m_sourceLang == Auto) {
+            const QString detectedCode = jsonData.value(QStringLiteral("detectedLanguage")).toObject().value(QStringLiteral("language")).toString();
+            m_sourceLang = language(m_engine, detectedCode);
+            if (m_sourceLang == NoLanguage) {
+                resetData(ParsingError, tr("Error: Unable to parse autodetected language"));
+                return;
+            }
+            if (m_onlyDetectLanguage)
+                return;
+        }
+
+        addSpaceBetweenParts(m_translation);
+        m_translation.append(jsonData.value(QStringLiteral("translatedText")).toString());
+        return;
+    }
+
     if (m_sourceLang == Auto) {
         // Parse language
         m_sourceLang = language(m_engine, jsonData.value(QStringLiteral("detected")).toString());
diff --git a/src/onlinetranslator.h b/src/onlinetranslator.h
index 9e65d02f..66e496f1 100644
--- a/src/onlinetranslator.h
+++ b/src/onlinetranslator.h
@@ -537,6 +537,36 @@ public:
      */
     void setInstance(QString url);
 
+    /**
+     * @brief Returns the API key used for the LibreTranslate engine.
+     *
+     * @return API key (empty for open instances that require none)
+     */
+    const QString &apiKey();
+
+    /**
+     * @brief Sets the API key used for the LibreTranslate engine.
+     *
+     * The key is sent as `api_key` to LibreTranslate instances (self-hosted
+     * and libretranslate.com require it; open instances ignore it).
+     *
+     * @param apiKey API key, or an empty string for instances that require none
+     */
+    void setApiKey(QString apiKey);
+
+    /**
+     * @brief Returns whether the LibreTranslate engine uses the direct
+     * LibreTranslate REST API rather than the Mozhi-shaped API.
+     */
+    bool isDirect() const;
+
+    /**
+     * @brief Sets the LibreTranslate API mode.
+     *
+     * @param direct true for the direct LibreTranslate API, false for Mozhi
+     */
+    void setDirect(bool direct);
+
     /**
      * @brief Language name
      *
@@ -646,6 +676,8 @@ private:
     QString m_errorString;
 
     QString m_instance;
+    QString m_apiKey;
+    bool m_direct = false;
 
     QVector<TranslationOptions> m_translationOptions;
     QVector<TranslationExample> m_examples;
diff --git a/src/provideroptionsmanager.cpp b/src/provideroptionsmanager.cpp
index c08f3153..20168c60 100644
--- a/src/provideroptionsmanager.cpp
+++ b/src/provideroptionsmanager.cpp
@@ -91,6 +91,8 @@ std::unique_ptr<ProviderOptions> ProviderOptionsManager::createMozhiTranslationO
 
     options->setOption("instance", settings.instance());
     options->setOption("engine", static_cast<int>(settings.currentEngine()));
+    options->setOption("apikey", settings.libreTranslateApiKey());
+    options->setOption("direct", settings.libreTranslateDirect());
 
     return options;
 }
diff --git a/src/settings/appsettings.cpp b/src/settings/appsettings.cpp
index db93165d..915ff182 100644
--- a/src/settings/appsettings.cpp
+++ b/src/settings/appsettings.cpp
@@ -633,6 +633,26 @@ void AppSettings::setInstance(const QString &url)
     m_settings->setValue(QStringLiteral("Translation/Instance"), url);
 }
 
+QString AppSettings::libreTranslateApiKey() const
+{
+    return m_settings->value(QStringLiteral("Translation/LibreTranslateApiKey")).toString();
+}
+
+void AppSettings::setLibreTranslateApiKey(const QString &apiKey)
+{
+    m_settings->setValue(QStringLiteral("Translation/LibreTranslateApiKey"), apiKey);
+}
+
+bool AppSettings::libreTranslateDirect() const
+{
+    return m_settings->value(QStringLiteral("Translation/LibreTranslateDirect")).toBool();
+}
+
+void AppSettings::setLibreTranslateDirect(bool direct)
+{
+    m_settings->setValue(QStringLiteral("Translation/LibreTranslateDirect"), direct);
+}
+
 // ── LocalAI backend ───────────────────────────────────────────
 
 QStringList AppSettings::localProviderIds()
diff --git a/src/settings/appsettings.h b/src/settings/appsettings.h
index 9163b236..f0a0cdde 100644
--- a/src/settings/appsettings.h
+++ b/src/settings/appsettings.h
@@ -189,6 +189,10 @@ public:
 
     QString instance() const;
     void setInstance(const QString &url);
+    QString libreTranslateApiKey() const;
+    void setLibreTranslateApiKey(const QString &apiKey);
+    bool libreTranslateDirect() const;
+    void setLibreTranslateDirect(bool direct);
 
     // LocalAI backend: local OpenAI-compatible servers (Ollama / FastFlowLM
     // / LM Studio), a custom OpenAI-compatible remote endpoint, or Anthropic.
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp
index 50238e35..0be92c80 100644
--- a/src/settings/settingsdialog.cpp
+++ b/src/settings/settingsdialog.cpp
@@ -278,6 +278,8 @@ void SettingsDialog::accept()
     settings.setForceTranslationAutodetect(ui->forceTranslationAutodetectCheckBox->isChecked());
     // Mozhi instance settings
     settings.setInstance(ui->mozhiUrlComboBox->currentText());
+    settings.setLibreTranslateApiKey(ui->mozhiApiKeyEdit->text());
+    settings.setLibreTranslateDirect(ui->mozhiDirectCheckBox->isChecked());
 
     // LocalAI settings
     saveLocalAiSettings();
@@ -1202,6 +1204,8 @@ void SettingsDialog::loadSettings()
     // Temporarily disconnect signal to avoid triggering provider updates while loading settings
     disconnect(ui->mozhiUrlComboBox, &QComboBox::currentTextChanged, this, &SettingsDialog::mozhiInstanceChanged);
     ui->mozhiUrlComboBox->setCurrentText(settings.instance());
+    ui->mozhiApiKeyEdit->setText(settings.libreTranslateApiKey());
+    ui->mozhiDirectCheckBox->setChecked(settings.libreTranslateDirect());
     connect(ui->mozhiUrlComboBox, &QComboBox::currentTextChanged, this, &SettingsDialog::mozhiInstanceChanged);
 
     // LocalAI
diff --git a/src/settings/settingsdialog.ui b/src/settings/settingsdialog.ui
index 859bafe9..47e376ed 100644
--- a/src/settings/settingsdialog.ui
+++ b/src/settings/settingsdialog.ui
@@ -893,42 +893,80 @@
               <property name="title">
                <string notr="true">Mozhi</string>
               </property>
-              <layout class="QHBoxLayout" name="horizontalLayout">
+              <layout class="QVBoxLayout" name="mozhiGroupBoxLayout">
                <item>
-                <widget class="QLabel" name="mozhiUrlLabel">
-                 <property name="text">
-                  <string notr="true">URL:</string>
-                 </property>
-                </widget>
-               </item>
-               <item>
-                <widget class="QComboBox" name="mozhiUrlComboBox">
-                 <property name="sizePolicy">
-                  <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
-                   <horstretch>0</horstretch>
-                   <verstretch>0</verstretch>
-                  </sizepolicy>
-                 </property>
-                 <property name="toolTip">
-                  <string>Mozhi instance URL</string>
-                 </property>
-                 <property name="editable">
-                  <bool>true</bool>
-                 </property>
-                 <property name="currentText">
-                  <string notr="true"/>
-                 </property>
-                </widget>
+                <layout class="QHBoxLayout" name="instanceRowLayout">
+                 <item>
+                  <widget class="QLabel" name="mozhiUrlLabel">
+                   <property name="text">
+                    <string notr="true">URL:</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QComboBox" name="mozhiUrlComboBox">
+                   <property name="sizePolicy">
+                    <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+                     <horstretch>0</horstretch>
+                     <verstretch>0</verstretch>
+                    </sizepolicy>
+                   </property>
+                   <property name="toolTip">
+                    <string>Mozhi instance URL</string>
+                   </property>
+                   <property name="editable">
+                    <bool>true</bool>
+                   </property>
+                   <property name="currentText">
+                    <string notr="true"/>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QPushButton" name="detectFastestButton">
+                   <property name="toolTip">
+                    <string>Automatically pick the fastest instance based on the ping</string>
+                   </property>
+                   <property name="text">
+                    <string>Detect fastest</string>
+                   </property>
+                  </widget>
+                 </item>
+                </layout>
                </item>
                <item>
-                <widget class="QPushButton" name="detectFastestButton">
-                 <property name="toolTip">
-                  <string>Automatically pick the fastest instance based on the ping</string>
-                 </property>
-                 <property name="text">
-                  <string>Detect fastest</string>
-                 </property>
-                </widget>
+                <layout class="QHBoxLayout" name="apiKeyRowLayout">
+                 <item>
+                  <widget class="QLabel" name="mozhiApiKeyLabel">
+                   <property name="text">
+                    <string notr="true">LibreTranslate API key:</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QLineEdit" name="mozhiApiKeyEdit">
+                   <property name="echoMode">
+                    <enum>QLineEdit::Password</enum>
+                   </property>
+                   <property name="toolTip">
+                    <string>Optional. Required by self-hosted LibreTranslate instances and libretranslate.com; the Mozhi front-end ignores it.</string>
+                   </property>
+                   <property name="placeholderText">
+                    <string notr="true">optional</string>
+                   </property>
+                  </widget>
+                 </item>
+                 <item>
+                  <widget class="QCheckBox" name="mozhiDirectCheckBox">
+                   <property name="text">
+                    <string notr="true">Use direct LibreTranslate API</string>
+                   </property>
+                   <property name="toolTip">
+                    <string>Talk the direct LibreTranslate REST API to the instance URL instead of the Mozhi-compatible API; use this for self-hosted LibreTranslate servers.</string>
+                   </property>
+                  </widget>
+                 </item>
+                </layout>
                </item>
               </layout>
              </widget>
diff --git a/src/translator/mozhitranslationprovider.cpp b/src/translator/mozhitranslationprovider.cpp
index 85388bf6..ca8c3b58 100644
--- a/src/translator/mozhitranslationprovider.cpp
+++ b/src/translator/mozhitranslationprovider.cpp
@@ -318,6 +318,14 @@ void MozhiTranslationProvider::applyOptions(const ProviderOptions &options)
             setEngine(static_cast<OnlineTranslator::Engine>(engineValue));
         }
     }
+
+    if (options.hasOption("apikey")) {
+        m_translator->setApiKey(options.getOption("apikey").toString());
+    }
+
+    if (options.hasOption("direct")) {
+        m_translator->setDirect(options.getOption("direct").toBool());
+    }
 }
 
 std::unique_ptr<ProviderOptions> MozhiTranslationProvider::getDefaultOptions() const
@@ -325,12 +333,14 @@ std::unique_ptr<ProviderOptions> MozhiTranslationProvider::getDefaultOptions() c
     auto options = std::make_unique<ProviderOptions>();
     options->setOption("instance", "https://mozhi.aryak.me");
     options->setOption("engine", static_cast<int>(OnlineTranslator::LibreTranslate));
+    options->setOption("apikey", QString());
+    options->setOption("direct", false);
     return options;
 }
 
 QStringList MozhiTranslationProvider::getAvailableOptions() const
 {
-    return {"instance", "engine"};
+    return {"instance", "engine", "apikey", "direct"};
 }
 
 ProviderUIRequirements MozhiTranslationProvider::getUIRequirements() const
@@ -349,6 +359,10 @@ void MozhiTranslationProvider::saveOptionToSettings(const QString &optionKey, co
         settings.setCurrentEngine(static_cast<OnlineTranslator::Engine>(value.toInt()));
     } else if (optionKey == "instance") {
         settings.setInstance(value.toString());
+    } else if (optionKey == "apikey") {
+        settings.setLibreTranslateApiKey(value.toString());
+    } else if (optionKey == "direct") {
+        settings.setLibreTranslateDirect(value.toBool());
     }
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.