Füge Unterstützung für Regressionstests hinzu und erweitere die Benutzeroberfläche mit neuen Schaltflächen zur Genehmigung identischer Übersetzungen und zur Wiederverwendung vorhandener Übersetzungen.

This commit is contained in:
Torsten Schulz (local)
2026-10-29 09:48:07 +01:00
parent d1c68d78f1
commit 05d62aad5f
5 changed files with 508 additions and 58 deletions

View File

@@ -6,6 +6,7 @@
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QSaveFile>
#include <QTextStream>
#include <QDebug>
#include <QDateTime>
@@ -16,6 +17,8 @@
#include <QJsonObject>
#include <QDialogButtonBox>
#include <QInputDialog>
#include <QHeaderView>
#include <QMap>
#include <QClipboard>
#include <QCheckBox>
#include <QCryptographicHash>
@@ -36,9 +39,83 @@
#include <QSignalBlocker>
#include <QScrollBar>
#include <QVBoxLayout>
#include <QWheelEvent>
namespace {
QString escapeTranslationForRenpy(const QString &text)
{
// The editor may contain both literal quotes and already escaped Ren'Py
// text. Preserve existing escapes and make every output string valid.
QString escaped;
int backslashes = 0;
for (const QChar character : text) {
if (character == '\\') {
escaped += character;
++backslashes;
continue;
}
if (character == '\n' || character == '\r' || character == '\t') {
if (backslashes % 2 != 0) escaped += '\\';
escaped += character == '\n' ? QStringLiteral("\\n")
: character == '\r' ? QStringLiteral("\\r") : QStringLiteral("\\t");
} else {
if (character == '"' && backslashes % 2 == 0) escaped += '\\';
escaped += character;
}
backslashes = 0;
}
if (backslashes % 2 != 0) escaped += '\\';
return escaped;
}
class TranslationChoiceComboBox final : public QComboBox
{
public:
explicit TranslationChoiceComboBox(QWidget *parent) : QComboBox(parent)
{
setFocusPolicy(Qt::StrongFocus);
}
protected:
void wheelEvent(QWheelEvent *event) override
{
// Scroll the preview instead of silently changing a translation choice.
if (view()->isVisible()) {
QComboBox::wheelEvent(event);
} else {
for (QWidget *parent = parentWidget(); parent; parent = parent->parentWidget()) {
if (auto *area = qobject_cast<QAbstractScrollArea *>(parent)) {
QWheelEvent forwarded(area->viewport()->mapFromGlobal(event->globalPosition().toPoint()),
event->globalPosition(), event->pixelDelta(), event->angleDelta(),
event->buttons(), event->modifiers(), event->phase(),
event->inverted(), event->source(), event->pointingDevice());
QCoreApplication::sendEvent(area->viewport(), &forwarded);
event->setAccepted(forwarded.isAccepted());
return;
}
}
event->ignore();
}
}
};
bool containsOnlyPlaceholders(const QString &text)
{
// Ren'Py interpolation and positional placeholders may be separated by
// punctuation, e.g. "[skill]: [desc]". Escaped "[[" is literal text.
static const QRegularExpression placeholders(
QStringLiteral("(?<!\\[)\\[(?!\\[)[^\\[\\]\\r\\n]+\\]|\\{[0-9]+\\}"));
static const QRegularExpression separatorsOnly(
QStringLiteral("\\A[\\s\\p{P}\\p{S}]*\\z"));
if (!placeholders.match(text).hasMatch()) {
return false;
}
QString remainder = text;
remainder.remove(placeholders);
return separatorsOnly.match(remainder).hasMatch();
}
class OriginalWordReplacementEdit final : public QPlainTextEdit
{
public:
@@ -383,12 +460,10 @@ QVector<MainWindow::TranslationItem> MainWindow::parseTextBlock(const QString& i
int lineNumber = 0;
QString identifier, originalText, translatedText;
for (const QString& line : std::as_const(lines)) {
if (line.contains("# game/")) {
QRegularExpression regExp("# game/.+:(\\d+)");
QRegularExpressionMatch match = regExp.match(line.trimmed());
if (match.hasMatch()) {
lineNumber = match.captured(1).toInt();
}
static const QRegularExpression sourceLocation(QStringLiteral("^#\\s+.+:(\\d+)\\s*$"));
const auto sourceMatch = sourceLocation.match(line.trimmed());
if (sourceMatch.hasMatch()) {
lineNumber = sourceMatch.captured(1).toInt();
} else if (line.startsWith(" # ") || line.startsWith(" old ")) {
const int firstQuote = line.indexOf('"');
const int lastQuote = line.lastIndexOf('"');
@@ -451,19 +526,28 @@ void MainWindow::on_reloadFilesButton_clicked()
}
void MainWindow::on_nextUntranslatedButton_clicked() {
QVector<QTreeWidgetItem *> items;
int currentIndex = -1;
for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) {
QTreeWidgetItem *topLevelItem = ui->treeWidget->topLevelItem(i);
for (int j = 0; j < topLevelItem->childCount(); ++j) {
QTreeWidgetItem *childItem = topLevelItem->child(j);
// Prüfe, ob Übersetzung (Spalte 3) leer ist
if (isOpenTranslation(childItem)) {
ui->originalTextEdit->setText(childItem->text(2)); // Originaltext aus Spalte 2
ui->translationEdit->clear();
ui->treeWidget->scrollToItem(childItem);
ui->treeWidget->setCurrentItem(childItem);
on_copyButton_clicked();
return;
if (childItem == ui->treeWidget->currentItem()) {
currentIndex = items.size();
}
items.append(childItem);
}
}
// Continue after the selected row and wrap around once at the end.
for (int offset = 1; offset <= items.size(); ++offset) {
QTreeWidgetItem *item = items.at((currentIndex + offset) % items.size());
if (isOpenTranslation(item)) {
ui->treeWidget->setCurrentItem(item);
ui->originalTextEdit->setText(item->text(2));
ui->translationEdit->setText(item->text(3));
ui->treeWidget->scrollToItem(item);
on_copyButton_clicked();
return;
}
}
QMessageBox::information(this, tr("Information"), tr("No untranslated item found."));
@@ -480,11 +564,25 @@ void MainWindow::on_treeWidget_itemSelectionChanged() {
}
void MainWindow::on_setTranslationButton_clicked() {
QJsonObject approvals = configuration.value("approved-identical-translations").toObject();
bool approvalsChanged = false;
const auto applyTranslation = [&](QTreeWidgetItem *item) {
const QString translation = ui->translationEdit->text();
item->setText(3, translation);
// Explicitly accepting an unchanged word (e.g. "Million") confirms it.
if (!translation.trimmed().isEmpty() && translation.trimmed() == item->text(2).trimmed()) {
const QString key = identicalTranslationApprovalKey(item);
if (!approvals.value(key).toBool()) {
approvals[key] = true;
approvalsChanged = true;
}
}
};
QTreeWidgetItem *selectedItem = ui->treeWidget->currentItem();
if (selectedItem) {
if (selectedItem->treeWidget()->indexOfTopLevelItem(selectedItem) < 0) {
// Setze Übersetzung in Spalte 3
selectedItem->setText(3, ui->translationEdit->text());
applyTranslation(selectedItem);
QTreeWidgetItem *parentItem = selectedItem->parent();
if (parentItem) {
parentItem->setData(0, Qt::UserRole, true);
@@ -492,12 +590,16 @@ void MainWindow::on_setTranslationButton_clicked() {
auto item = parentItem->child(index);
// Vergleiche den Originaltext (Spalte 2)
if (item->text(2) == ui->originalTextEdit->text()) {
item->setText(3, ui->translationEdit->text());
applyTranslation(item);
}
}
}
}
}
if (approvalsChanged) {
configuration["approved-identical-translations"] = approvals;
saveConfiguration();
}
ui->originalTextEdit->setFocus();
countAndShowUntranslated();
}
@@ -537,9 +639,9 @@ void MainWindow::on_saveButton_clicked()
QStringList failedFiles;
saveFiles(failedFiles);
if (!failedFiles.isEmpty()) {
QString errorMessage = "Failed to save the following files:\n\n";
QString errorMessage = tr("Folgende Dateien konnten nicht gespeichert werden:\n\n");
errorMessage += failedFiles.join("\n");
QMessageBox::critical(this, tr("Error"), errorMessage);
QMessageBox::critical(this, tr("Fehler beim Speichern"), errorMessage);
} else {
QMessageBox::information(this, "Information", "The translations were saved succesfully.");
}
@@ -564,57 +666,84 @@ bool MainWindow::saveFile(QTreeWidgetItem *fileItem, QString &errorMessage)
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";
const QString backupBase = QString("%1.bak.%2")
.arg(filePath, QDateTime::currentDateTime().toString("yyyyMMddHHmmsszzz"));
QString backupFileName = backupBase;
for (int suffix = 1; QFileInfo::exists(backupFileName); ++suffix) {
backupFileName = backupBase + QStringLiteral(".%1").arg(suffix);
}
if (!createBackup(filePath, backupFileName, errorMessage)) {
errorMessage = filePath + '\n' + errorMessage;
return false;
}
if (!editAndSaveFile(fileItem, backupFileName, filePath)) {
errorMessage = filePath + ": Failed to edit and save file";
if (!editAndSaveFile(fileItem, backupFileName, filePath, errorMessage)) {
errorMessage = filePath + '\n' + errorMessage + tr("\nBackup: %1").arg(backupFileName);
return false;
}
fileItem->setData(0, Qt::UserRole, false);
return true;
}
bool MainWindow::createBackup(const QString &filePath, const QString &backupFileName) {
if (!QFile::rename(filePath, backupFileName)) {
qDebug() << "Failed to create backup file: " << backupFileName;
qDebug() << "Original name: " << filePath;
bool MainWindow::createBackup(const QString &filePath, const QString &backupFileName, QString &errorMessage) {
QByteArray original;
QFile source(filePath);
if (source.open(QIODevice::ReadOnly)) {
original = source.readAll();
if (source.error() != QFileDevice::NoError) {
errorMessage = tr("Originaldatei konnte nicht gelesen werden: %1").arg(source.errorString());
return false;
}
} else {
const auto cached = fileContentsMap.find(filePath);
if (!QFileInfo::exists(filePath) && cached != fileContentsMap.end()) {
// Preserve the loaded source if an earlier save removed the file.
original = cached->second.toUtf8();
} else {
errorMessage = tr("Originaldatei konnte nicht geöffnet werden: %1").arg(source.errorString());
if (!QFileInfo::exists(filePath)) {
errorMessage += tr("\nDie Originaldatei fehlt; es ist auch keine geladene Kopie verfügbar.");
}
return false;
}
}
QSaveFile backup(backupFileName);
if (!backup.open(QIODevice::WriteOnly) || backup.write(original) != original.size() || !backup.commit()) {
errorMessage = tr("Backup konnte nicht erstellt werden:\n%1\nGrund: %2")
.arg(backupFileName, backup.errorString());
return false;
}
return true;
}
bool MainWindow::editAndSaveFile(QTreeWidgetItem *fileItem, const QString &backupFileName, const QString &fileName) {
bool MainWindow::editAndSaveFile(QTreeWidgetItem *fileItem, const QString &backupFileName, const QString &fileName, QString &errorMessage) {
QFile backupFile(backupFileName);
if (!backupFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Failed to open backup file for reading: " << backupFileName;
errorMessage = tr("Backup konnte nicht gelesen werden: %1").arg(backupFile.errorString());
return false;
}
QStringList lines;
bool changed = false;
if (!parseAndEditFile(backupFile, fileItem, lines, changed)) {
qDebug() << "Failed to parse and edit file: " << backupFileName;
errorMessage = tr("Die Übersetzungsdatei konnte nicht verarbeitet werden.");
return false;
}
backupFile.close();
if (changed) {
if (!saveToFile(fileName, lines)) {
qDebug() << "Failed to save file: " << backupFileName;
return false;
}
if (!changed) {
errorMessage = tr("Keine unterstützten Übersetzungsblöcke gefunden. Die Originaldatei bleibt unverändert.");
return false;
}
qDebug() << fileName << " created";
if (!saveToFile(fileName, lines, errorMessage)) {
return false;
}
fileContentsMap[fileName] = lines.join('\n');
return true;
}
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\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*\"(.*?)\")");
static QRegularExpression translateRegex(R"rx(# ([^\r\n]+):(\d+)\ntranslate\s+[^\s]+\s+(.*?)\n\n\s*#.*?"((?:\\.|[^"\\\r\n])*)"\s*\n\s*(.*?)\s*"((?:\\.|[^"\\\r\n])*)")rx");
static QRegularExpression oldNewRegex(R"rx((?:^|\n)\s*#\s*([^\r\n]+?):(\d+?)\n\s*old\s*"((?:\\.|[^"\\\r\n])*)"\n\s*new\s*"((?:\\.|[^"\\\r\n])*)")rx");
QRegularExpressionMatch match;
int offset = 0;
while ((match = translateRegex.match(content, offset)).hasMatch()) {
@@ -627,7 +756,7 @@ bool MainWindow::parseAndEditFile(QFile &backupFile, QTreeWidgetItem *fileItem,
QString replacedBlock = block;
const int translatedTextOffset = match.capturedStart(6) - match.capturedStart(0);
replacedBlock.replace(translatedTextOffset, match.capturedLength(6),
replacedText.isEmpty() ? newText : replacedText);
replacedText.isEmpty() ? newText : escapeTranslationForRenpy(replacedText));
content.replace(match.capturedStart(0), match.capturedLength(0), replacedBlock);
// The replacement can have a different length. Continue after the new
// block, otherwise later translations in this file may be skipped.
@@ -644,7 +773,7 @@ bool MainWindow::parseAndEditFile(QFile &backupFile, QTreeWidgetItem *fileItem,
QString replacedBlock = block;
const int translatedTextOffset = match.capturedStart(4) - match.capturedStart(0);
replacedBlock.replace(translatedTextOffset, match.capturedLength(4),
replacedText.isEmpty() ? newText : replacedText);
replacedText.isEmpty() ? newText : escapeTranslationForRenpy(replacedText));
content.replace(match.capturedStart(0), match.capturedLength(0), replacedBlock);
offset = match.capturedStart(0) + replacedBlock.size();
changed = true;
@@ -735,16 +864,13 @@ void MainWindow::onDeeplTranslationPossibilitiesLoaded(QNetworkReply *reply) {
reply->deleteLater();
}
bool MainWindow::saveToFile(const QString &backupFileName, const QStringList &lines) {
QFile backupFile(backupFileName);
if (!backupFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
qDebug() << "Failed to open backup file for writing: " << backupFileName;
bool MainWindow::saveToFile(const QString &fileName, const QStringList &lines, QString &errorMessage) {
QSaveFile output(fileName);
const QByteArray data = lines.join('\n').toUtf8();
if (!output.open(QIODevice::WriteOnly) || output.write(data) != data.size() || !output.commit()) {
errorMessage = tr("Datei konnte nicht geschrieben werden: %1").arg(output.errorString());
return false;
}
QTextStream out(&backupFile);
out << lines.join("\n") << Qt::endl;
backupFile.close();
return true;
}
@@ -861,8 +987,7 @@ void MainWindow::on_reviewIdenticalTranslationsButton_clicked()
QTreeWidgetItem *fileItem = ui->treeWidget->topLevelItem(fileIndex);
for (int itemIndex = 0; itemIndex < fileItem->childCount(); ++itemIndex) {
QTreeWidgetItem *item = fileItem->child(itemIndex);
if (!item->text(2).isEmpty() && item->text(2).trimmed() == item->text(3).trimmed()
&& !isIdenticalTranslationApproved(item)) {
if (isOpenTranslation(item) && item->text(2).trimmed() == item->text(3).trimmed()) {
candidates.append(item);
}
}
@@ -914,6 +1039,157 @@ void MainWindow::on_reviewIdenticalTranslationsButton_clicked()
countAndShowUntranslated();
}
void MainWindow::on_approveIdenticalTranslationButton_clicked()
{
QTreeWidgetItem *item = ui->treeWidget->currentItem();
if (!item || ui->treeWidget->indexOfTopLevelItem(item) >= 0) {
QMessageBox::information(this, tr("Gleicher Text"), tr("Bitte wähle zuerst einen Übersetzungseintrag aus."));
return;
}
if (item->text(2).trimmed() != ui->translationEdit->text().trimmed()) {
QMessageBox::information(this, tr("Gleicher Text"), tr("Diese Aktion ist nur möglich, wenn Original und Übersetzung identisch sind."));
return;
}
on_setTranslationButton_clicked();
on_nextUntranslatedButton_clicked();
}
void MainWindow::on_reuseTranslationsButton_clicked()
{
// Index by exact source text, never by the source line number. Keep distinct
// translations separate so context-dependent variants require a choice.
QMap<QString, QMap<QString, QStringList>> existing;
QVector<QTreeWidgetItem *> openItems;
const auto location = [this](const QTreeWidgetItem *item) {
const auto *file = item->parent();
const QString path = file->data(0, Qt::UserRole + 1).toString();
return tr("%1 — Zeile %2 (%3)")
.arg(path.isEmpty() ? file->text(0) : QDir(ui->projectDir->text()).relativeFilePath(path),
item->text(0), item->text(1).isEmpty() ? tr("Erzähler") : item->text(1));
};
for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) {
auto *file = ui->treeWidget->topLevelItem(i);
for (int j = 0; j < file->childCount(); ++j) {
auto *item = file->child(j);
if (isOpenTranslation(item)) {
openItems.append(item);
} else if (!item->text(2).trimmed().isEmpty() && !item->text(3).trimmed().isEmpty()) {
existing[item->text(2)][item->text(3)].append(location(item));
}
}
}
QVector<QTreeWidgetItem *> targets;
for (auto *item : std::as_const(openItems)) {
if (existing.contains(item->text(2))) {
targets.append(item);
}
}
if (targets.isEmpty()) {
QMessageBox::information(this, tr("Vorhandene Übersetzungen"),
tr("Für die offenen Einträge wurden keine bestätigten Übersetzungen mit exakt gleichem Originaltext in den geladenen Dateien gefunden."));
return;
}
QDialog dialog(this);
dialog.setObjectName(QStringLiteral("reuseTranslationsDialog"));
dialog.setWindowTitle(tr("Vorhandene Übersetzungen übernehmen"));
dialog.resize(1100, 650);
auto *layout = new QVBoxLayout(&dialog);
auto *description = new QLabel(tr("%1 offene Einträge haben bereits übersetzte Originaltexte. Prüfe die Auswahl und den Sprecher. Bei mehreren Varianten wähle eine Übersetzung aus. Nur markierte Einträge werden geändert; gespeichert wird anschließend über „Übersetzungen speichern“. Die bisherigen Einträge bleiben erhalten.").arg(targets.size()), &dialog);
description->setWordWrap(true);
layout->addWidget(description);
auto *table = new QTreeWidget(&dialog);
table->setObjectName(QStringLiteral("reuseTranslationsTable"));
table->setHeaderLabels({tr("Übernehmen / Ziel"), tr("Original"), tr("Vorhandene Übersetzung")});
table->setRootIsDecorated(false);
table->setAlternatingRowColors(true);
table->setUniformRowHeights(true);
table->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
table->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
table->verticalScrollBar()->setSingleStep(24);
table->horizontalScrollBar()->setSingleStep(24);
table->setColumnWidth(0, 290);
table->setColumnWidth(1, 340);
table->header()->setStretchLastSection(true);
layout->addWidget(table);
QVector<QComboBox *> choices;
for (auto *target : std::as_const(targets)) {
auto *row = new QTreeWidgetItem(table, {location(target), target->text(2)});
row->setToolTip(0, location(target));
row->setToolTip(1, target->text(2));
auto *choice = new TranslationChoiceComboBox(table);
choice->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
choice->setMinimumContentsLength(20);
choice->setPlaceholderText(tr("Übersetzung auswählen …"));
const auto &variants = existing[target->text(2)];
for (auto it = variants.cbegin(); it != variants.cend(); ++it) {
choice->addItem(it.key(), it.key());
choice->setItemData(choice->count() - 1,
it.key() + QStringLiteral("\n\n") + tr("Quelle:\n") + it.value().join('\n'),
Qt::ToolTipRole);
}
const bool unique = variants.size() == 1;
choice->setCurrentIndex(unique ? 0 : -1);
row->setCheckState(0, unique ? Qt::Checked : Qt::Unchecked);
table->setItemWidget(row, 2, choice);
choices.append(choice);
}
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Cancel, &dialog);
auto *apply = buttons->button(QDialogButtonBox::Apply);
apply->setText(tr("Ausgewählte übernehmen"));
const auto updateApply = [table, choices, apply] {
bool anySelected = false;
for (int i = 0; i < choices.size(); ++i) {
if (table->topLevelItem(i)->checkState(0) == Qt::Checked) {
if (choices[i]->currentIndex() < 0) {
apply->setEnabled(false);
return;
}
anySelected = true;
}
}
apply->setEnabled(anySelected);
};
connect(table, &QTreeWidget::itemChanged, &dialog, updateApply);
for (int i = 0; i < choices.size(); ++i) {
connect(choices[i], &QComboBox::currentIndexChanged, &dialog, [table, i, updateApply] {
table->topLevelItem(i)->setCheckState(0, Qt::Checked);
updateApply();
});
}
updateApply();
connect(apply, &QPushButton::clicked, &dialog, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
layout->addWidget(buttons);
if (dialog.exec() != QDialog::Accepted) {
return;
}
int copied = 0;
QJsonObject approvals = configuration.value("approved-identical-translations").toObject();
bool approvalsChanged = false;
for (int i = 0; i < targets.size(); ++i) {
if (table->topLevelItem(i)->checkState(0) != Qt::Checked || choices[i]->currentIndex() < 0) {
continue;
}
auto *target = targets[i];
const QString translation = choices[i]->currentData().toString();
target->setText(3, translation);
target->parent()->setData(0, Qt::UserRole, true);
if (translation.trimmed() == target->text(2).trimmed()) {
approvals[identicalTranslationApprovalKey(target)] = true;
approvalsChanged = true;
}
++copied;
}
if (approvalsChanged) {
configuration["approved-identical-translations"] = approvals;
saveConfiguration();
}
countAndShowUntranslated();
on_treeWidget_itemSelectionChanged();
ui->statusbar->showMessage(tr("%1 vorhandene Übersetzungen übernommen. Zum Schreiben der Dateien bitte speichern.").arg(copied));
}
void MainWindow::on_bulkAutoTranslateButton_clicked()
{
bulkUsesLibreTranslate = isLibreTranslateProvider();
@@ -1086,7 +1362,6 @@ void MainWindow::handleBulkTranslationReply(QNetworkReply *reply, int firstItem,
: translations.at(index).toObject().value("text").toString();
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()) {
@@ -1199,7 +1474,6 @@ void MainWindow::showBulkTranslationDialog()
? 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);
}
@@ -1305,7 +1579,6 @@ void MainWindow::translationRequestFinished(QNetworkReply *reply, bool isLibreTr
: translationsArray[0].toObject()["text"].toString();
// Restore any placeholders that were protected before the request
QString restored = restorePlaceholdersFromDeepl(translatedText);
restored.replace("\"", "'");
ui->translationEdit->setText(restored);
}
}
@@ -1393,7 +1666,11 @@ bool MainWindow::isOpenTranslation(const QTreeWidgetItem *item) const
}
const QString original = item->text(2).trimmed();
const QString translation = item->text(3).trimmed();
return translation.isEmpty() || (translation == original && !isIdenticalTranslationApproved(item));
if (translation.isEmpty()) {
return true;
}
return translation == original && !containsOnlyPlaceholders(original)
&& !isIdenticalTranslationApproved(item);
}
QString MainWindow::identicalTranslationApprovalKey(const QTreeWidgetItem *item) const