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:
387
mainwindow.cpp
387
mainwindow.cpp
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user