Füge Unterstützung für LibreTranslate hinzu und verbessere die Benutzeroberfläche

This commit is contained in:
Torsten Schulz (local)
2026-09-16 12:01:39 +02:00
parent 701720f029
commit 414c218839
4 changed files with 354 additions and 426 deletions

View File

@@ -22,6 +22,7 @@
#include <QDialogButtonBox>
#include <QFrame>
#include <QLabel>
#include <QLocale>
#include <QMenu>
#include <QPlainTextEdit>
#include <QScrollArea>
@@ -154,6 +155,13 @@ MainWindow::MainWindow(QWidget *parent)
ui->deeplApiKey->setText(configuration["deepl-key"].toString());
loadDeeplTranslationPossibilities();
}
if (configuration.contains("libretranslate-url")) {
ui->libreTranslateUrl->setText(configuration["libretranslate-url"].toString());
}
if (configuration.contains("translation-provider")) {
ui->translationProviderCombo->setCurrentText(configuration["translation-provider"].toString());
}
on_translationProviderCombo_currentIndexChanged(ui->translationProviderCombo->currentIndex());
if (configuration.contains("last-dir") && (QDir()).exists(configuration["last-dir"].toString())) {
ui->projectDir->setText(configuration["last-dir"].toString());
crawlProject();
@@ -164,6 +172,67 @@ MainWindow::MainWindow(QWidget *parent)
noConfigChange = false;
}
bool MainWindow::isLibreTranslateProvider() const
{
return ui->translationProviderCombo->currentIndex() == 1;
}
QString MainWindow::libreTranslateUrl(const QString &path) const
{
return ui->libreTranslateUrl->text().trimmed().replace(QRegularExpression("/+$/"), "") + path;
}
void MainWindow::on_translationProviderCombo_currentIndexChanged(int)
{
const bool libreTranslate = isLibreTranslateProvider();
ui->label_9->setVisible(!libreTranslate);
ui->deeplApiKey->setVisible(!libreTranslate);
ui->checkDeeplUsageButton->setVisible(!libreTranslate);
ui->libreTranslateUrlLabel->setVisible(libreTranslate);
ui->libreTranslateUrl->setVisible(libreTranslate);
setConfigValue("translation-provider", ui->translationProviderCombo->currentText());
if (libreTranslate) {
loadLibreTranslateLanguages();
} else {
translationsMap.clear();
loadDeeplTranslationPossibilities();
}
}
void MainWindow::on_libreTranslateUrl_editingFinished()
{
setConfigValue("libretranslate-url", ui->libreTranslateUrl->text().trimmed());
if (isLibreTranslateProvider()) {
loadLibreTranslateLanguages();
}
}
void MainWindow::loadLibreTranslateLanguages()
{
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
connect(manager, &QNetworkAccessManager::finished, this, [this, manager](QNetworkReply *reply) {
if (!isLibreTranslateProvider()) {
// The provider changed while this request was in flight.
} else if (reply->error() != QNetworkReply::NoError) {
ui->statusbar->showMessage(tr("LibreTranslate nicht erreichbar: %1").arg(reply->errorString()), 6000);
} else {
translationsMap.clear();
const QJsonArray languages = QJsonDocument::fromJson(reply->readAll()).array();
for (const QJsonValue &value : languages) {
const QJsonObject language = value.toObject();
const QString source = language.value("code").toString();
for (const QJsonValue &target : language.value("targets").toArray()) {
translationsMap[source].push_back(target.toString());
}
}
renderDeeplSources();
}
reply->deleteLater();
manager->deleteLater();
});
manager->get(QNetworkRequest(QUrl(libreTranslateUrl("/languages"))));
}
MainWindow::~MainWindow()
{
delete ui;
@@ -559,6 +628,9 @@ void MainWindow::renderDeeplSources()
}
void MainWindow::onDeeplTranslationPossibilitiesLoaded(QNetworkReply *reply) {
if (isLibreTranslateProvider()) {
return;
}
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
@@ -638,7 +710,8 @@ void MainWindow::on_deeplTranslateFrom_currentTextChanged(const QString &sourceL
}
void MainWindow::on_autoTranslateButton_clicked() {
if (ui->deeplApiKey->text().isEmpty()) {
const bool libreTranslate = isLibreTranslateProvider();
if (!libreTranslate && ui->deeplApiKey->text().isEmpty()) {
QMessageBox::information(this, "Deepl error", "For auto translation, you need to add a deepl API key.");
return;
}
@@ -648,21 +721,29 @@ void MainWindow::on_autoTranslateButton_clicked() {
QJsonObject jsonData;
// Protect Mustache-/Platzhalter vor DeepL-Übersetzung
QString protectedText = protectPlaceholdersForDeepl(originalText);
jsonData["text"] = QJsonArray::fromStringList({protectedText});
jsonData["source_lang"] = sourceLang;
jsonData["target_lang"] = targetLang;
jsonData["formality"] = "less";
// Use XML tag handling so our <x id="N"/> placeholders are preserved
jsonData["tag_handling"] = "xml";
jsonData["tag_handling_version"] = "v2";
QUrl url("https://api-free.deepl.com/v2/translate");
if (libreTranslate) {
jsonData["q"] = protectedText;
jsonData["source"] = sourceLang;
jsonData["target"] = targetLang;
jsonData["format"] = "text";
} else {
jsonData["text"] = QJsonArray::fromStringList({protectedText});
jsonData["source_lang"] = sourceLang;
jsonData["target_lang"] = targetLang;
jsonData["formality"] = "less";
jsonData["tag_handling"] = "xml";
jsonData["tag_handling_version"] = "v2";
}
QUrl url(libreTranslate ? libreTranslateUrl("/translate") : QStringLiteral("https://api-free.deepl.com/v2/translate"));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "DeepL-Auth-Key " + ui->deeplApiKey->text().toUtf8());
if (!libreTranslate) {
request.setRawHeader("Authorization", "DeepL-Auth-Key " + ui->deeplApiKey->text().toUtf8());
}
static int attempt = 1;
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
connect(manager, &QNetworkAccessManager::finished, this,
[this, manager, jsonData, request](QNetworkReply *reply) mutable {
[this, manager, jsonData, request, libreTranslate](QNetworkReply *reply) mutable {
if (reply->error() != QNetworkReply::NoError) {
qDebug() << "Translation request failed:" << reply->errorString();
qDebug() << "Attempt:" << attempt;
@@ -675,7 +756,7 @@ void MainWindow::on_autoTranslateButton_clicked() {
}
} else {
attempt = 1;
translationRequestFinished(reply);
translationRequestFinished(reply, libreTranslate);
}
reply->deleteLater();
manager->deleteLater();
@@ -686,7 +767,8 @@ void MainWindow::on_autoTranslateButton_clicked() {
void MainWindow::on_bulkAutoTranslateButton_clicked()
{
if (ui->deeplApiKey->text().isEmpty()) {
bulkUsesLibreTranslate = isLibreTranslateProvider();
if (!bulkUsesLibreTranslate && ui->deeplApiKey->text().isEmpty()) {
QMessageBox::information(this, tr("DeepL error"), tr("Für die automatische Übersetzung wird ein DeepL-API-Key benötigt."));
return;
}
@@ -718,6 +800,54 @@ void MainWindow::on_bulkAutoTranslateButton_clicked()
requestNextBulkTranslationBatch();
}
void MainWindow::on_checkDeeplUsageButton_clicked()
{
if (ui->deeplApiKey->text().isEmpty()) {
QMessageBox::information(this, tr("DeepL error"), tr("Bitte hinterlege zuerst einen DeepL-API-Key."));
return;
}
ui->checkDeeplUsageButton->setEnabled(false);
ui->statusbar->showMessage(tr("DeepL-Kontingent wird geprüft …"));
QNetworkRequest request(QUrl("https://api-free.deepl.com/v2/usage"));
request.setRawHeader("Authorization", "DeepL-Auth-Key " + ui->deeplApiKey->text().toUtf8());
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
connect(manager, &QNetworkAccessManager::finished, this, [this, manager](QNetworkReply *reply) {
ui->checkDeeplUsageButton->setEnabled(true);
ui->statusbar->clearMessage();
const QByteArray responseBody = reply->readAll();
if (reply->error() != QNetworkReply::NoError) {
QString details;
const QJsonDocument response = QJsonDocument::fromJson(responseBody);
if (response.isObject()) {
details = response.object().value("message").toString();
}
QMessageBox::critical(this, tr("DeepL error"),
tr("Das DeepL-Kontingent konnte nicht abgefragt werden: %1")
.arg(details.isEmpty() ? reply->errorString() : details));
} else {
const QJsonObject usage = QJsonDocument::fromJson(responseBody).object();
const qint64 usedCharacters = usage.value("character_count").toVariant().toLongLong();
const qint64 characterLimit = usage.value("character_limit").toVariant().toLongLong();
if (characterLimit <= 0) {
QMessageBox::critical(this, tr("DeepL error"), tr("DeepL hat keine gültigen Kontingentdaten geliefert."));
} else {
const qint64 remainingCharacters = characterLimit - usedCharacters;
const double usedPercentage = 100.0 * usedCharacters / characterLimit;
QMessageBox::information(this, tr("DeepL-Kontingent"),
tr("Verbraucht: %1 von %2 Zeichen (%3 %).\nVerfügbar: %4 Zeichen.")
.arg(QLocale().toString(usedCharacters),
QLocale().toString(characterLimit),
QLocale().toString(usedPercentage, 'f', 1),
QLocale().toString(remainingCharacters)));
}
}
reply->deleteLater();
manager->deleteLater();
});
manager->get(request);
}
void MainWindow::requestNextBulkTranslationBatch()
{
if (nextBulkTranslationIndex >= bulkTranslations.size()) {
@@ -744,16 +874,25 @@ void MainWindow::requestNextBulkTranslationBatch()
}
QJsonObject payload;
payload["text"] = QJsonArray::fromStringList(texts);
payload["source_lang"] = ui->deeplTranslateFrom->currentText();
payload["target_lang"] = ui->deeplTranslateTo->currentText();
payload["formality"] = "less";
payload["tag_handling"] = "xml";
payload["tag_handling_version"] = "v2";
if (bulkUsesLibreTranslate) {
payload["q"] = QJsonArray::fromStringList(texts);
payload["source"] = ui->deeplTranslateFrom->currentText();
payload["target"] = ui->deeplTranslateTo->currentText();
payload["format"] = "text";
} else {
payload["text"] = QJsonArray::fromStringList(texts);
payload["source_lang"] = ui->deeplTranslateFrom->currentText();
payload["target_lang"] = ui->deeplTranslateTo->currentText();
payload["formality"] = "less";
payload["tag_handling"] = "xml";
payload["tag_handling_version"] = "v2";
}
QNetworkRequest request(QUrl("https://api-free.deepl.com/v2/translate"));
QNetworkRequest request(QUrl(bulkUsesLibreTranslate ? libreTranslateUrl("/translate") : QStringLiteral("https://api-free.deepl.com/v2/translate")));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "DeepL-Auth-Key " + ui->deeplApiKey->text().toUtf8());
if (!bulkUsesLibreTranslate) {
request.setRawHeader("Authorization", "DeepL-Auth-Key " + ui->deeplApiKey->text().toUtf8());
}
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
connect(manager, &QNetworkAccessManager::finished, this,
[this, manager, firstItem, itemCount = texts.size()](QNetworkReply *reply) {
@@ -769,13 +908,21 @@ void MainWindow::handleBulkTranslationReply(QNetworkReply *reply, int firstItem,
if (reply->error() != QNetworkReply::NoError) {
ui->bulkAutoTranslateButton->setEnabled(true);
ui->statusbar->clearMessage();
QMessageBox::critical(this, tr("DeepL error"), tr("Die Sammelübersetzung ist fehlgeschlagen: %1").arg(reply->errorString()));
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
const QString errorMessage = httpStatus == 456
? tr("Das DeepL-Zeichenkontingent für diesen API-Key ist ausgeschöpft. "
"Bei API Free wird es im nächsten Abrechnungszeitraum zurückgesetzt; bei API Pro prüfe bitte das Cost-Control-Limit.")
: tr("Die Sammelübersetzung ist fehlgeschlagen: %1").arg(reply->errorString());
QMessageBox::critical(this, tr("DeepL error"), errorMessage);
bulkTranslations.clear();
return;
}
const QJsonDocument response = QJsonDocument::fromJson(reply->readAll());
const QJsonArray translations = response.object().value("translations").toArray();
const QJsonObject responseObject = response.object();
const QJsonArray translations = bulkUsesLibreTranslate
? responseObject.value("translatedText").toArray()
: responseObject.value("translations").toArray();
if (translations.size() != itemCount) {
ui->bulkAutoTranslateButton->setEnabled(true);
ui->statusbar->clearMessage();
@@ -785,7 +932,9 @@ void MainWindow::handleBulkTranslationReply(QNetworkReply *reply, int firstItem,
}
for (int index = 0; index < translations.size(); ++index) {
QString translatedText = translations.at(index).toObject().value("text").toString();
QString translatedText = bulkUsesLibreTranslate
? translations.at(index).toString()
: translations.at(index).toObject().value("text").toString();
BulkTranslationItem &item = bulkTranslations[firstItem + index];
item.translatedText = restoreBulkPlaceholders(translatedText, item.placeholders);
item.translatedText.replace('"', '\'');
@@ -915,16 +1064,17 @@ void MainWindow::showBulkTranslationDialog()
bulkTranslations.clear();
}
void MainWindow::translationRequestFinished(QNetworkReply *reply) {
void MainWindow::translationRequestFinished(QNetworkReply *reply, bool isLibreTranslate) {
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
if (!jsonResponse.isNull() && jsonResponse.isObject()) {
QJsonObject jsonObject = jsonResponse.object();
QJsonArray translationsArray = jsonObject["translations"].toArray();
if (!translationsArray.isEmpty()) {
QJsonObject translationObject = translationsArray[0].toObject();
QString translatedText = translationObject["text"].toString();
if (isLibreTranslate || !translationsArray.isEmpty()) {
QString translatedText = isLibreTranslate
? jsonObject["translatedText"].toString()
: translationsArray[0].toObject()["text"].toString();
// Restore any placeholders that were protected before the request
QString restored = restorePlaceholdersFromDeepl(translatedText);
restored.replace("\"", "'");