Füge Schaltfläche zum erneuten Übersetzen hinzu und verbessere das Layout der Einstellungen

This commit is contained in:
Torsten Schulz (local)
2026-09-16 14:32:43 +02:00
parent 414c218839
commit 3a0f28bdd1
3 changed files with 139 additions and 26 deletions

View File

@@ -25,6 +25,7 @@
#include <QLocale>
#include <QMenu>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QScrollArea>
#include <QSet>
#include <QScrollBar>
@@ -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<QString> 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<OriginalWordReplacementEdit *> edits;
QVector<QPair<QWidget *, QTreeWidgetItem *>> 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<QString> 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);
}

View File

@@ -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);

View File

@@ -50,7 +50,7 @@
</layout>
</item>
<item>
<layout class="QFormLayout" name="textFormLayout">
<layout class="QGridLayout" name="textFormLayout">
<item row="0" column="0"><widget class="QLabel" name="originalTextLabel"><property name="text"><string>Originaltext</string></property></widget></item>
<item row="0" column="1"><widget class="QLineEdit" name="originalTextEdit"/></item>
<item row="0" column="2"><widget class="QPushButton" name="copyButton"><property name="text"><string>Kopieren</string></property></widget></item>
@@ -63,6 +63,7 @@
<item><widget class="QPushButton" name="setTranslationButton"><property name="text"><string>Übernehmen</string></property></widget></item>
<item><widget class="QPushButton" name="setTranslationAndJumpNextButton"><property name="text"><string>Übernehmen &amp;&amp; weiter</string></property></widget></item>
<item><widget class="QPushButton" name="setAndNextAndAutoTranslate"><property name="text"><string>Übernehmen, weiter &amp;&amp; automatisch übersetzen</string></property></widget></item>
<item><widget class="QPushButton" name="retryTranslationButton"><property name="toolTip"><string>Fordert für den aktuellen Originaltext einen neuen Übersetzungsvorschlag an.</string></property><property name="text"><string>Neu übersetzen</string></property></widget></item>
<item><spacer name="translationActionSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
</layout>
</item>
@@ -79,13 +80,38 @@
<item row="0" column="3"><widget class="QComboBox" name="deeplTranslateFrom"/></item>
<item row="0" column="4"><widget class="QLabel" name="label_5"><property name="text"><string>Nach</string></property></widget></item>
<item row="0" column="5"><widget class="QComboBox" name="deeplTranslateTo"/></item>
<item row="0" column="6"><widget class="QPushButton" name="autoTranslateButton"><property name="text"><string>Aktuellen Text übersetzen</string></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="label_9"><property name="text"><string>DeepL-API-Key</string></property></widget></item>
<item row="1" column="1" colspan="3"><widget class="QLineEdit" name="deeplApiKey"/></item>
<item row="1" column="4"><widget class="QPushButton" name="checkDeeplUsageButton"><property name="text"><string>Kontingent prüfen</string></property></widget></item>
<item row="1" column="5" colspan="2"><widget class="QPushButton" name="bulkAutoTranslateButton"><property name="text"><string>Alle offenen Texte übersetzen</string></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="libreTranslateUrlLabel"><property name="text"><string>Lokale URL</string></property></widget></item>
<item row="2" column="1" colspan="5"><widget class="QLineEdit" name="libreTranslateUrl"><property name="text"><string>http://localhost:5000</string></property></widget></item>
<item row="1" column="0" colspan="6">
<layout class="QHBoxLayout" name="automaticActionLayout">
<item><widget class="QPushButton" name="autoTranslateButton"><property name="text"><string>Aktuellen Text übersetzen</string></property></widget></item>
<item><widget class="QPushButton" name="bulkAutoTranslateButton"><property name="text"><string>Alle offenen Texte übersetzen</string></property></widget></item>
<item><spacer name="automaticActionSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
</layout>
</item>
<item row="2" column="0" colspan="6">
<widget class="QWidget" name="deeplSettingsWidget">
<layout class="QHBoxLayout" name="deeplSettingsLayout">
<property name="leftMargin"><number>0</number></property>
<property name="topMargin"><number>0</number></property>
<property name="rightMargin"><number>0</number></property>
<property name="bottomMargin"><number>0</number></property>
<item><widget class="QLabel" name="label_9"><property name="text"><string>DeepL-API-Key</string></property></widget></item>
<item><widget class="QLineEdit" name="deeplApiKey"/></item>
<item><widget class="QPushButton" name="checkDeeplUsageButton"><property name="text"><string>Kontingent prüfen</string></property></widget></item>
</layout>
</widget>
</item>
<item row="3" column="0" colspan="6">
<widget class="QWidget" name="libreSettingsWidget">
<layout class="QHBoxLayout" name="libreSettingsLayout">
<property name="leftMargin"><number>0</number></property>
<property name="topMargin"><number>0</number></property>
<property name="rightMargin"><number>0</number></property>
<property name="bottomMargin"><number>0</number></property>
<item><widget class="QLabel" name="libreTranslateUrlLabel"><property name="text"><string>Lokale URL</string></property></widget></item>
<item><widget class="QLineEdit" name="libreTranslateUrl"><property name="text"><string>http://localhost:5000</string></property></widget></item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>