[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());
}
}