From 3a0f28bdd1965090bdc0a987d3b868c04f79c106 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 16 Sep 2026 14:32:43 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCge=20Schaltfl=C3=A4che=20zum=20erneuten?= =?UTF-8?q?=20=C3=9Cbersetzen=20hinzu=20und=20verbessere=20das=20Layout=20?= =?UTF-8?q?der=20Einstellungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mainwindow.cpp | 122 +++++++++++++++++++++++++++++++++++++++++-------- mainwindow.h | 1 + mainwindow.ui | 42 +++++++++++++---- 3 files changed, 139 insertions(+), 26 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index e96add0..c662c69 100755 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -45,15 +46,18 @@ public: protected: void contextMenuEvent(QContextMenuEvent *event) override { - QTextCursor wordCursor = cursorForPosition(event->pos()); - wordCursor.select(QTextCursor::WordUnderCursor); - if (!wordCursor.hasSelection()) { + QTextCursor replacementCursor = textCursor(); + if (!replacementCursor.hasSelection()) { + replacementCursor = cursorForPosition(event->pos()); + replacementCursor.select(QTextCursor::WordUnderCursor); + } + if (!replacementCursor.hasSelection()) { QPlainTextEdit::contextMenuEvent(event); return; } - const int wordStart = wordCursor.selectionStart(); - const int wordEnd = wordCursor.selectionEnd(); + const int wordStart = replacementCursor.selectionStart(); + const int wordEnd = replacementCursor.selectionEnd(); QMenu *menu = createStandardContextMenu(); QMenu *replacementMenu = menu->addMenu(tr("Durch Wort aus Original ersetzen")); QSet seenWords; @@ -185,11 +189,8 @@ QString MainWindow::libreTranslateUrl(const QString &path) const 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); + ui->deeplSettingsWidget->setVisible(!libreTranslate); + ui->libreSettingsWidget->setVisible(libreTranslate); setConfigValue("translation-provider", ui->translationProviderCombo->currentText()); if (libreTranslate) { loadLibreTranslateLanguages(); @@ -306,6 +307,8 @@ void MainWindow::populateTreeWidgetFromMap() { QString fileName = QFileInfo(pair.first).fileName(); fileItem->setText(0, fileName); fileItem->setData(0, Qt::UserRole, false); + // Keep the full path: identical file names can exist in different subdirectories. + fileItem->setData(0, Qt::UserRole + 1, pair.first); auto parsedBlocks = parseTextBlock(pair.second); for (const auto &block : std::as_const(parsedBlocks)) { QTreeWidgetItem *blockItem = new QTreeWidgetItem(fileItem); @@ -493,9 +496,11 @@ bool MainWindow::saveFile(QTreeWidgetItem *fileItem, QString &errorMessage) { const QString projectDir = ui->projectDir->text(); const QString fileName = fileItem->text(0); - const QString filePath = QString("%1/game/tl/%2/%3").arg(projectDir, ui->languageCombo->currentText(), fileName); - const QString backupFileName = QString("%1/game/tl/%2/%3.bak.%4") - .arg(projectDir, ui->languageCombo->currentText(), fileName, QDateTime::currentDateTime().toString("yyyyMMddHHmmss")); + const QString filePath = fileItem->data(0, Qt::UserRole + 1).toString().isEmpty() + ? QString("%1/game/tl/%2/%3").arg(projectDir, ui->languageCombo->currentText(), fileName) + : fileItem->data(0, Qt::UserRole + 1).toString(); + const QString backupFileName = QString("%1.bak.%2") + .arg(filePath, QDateTime::currentDateTime().toString("yyyyMMddHHmmss")); if (!createBackup(filePath, backupFileName)) { errorMessage = filePath + ": Failed to create backup"; return false; @@ -543,7 +548,7 @@ bool MainWindow::editAndSaveFile(QTreeWidgetItem *fileItem, const QString &backu bool MainWindow::parseAndEditFile(QFile &backupFile, QTreeWidgetItem *fileItem, QStringList &lines, bool &changed) { QTextStream backupStream(&backupFile); QString content = backupStream.readAll(); - static QRegularExpression translateRegex(R"(# game\/(.+):(\d+)\ntranslate german (.*?)\n\n\s*#.*?\"(.*?)\"\s*\n\s*(.*?)\s*\"(.*?)\")"); + static QRegularExpression translateRegex(R"(# game\/(.+):(\d+)\ntranslate\s+[^\s]+\s+(.*?)\n\n\s*#.*?\"(.*?)\"\s*\n\s*(.*?)\s*\"(.*?)\")"); static QRegularExpression oldNewRegex(R"(\n\s*#\s*game\/(.+?):(\d+?)\n\s*old\s*\"(.+?)\"\n\s*new\s*\"(.*?)\")"); QRegularExpressionMatch match; int offset = 0; @@ -625,6 +630,9 @@ void MainWindow::renderDeeplSources() for (const auto& pair : translationsMap) { ui->deeplTranslateFrom->addItem(pair.first); } + if (isLibreTranslateProvider() && ui->deeplTranslateFrom->findText("en") >= 0) { + ui->deeplTranslateFrom->setCurrentText("en"); + } } void MainWindow::onDeeplTranslationPossibilitiesLoaded(QNetworkReply *reply) { @@ -706,6 +714,9 @@ void MainWindow::on_deeplTranslateFrom_currentTextChanged(const QString &sourceL targetLang.replace("\"", "'"); ui->deeplTranslateTo->addItem(targetLang); } + if (isLibreTranslateProvider() && ui->deeplTranslateTo->findText("de") >= 0) { + ui->deeplTranslateTo->setCurrentText("de"); + } } } @@ -765,6 +776,15 @@ void MainWindow::on_autoTranslateButton_clicked() { manager->post(request, QJsonDocument(jsonData).toJson()); } +void MainWindow::on_retryTranslationButton_clicked() +{ + if (ui->originalTextEdit->text().trimmed().isEmpty()) { + QMessageBox::information(this, tr("Information"), tr("Bitte wähle zuerst einen Übersetzungseintrag aus.")); + return; + } + on_autoTranslateButton_clicked(); +} + void MainWindow::on_bulkAutoTranslateButton_clicked() { bulkUsesLibreTranslate = isLibreTranslateProvider(); @@ -938,6 +958,11 @@ void MainWindow::handleBulkTranslationReply(QNetworkReply *reply, int firstItem, BulkTranslationItem &item = bulkTranslations[firstItem + index]; item.translatedText = restoreBulkPlaceholders(translatedText, item.placeholders); item.translatedText.replace('"', '\''); + // An unchanged response normally indicates that the selected local model + // cannot translate this language pair. Keep it open for a later retry. + if (item.translatedText.trimmed() == item.originalText.trimmed()) { + item.translatedText.clear(); + } } ui->statusbar->showMessage(tr("Übersetzt: %1 von %2 …").arg(nextBulkTranslationIndex).arg(bulkTranslations.size())); requestNextBulkTranslationBatch(); @@ -960,10 +985,14 @@ void MainWindow::showBulkTranslationDialog() QVBoxLayout *contentLayout = new QVBoxLayout(content); QVector edits; QVector> fileSections; + const bool dialogUsesLibreTranslate = bulkUsesLibreTranslate; + const QString dialogSourceLanguage = ui->deeplTranslateFrom->currentText(); + const QString dialogTargetLanguage = ui->deeplTranslateTo->currentText(); edits.reserve(bulkTranslations.size()); QTreeWidgetItem *currentFile = nullptr; QVBoxLayout *fileLayout = nullptr; - for (const BulkTranslationItem &item : std::as_const(bulkTranslations)) { + for (int bulkIndex = 0; bulkIndex < bulkTranslations.size(); ++bulkIndex) { + BulkTranslationItem &item = bulkTranslations[bulkIndex]; QTreeWidgetItem *fileItem = item.item->parent(); if (fileItem != currentFile) { if (currentFile) { @@ -984,15 +1013,72 @@ void MainWindow::showBulkTranslationDialog() fileSections.append(qMakePair(fileSection, fileItem)); } - QLabel *originalLabel = new QLabel(item.originalText, content); + auto *originalRow = new QWidget(content); + auto *originalRowLayout = new QHBoxLayout(originalRow); + originalRowLayout->setContentsMargins(0, 0, 0, 0); + QLabel *originalLabel = new QLabel(item.originalText, originalRow); originalLabel->setWordWrap(true); originalLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); originalLabel->setStyleSheet(QStringLiteral("font-weight: 600; padding-top: 6px;")); - fileLayout->addWidget(originalLabel); + originalRowLayout->addWidget(originalLabel, 1); auto *translationEdit = new OriginalWordReplacementEdit(item.originalText, content); translationEdit->setPlainText(item.translatedText); - translationEdit->setPlaceholderText(tr("Übersetzung")); + translationEdit->setPlaceholderText(tr("Keine Übersetzung erhalten – später erneut übersetzen oder manuell eintragen.")); + auto *retryButton = new QPushButton(tr("Neu übersetzen"), originalRow); + retryButton->setToolTip(tr("Fordert für diesen einzelnen Text einen neuen Vorschlag an.")); + originalRowLayout->addWidget(retryButton); + connect(retryButton, &QPushButton::clicked, &dialog, + [this, retryButton, translationEdit, bulkIndex, dialogUsesLibreTranslate, dialogSourceLanguage, dialogTargetLanguage, &dialog] { + BulkTranslationItem &retryItem = bulkTranslations[bulkIndex]; + QVector placeholders; + const QString protectedText = protectBulkPlaceholders(retryItem.originalText, placeholders); + retryButton->setEnabled(false); + + QJsonObject payload; + if (dialogUsesLibreTranslate) { + payload["q"] = protectedText; + payload["source"] = dialogSourceLanguage; + payload["target"] = dialogTargetLanguage; + payload["format"] = "text"; + } else { + payload["text"] = QJsonArray::fromStringList({protectedText}); + payload["source_lang"] = dialogSourceLanguage; + payload["target_lang"] = dialogTargetLanguage; + payload["formality"] = "less"; + payload["tag_handling"] = "xml"; + payload["tag_handling_version"] = "v2"; + } + QNetworkRequest request(QUrl(dialogUsesLibreTranslate + ? libreTranslateUrl("/translate") + : QStringLiteral("https://api-free.deepl.com/v2/translate"))); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + if (!dialogUsesLibreTranslate) { + request.setRawHeader("Authorization", "DeepL-Auth-Key " + ui->deeplApiKey->text().toUtf8()); + } + auto *manager = new QNetworkAccessManager(&dialog); + connect(manager, &QNetworkAccessManager::finished, &dialog, + [this, manager, retryButton, translationEdit, bulkIndex, placeholders, dialogUsesLibreTranslate](QNetworkReply *reply) { + retryButton->setEnabled(true); + if (reply->error() != QNetworkReply::NoError) { + QMessageBox::warning(this, tr("Übersetzung fehlgeschlagen"), reply->errorString()); + } else { + const QJsonObject response = QJsonDocument::fromJson(reply->readAll()).object(); + const QJsonArray translations = response.value("translations").toArray(); + QString translatedText = dialogUsesLibreTranslate + ? response.value("translatedText").toString() + : (!translations.isEmpty() ? translations.at(0).toObject().value("text").toString() : QString()); + translatedText = restoreBulkPlaceholders(translatedText, placeholders); + translatedText.replace('"', '\''); + bulkTranslations[bulkIndex].translatedText = translatedText; + translationEdit->setPlainText(translatedText); + } + reply->deleteLater(); + manager->deleteLater(); + }); + manager->post(request, QJsonDocument(payload).toJson()); + }); + fileLayout->addWidget(originalRow); fileLayout->addWidget(translationEdit); edits.append(translationEdit); } diff --git a/mainwindow.h b/mainwindow.h index 51fbe8b..5fb43d3 100755 --- a/mainwindow.h +++ b/mainwindow.h @@ -41,6 +41,7 @@ private slots: void onDeeplTranslationPossibilitiesLoaded(QNetworkReply *reply); void on_deeplTranslateFrom_currentTextChanged(const QString &sourceLanguage); void on_autoTranslateButton_clicked(); + void on_retryTranslationButton_clicked(); void on_bulkAutoTranslateButton_clicked(); void on_checkDeeplUsageButton_clicked(); void on_translationProviderCombo_currentIndexChanged(int index); diff --git a/mainwindow.ui b/mainwindow.ui index 69a2922..8a8f24d 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -50,7 +50,7 @@ - + Originaltext Kopieren @@ -63,6 +63,7 @@ Übernehmen Übernehmen && weiter Übernehmen, weiter && automatisch übersetzen + Fordert für den aktuellen Originaltext einen neuen Übersetzungsvorschlag an.Neu übersetzen Qt::Horizontal4020 @@ -79,13 +80,38 @@ Nach - Aktuellen Text übersetzen - DeepL-API-Key - - Kontingent prüfen - Alle offenen Texte übersetzen - Lokale URL - http://localhost:5000 + + + Aktuellen Text übersetzen + Alle offenen Texte übersetzen + Qt::Horizontal4020 + + + + + + 0 + 0 + 0 + 0 + DeepL-API-Key + + Kontingent prüfen + + + + + + + 0 + 0 + 0 + 0 + Lokale URL + http://localhost:5000 + + +