#include "mainwindow.h" #include "./ui_mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { class OriginalWordReplacementEdit final : public QPlainTextEdit { public: explicit OriginalWordReplacementEdit(const QString &originalText, QWidget *parent = nullptr) : QPlainTextEdit(parent), m_originalText(originalText) { setMinimumHeight(72); setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); } protected: void contextMenuEvent(QContextMenuEvent *event) override { QTextCursor wordCursor = cursorForPosition(event->pos()); wordCursor.select(QTextCursor::WordUnderCursor); if (!wordCursor.hasSelection()) { QPlainTextEdit::contextMenuEvent(event); return; } const int wordStart = wordCursor.selectionStart(); const int wordEnd = wordCursor.selectionEnd(); QMenu *menu = createStandardContextMenu(); QMenu *replacementMenu = menu->addMenu(tr("Durch Wort aus Original ersetzen")); QSet seenWords; const QRegularExpression wordExpression(QStringLiteral("[\\p{L}\\p{N}_'-]+")); QRegularExpressionMatchIterator matches = wordExpression.globalMatch(m_originalText); while (matches.hasNext()) { const QString originalWord = matches.next().captured(0); if (seenWords.contains(originalWord)) { continue; } seenWords.insert(originalWord); QAction *action = replacementMenu->addAction(originalWord); connect(action, &QAction::triggered, this, [this, wordStart, wordEnd, originalWord] { QTextCursor replacementCursor = textCursor(); replacementCursor.setPosition(wordStart); replacementCursor.setPosition(wordEnd, QTextCursor::KeepAnchor); replacementCursor.insertText(originalWord); setTextCursor(replacementCursor); }); } if (replacementMenu->isEmpty()) { replacementMenu->setEnabled(false); } menu->exec(event->globalPos()); delete menu; } private: QString m_originalText; }; QString protectBulkPlaceholders(const QString &input, QVector &placeholders) { placeholders.clear(); const QRegularExpression expression(QStringLiteral("\\{[^}]+\\}")); QRegularExpressionMatchIterator matches = expression.globalMatch(input); if (!matches.hasNext()) { return input; } QString protectedText; int previousEnd = 0; while (matches.hasNext()) { const QRegularExpressionMatch match = matches.next(); protectedText += input.mid(previousEnd, match.capturedStart() - previousEnd); placeholders.append(match.captured(0)); protectedText += QStringLiteral("").arg(placeholders.size() - 1); previousEnd = match.capturedEnd(); } return protectedText + input.mid(previousEnd); } QString restoreBulkPlaceholders(const QString &input, const QVector &placeholders) { const QRegularExpression expression(QStringLiteral("")); QRegularExpressionMatchIterator matches = expression.globalMatch(input); if (!matches.hasNext()) { return input; } QString restoredText; int previousEnd = 0; while (matches.hasNext()) { const QRegularExpressionMatch match = matches.next(); restoredText += input.mid(previousEnd, match.capturedStart() - previousEnd); bool validId = false; const int id = match.captured(1).toInt(&validId); restoredText += validId && id >= 0 && id < placeholders.size() ? placeholders.at(id) : match.captured(0); previousEnd = match.capturedEnd(); } return restoredText + input.mid(previousEnd); } } // namespace MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { noConfigChange = true; ui->setupUi(this); loadDeeplTranslationPossibilities(); QString configPath = QDir::homePath() + "/.renpytranslate.conf"; QFile configFile(configPath); if (configFile.exists()) { if (configFile.open(QIODevice::ReadOnly)) { QByteArray data = configFile.readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); if (!doc.isNull()) { configuration = doc.object(); } else { } configFile.close(); } } if (configuration.contains("deepl-key")) { ui->deeplApiKey->setText(configuration["deepl-key"].toString()); loadDeeplTranslationPossibilities(); } if (configuration.contains("last-dir") && (QDir()).exists(configuration["last-dir"].toString())) { ui->projectDir->setText(configuration["last-dir"].toString()); crawlProject(); if (configuration.contains("last-language") && ui->languageCombo->findText(configuration["last-language"].toString()) >= 0) { ui->languageCombo->setCurrentText(configuration["last-language"].toString()); } } noConfigChange = false; } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_selectProjectDirButton_clicked() { auto dir = configuration.contains("last-dir") && (QDir()).exists(configuration["last-dir"].toString()) ? configuration["last-dir"].toString() : QDir::homePath(); QString selectedDir = QFileDialog::getExistingDirectory(this, tr("Verzeichnis auswählen"), dir); if (!selectedDir.isEmpty()) { setConfigValue("last-dir", selectedDir); ui->projectDir->setText(selectedDir); crawlProject(); } } void MainWindow::crawlProject() { QString projectDir = ui->projectDir->text(); QDir dir(projectDir); if (!dir.exists()) { QMessageBox::critical(this, tr("Error"), tr("The project directory does not exist!")); return; } QString tlDirPath = QDir::cleanPath(projectDir + QDir::separator() + "game" + QDir::separator() + "tl"); if (!QDir(tlDirPath).exists()) { QMessageBox::information(this, tr("Information"), tr("No translations available.")); return; } QStringList languageDirs = QDir(tlDirPath).entryList(QDir::Dirs | QDir::NoDotAndDotDot); if (languageDirs.isEmpty()) { QMessageBox::information(this, tr("Information"), tr("No translations available.")); return; } ui->languageCombo->clear(); ui->languageCombo->addItems(languageDirs); ui->languageCombo->setCurrentIndex(-1); } void MainWindow::on_languageCombo_currentTextChanged(const QString &selectedLanguage) { setConfigValue("last-language", selectedLanguage); fileContentsMap.clear(); ui->treeWidget->clear(); if (ui->languageCombo->currentIndex() == -1) { return; } QString projectDir = ui->projectDir->text(); QString languageDirPath = QDir::cleanPath(projectDir + QDir::separator() + "game" + QDir::separator() + "tl" + QDir::separator() + selectedLanguage); QDirIterator it(languageDirPath, QStringList() << "*.rpy", QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { QString filePath = it.next(); QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) continue; QTextStream in(&file); QString fileContent = in.readAll(); file.close(); fileContentsMap[filePath] = fileContent; populateTreeWidgetFromMap(); } } void MainWindow::populateTreeWidgetFromMap() { try { ui->treeWidget->clear(); foreach (const auto &pair, fileContentsMap) { QTreeWidgetItem *fileItem = new QTreeWidgetItem(ui->treeWidget); QString fileName = QFileInfo(pair.first).fileName(); fileItem->setText(0, fileName); fileItem->setData(0, Qt::UserRole, false); auto parsedBlocks = parseTextBlock(pair.second); for (const auto &block : std::as_const(parsedBlocks)) { QTreeWidgetItem *blockItem = new QTreeWidgetItem(fileItem); blockItem->setText(0, QString::number(block.line)); blockItem->setText(1, QString("%1").arg(block.character)); blockItem->setText(2, block.oldText); blockItem->setText(3, block.newText); } } ui->treeWidget->resizeColumnToContents(0); int firstColumnWidth = ui->treeWidget->columnWidth(0); int secondColumnWidth = ui->treeWidget->columnWidth(1); int remainingWidth = ui->treeWidget->viewport()->width() - firstColumnWidth - secondColumnWidth; int columnWidth = remainingWidth / 2; ui->treeWidget->setColumnWidth(2, columnWidth); ui->treeWidget->setColumnWidth(3, columnWidth); countAndShowUntranslated(); } catch (const std::exception &e) { qDebug() << e.what(); } } QVector MainWindow::parseTextBlock(const QString& input) { QVector blocks; QStringList lines = input.split("\n", Qt::SkipEmptyParts); 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(); } } else if (line.startsWith(" # ") || line.startsWith(" old ")) { originalText = line.mid(line.indexOf("\"") + 1).trimmed(); originalText.chop(1); identifier = ""; } else if ((line.startsWith(" ") && !line.startsWith(" # ")) || line.startsWith(" new ")) { translatedText = line.mid(4).trimmed(); if (translatedText.startsWith("new \"")) { translatedText = translatedText.mid(5).trimmed(); } else if (translatedText.startsWith("\"")) { translatedText = translatedText.mid(1).trimmed(); } else { int firstQuoteIndex = translatedText.indexOf("\""); int lastQuoteIndex = translatedText.lastIndexOf("\""); identifier = translatedText.mid(0, firstQuoteIndex).trimmed(); translatedText = translatedText.mid(firstQuoteIndex + 1, lastQuoteIndex - firstQuoteIndex).trimmed(); } translatedText.chop(1); blocks.append(TranslationItem(lineNumber, identifier, originalText, translatedText)); originalText.clear(); translatedText.clear(); } } blocks.erase(std::remove_if(blocks.begin(), blocks.end(), [](const TranslationItem& item) { return item.oldText.trimmed().isEmpty(); // Prüft nun, ob oldText leer oder nur aus Leerzeichen besteht }), blocks.end()); std::sort(blocks.begin(), blocks.end(), [](const TranslationItem& a, const TranslationItem& b) { return a.line < b.line; }); return blocks; } void MainWindow::on_reloadTranslationsButton_clicked() { crawlProject(); } void MainWindow::on_reloadFilesButton_clicked() { populateTreeWidgetFromMap(); } void MainWindow::on_nextUntranslatedButton_clicked() { 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 (childItem->text(3).isEmpty()) { 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; } } } QMessageBox::information(this, tr("Information"), tr("No untranslated item found.")); } void MainWindow::on_treeWidget_itemSelectionChanged() { QTreeWidgetItem *selectedItem = ui->treeWidget->currentItem(); if (selectedItem) { if (selectedItem->treeWidget()->indexOfTopLevelItem(selectedItem) < 0) { ui->originalTextEdit->setText(selectedItem->text(2)); // Originaltext ui->translationEdit->setText(selectedItem->text(3)); // Übersetzung } } } void MainWindow::on_setTranslationButton_clicked() { QTreeWidgetItem *selectedItem = ui->treeWidget->currentItem(); if (selectedItem) { if (selectedItem->treeWidget()->indexOfTopLevelItem(selectedItem) < 0) { // Setze Übersetzung in Spalte 3 selectedItem->setText(3, ui->translationEdit->text()); QTreeWidgetItem *parentItem = selectedItem->parent(); if (parentItem) { parentItem->setData(0, Qt::UserRole, true); for (int index = 0; index < parentItem->childCount(); ++index) { auto item = parentItem->child(index); // Vergleiche den Originaltext (Spalte 2) if (item->text(2) == ui->originalTextEdit->text()) { item->setText(3, ui->translationEdit->text()); } } } } } ui->originalTextEdit->setFocus(); countAndShowUntranslated(); } void MainWindow::on_setTranslationAndJumpNextButton_clicked() { on_setTranslationButton_clicked(); on_nextUntranslatedButton_clicked(); } void MainWindow::on_setAndNextAndAutoTranslate_clicked() { on_setTranslationAndJumpNextButton_clicked(); on_autoTranslateButton_clicked(); } void MainWindow::on_translationEdit_returnPressed() { switch (ui->enterActionCombo->currentIndex()) { case 0: on_setAndNextAndAutoTranslate_clicked(); break; case 1: on_setTranslationAndJumpNextButton_clicked(); break; case 2: on_setTranslationButton_clicked(); break; } } void MainWindow::on_saveButton_clicked() { QStringList failedFiles; saveFiles(failedFiles); if (!failedFiles.isEmpty()) { QString errorMessage = "Failed to save the following files:\n\n"; errorMessage += failedFiles.join("\n"); QMessageBox::critical(this, tr("Error"), errorMessage); } else { QMessageBox::information(this, "Information", "The translations were saved succesfully."); } } void MainWindow::saveFiles(QStringList &failedFiles) { auto projectDir = ui->projectDir->text(); for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) { QTreeWidgetItem *fileItem = ui->treeWidget->topLevelItem(i); if (fileItem->data(0, Qt::UserRole).toBool()) { QString fileName = fileItem->text(0); QString filePath = QString("%1/game/tl/%2/%3").arg(projectDir, ui->languageCombo->currentText(), fileName); QString backupFileName = QString("%1/game/tl/%2/%3.bak.%4") .arg(projectDir, ui->languageCombo->currentText(), fileName, QDateTime::currentDateTime().toString("yyyyMMddHHmmss")); if (!createBackup(filePath, backupFileName)) { failedFiles << filePath + ": Failed to create backup"; continue; } if (!editAndSaveFile(fileItem, backupFileName, filePath)) { failedFiles << filePath + ": Failed to edit and save file"; } fileItem->setData(0, Qt::UserRole, false); } } } 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; return false; } return true; } bool MainWindow::editAndSaveFile(QTreeWidgetItem *fileItem, const QString &backupFileName, const QString &fileName) { QFile backupFile(backupFileName); if (!backupFile.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Failed to open backup file for reading: " << backupFileName; return false; } QStringList lines; bool changed = false; if (!parseAndEditFile(backupFile, fileItem, lines, changed)) { qDebug() << "Failed to parse and edit file: " << backupFileName; return false; } backupFile.close(); if (changed) { if (!saveToFile(fileName, lines)) { qDebug() << "Failed to save file: " << backupFileName; return false; } } qDebug() << fileName << " created"; 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 german (.*?)\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; while ((match = translateRegex.match(content, offset)).hasMatch()) { auto block = match.captured(0); auto lineNumber = match.captured(2); auto speaker = match.captured(5); auto oldText = match.captured(4); auto newText = match.captured(6); auto replacedText = findNewText(fileItem, lineNumber, oldText, speaker); QString toReplace = QString("\"%1\"").arg(newText).trimmed(); QString replacement = QString("\"%1\"").arg(replacedText.isEmpty() ? newText : replacedText); auto replacedBlock = block.replace(toReplace, replacement); content.replace(match.capturedStart(0), match.capturedLength(0), replacedBlock); offset = match.capturedEnd(0); changed = true; } offset = 0; while ((match = oldNewRegex.match(content, offset)).hasMatch()) { auto block = match.captured(0); auto lineNumber = match.captured(2); auto oldText = match.captured(3); auto newText = match.captured(4); auto replacedText = findNewText(fileItem, lineNumber, oldText, ""); QString toReplace = QString("\"%1\"").arg(newText); QString replacement = QString("\"%1\"").arg(replacedText.isEmpty() ? newText : replacedText); auto replacedBlock = block.replace(toReplace, replacement); content.replace(match.capturedStart(0), match.capturedLength(0), replacedBlock); offset = match.capturedEnd(0); changed = true; } lines.append(content.split("\n")); return true; } QString MainWindow::findNewText(QTreeWidgetItem *fileItem, const QString &lineNumber, const QString &originalText, const QString &speaker) { for (int i = 0; i < fileItem->childCount(); ++i) { QTreeWidgetItem *childItem = fileItem->child(i); if (childItem->text(0) == lineNumber && childItem->text(1) == speaker && childItem->text(2) == originalText) { return childItem->text(3); } } return QString(); } void MainWindow::loadDeeplTranslationPossibilities() { static int attempt = 1; QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, [this, manager](QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { qDebug() << "Network error:" << reply->errorString(); qDebug() << "Attempt:" << attempt; if (attempt < 5) { attempt++; QTimer::singleShot(250, this, &MainWindow::loadDeeplTranslationPossibilities); } else { qDebug() << "Max attempts reached, giving up."; attempt = 1; } } else { attempt = 1; onDeeplTranslationPossibilitiesLoaded(reply); } reply->deleteLater(); manager->deleteLater(); }); QNetworkRequest request(QUrl("https://api-free.deepl.com/v2/glossary-language-pairs")); request.setRawHeader("Authorization", "DeepL-Auth-Key " + ui->deeplApiKey->text().toUtf8()); manager->get(request); } void MainWindow::renderDeeplSources() { ui->deeplTranslateFrom->clear(); for (const auto& pair : translationsMap) { ui->deeplTranslateFrom->addItem(pair.first); } } void MainWindow::onDeeplTranslationPossibilitiesLoaded(QNetworkReply *reply) { if (reply->error() == QNetworkReply::NoError) { QByteArray responseData = reply->readAll(); QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData); if (jsonResponse.isObject()) { QJsonObject jsonResponseObject = jsonResponse.object(); if (jsonResponseObject["supported_languages"].isArray()) { QJsonArray languagePairsArray = jsonResponseObject["supported_languages"].toArray(); for (const QJsonValue &value : languagePairsArray) { QJsonObject languagePair = value.toObject(); QString sourceLang = languagePair["source_lang"].toString(); QString targetLang = languagePair["target_lang"].toString(); translationsMap[sourceLang].push_back(targetLang); } renderDeeplSources(); } else { qDebug() << "Invalid JSON format: Data not found"; qDebug() << responseData; } } else { qDebug() << "Invalid JSON format: Response is not an object"; } } else { qDebug() << "Network error:" << reply->errorString(); } 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; return false; } QTextStream out(&backupFile); out << lines.join("\n") << Qt::endl; backupFile.close(); return true; } void MainWindow::on_cleanupButton_clicked() { QString projectDir = ui->projectDir->text(); QString language = ui->languageCombo->currentText(); QString tlDirPath = QDir::cleanPath(projectDir + QDir::separator() + "game" + QDir::separator() + "tl" + QDir::separator() + language); QDir tlDir(tlDirPath); QStringList filters; filters << "*.bak.*"; tlDir.setNameFilters(filters); QStringList fileList = tlDir.entryList(QDir::Files); for (int i = 0; i < fileList.size(); ++i) { const QString &fileName = fileList.at(i); if (!tlDir.remove(fileName)) { qDebug() << "Failed to remove" << fileName; } } } void MainWindow::on_cleanupAndSaveButton_clicked() { on_cleanupButton_clicked(); on_saveButton_clicked(); } void MainWindow::on_deeplTranslateFrom_currentTextChanged(const QString &sourceLanguage) { ui->deeplTranslateTo->clear(); auto it = translationsMap.find(sourceLanguage); if (it != translationsMap.end()) { for (QString& targetLang : it->second) { targetLang.replace("\"", "'"); ui->deeplTranslateTo->addItem(targetLang); } } } void MainWindow::on_autoTranslateButton_clicked() { if (ui->deeplApiKey->text().isEmpty()) { QMessageBox::information(this, "Deepl error", "For auto translation, you need to add a deepl API key."); return; } QString originalText = ui->originalTextEdit->text(); QString sourceLang = ui->deeplTranslateFrom->currentText(); QString targetLang = ui->deeplTranslateTo->currentText(); 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 placeholders are preserved jsonData["tag_handling"] = "xml"; jsonData["tag_handling_version"] = "v2"; QUrl url("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()); static int attempt = 1; QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, [this, manager, jsonData, request](QNetworkReply *reply) mutable { if (reply->error() != QNetworkReply::NoError) { qDebug() << "Translation request failed:" << reply->errorString(); qDebug() << "Attempt:" << attempt; if (attempt < 5) { attempt++; QTimer::singleShot(250, this, &MainWindow::on_autoTranslateButton_clicked); } else { qDebug() << "Max attempts reached for auto translation."; attempt = 1; } } else { attempt = 1; translationRequestFinished(reply); } reply->deleteLater(); manager->deleteLater(); }); manager->post(request, QJsonDocument(jsonData).toJson()); } void MainWindow::on_bulkAutoTranslateButton_clicked() { if (ui->deeplApiKey->text().isEmpty()) { QMessageBox::information(this, tr("DeepL error"), tr("Für die automatische Übersetzung wird ein DeepL-API-Key benötigt.")); return; } if (ui->deeplTranslateFrom->currentText().isEmpty() || ui->deeplTranslateTo->currentText().isEmpty()) { QMessageBox::information(this, tr("DeepL error"), tr("Bitte wähle Quell- und Zielsprache aus.")); return; } bulkTranslations.clear(); for (int fileIndex = 0; fileIndex < ui->treeWidget->topLevelItemCount(); ++fileIndex) { 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(3).isEmpty()) { BulkTranslationItem translation{item, item->text(2), QString(), {}}; bulkTranslations.append(std::move(translation)); } } } if (bulkTranslations.isEmpty()) { QMessageBox::information(this, tr("Information"), tr("Es gibt keine offenen Texte zum Übersetzen.")); return; } nextBulkTranslationIndex = 0; ui->bulkAutoTranslateButton->setEnabled(false); ui->statusbar->showMessage(tr("Übersetze %1 offene Texte …").arg(bulkTranslations.size())); requestNextBulkTranslationBatch(); } void MainWindow::requestNextBulkTranslationBatch() { if (nextBulkTranslationIndex >= bulkTranslations.size()) { ui->bulkAutoTranslateButton->setEnabled(true); ui->statusbar->clearMessage(); showBulkTranslationDialog(); return; } constexpr int maximumTextsPerRequest = 50; constexpr int maximumCharactersPerRequest = 100000; const int firstItem = nextBulkTranslationIndex; QStringList texts; int characterCount = 0; while (nextBulkTranslationIndex < bulkTranslations.size() && texts.size() < maximumTextsPerRequest) { BulkTranslationItem &item = bulkTranslations[nextBulkTranslationIndex]; const QString protectedText = protectBulkPlaceholders(item.originalText, item.placeholders); if (!texts.isEmpty() && characterCount + protectedText.size() > maximumCharactersPerRequest) { break; } texts.append(protectedText); characterCount += protectedText.size(); ++nextBulkTranslationIndex; } 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"; QNetworkRequest request(QUrl("https://api-free.deepl.com/v2/translate")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); 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) { handleBulkTranslationReply(reply, firstItem, itemCount); reply->deleteLater(); manager->deleteLater(); }); manager->post(request, QJsonDocument(payload).toJson()); } void MainWindow::handleBulkTranslationReply(QNetworkReply *reply, int firstItem, int itemCount) { 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())); bulkTranslations.clear(); return; } const QJsonDocument response = QJsonDocument::fromJson(reply->readAll()); const QJsonArray translations = response.object().value("translations").toArray(); if (translations.size() != itemCount) { ui->bulkAutoTranslateButton->setEnabled(true); ui->statusbar->clearMessage(); QMessageBox::critical(this, tr("DeepL error"), tr("DeepL hat eine unvollständige Antwort für die Sammelübersetzung geliefert.")); bulkTranslations.clear(); return; } for (int index = 0; index < translations.size(); ++index) { QString translatedText = translations.at(index).toObject().value("text").toString(); BulkTranslationItem &item = bulkTranslations[firstItem + index]; item.translatedText = restoreBulkPlaceholders(translatedText, item.placeholders); item.translatedText.replace('"', '\''); } ui->statusbar->showMessage(tr("Übersetzt: %1 von %2 …").arg(nextBulkTranslationIndex).arg(bulkTranslations.size())); requestNextBulkTranslationBatch(); } void MainWindow::showBulkTranslationDialog() { QDialog dialog(this); dialog.setWindowTitle(tr("Automatische Übersetzungen prüfen")); dialog.resize(900, 700); QVBoxLayout *layout = new QVBoxLayout(&dialog); QLabel *description = new QLabel(tr("Prüfe und bearbeite die Vorschläge vor dem Übernehmen. Mit Rechtsklick auf ein Wort in einer Übersetzung kannst du es durch ein Wort aus dem Original ersetzen."), &dialog); description->setWordWrap(true); layout->addWidget(description); QScrollArea *scrollArea = new QScrollArea(&dialog); scrollArea->setWidgetResizable(true); QWidget *content = new QWidget(scrollArea); QVBoxLayout *contentLayout = new QVBoxLayout(content); QVector edits; edits.reserve(bulkTranslations.size()); for (const BulkTranslationItem &item : std::as_const(bulkTranslations)) { QLabel *originalLabel = new QLabel(item.originalText, content); originalLabel->setWordWrap(true); originalLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); originalLabel->setStyleSheet(QStringLiteral("font-weight: 600; padding-top: 10px;")); contentLayout->addWidget(originalLabel); auto *translationEdit = new OriginalWordReplacementEdit(item.originalText, content); translationEdit->setPlainText(item.translatedText); translationEdit->setPlaceholderText(tr("Übersetzung")); contentLayout->addWidget(translationEdit); edits.append(translationEdit); } contentLayout->addStretch(); scrollArea->setWidget(content); layout->addWidget(scrollArea); auto applyTranslations = [this, &edits] { for (int index = 0; index < bulkTranslations.size(); ++index) { QTreeWidgetItem *item = bulkTranslations[index].item; item->setText(3, edits[index]->toPlainText()); if (QTreeWidgetItem *parent = item->parent()) { parent->setData(0, Qt::UserRole, true); } } countAndShowUntranslated(); }; QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Apply | QDialogButtonBox::Cancel, &dialog); buttons->button(QDialogButtonBox::Save)->setText(tr("Zwischenspeichern")); buttons->button(QDialogButtonBox::Apply)->setText(tr("Übernehmen und schließen")); connect(buttons->button(QDialogButtonBox::Save), &QPushButton::clicked, &dialog, applyTranslations); connect(buttons->button(QDialogButtonBox::Apply), &QPushButton::clicked, &dialog, [applyTranslations, &dialog] { applyTranslations(); dialog.accept(); }); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); layout->addWidget(buttons); dialog.exec(); bulkTranslations.clear(); } void MainWindow::translationRequestFinished(QNetworkReply *reply) { 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(); // Restore any placeholders that were protected before the request QString restored = restorePlaceholdersFromDeepl(translatedText); restored.replace("\"", "'"); ui->translationEdit->setText(restored); } } } else { qDebug() << "Translation request failed:" << reply->errorString(); } reply->deleteLater(); } QString MainWindow::protectPlaceholdersForDeepl(const QString &input) { deeplPlaceholderMap.clear(); QRegularExpression rx(R"(\{[^}]+\})"); QRegularExpressionMatchIterator it = rx.globalMatch(input); if (!it.hasNext()) return input; QString result; int lastPos = 0; int idx = 0; while (it.hasNext()) { QRegularExpressionMatch m = it.next(); int start = m.capturedStart(); int len = m.capturedLength(); result += input.mid(lastPos, start - lastPos); deeplPlaceholderMap.append(m.captured(0)); result += QString("").arg(idx); idx++; lastPos = start + len; } result += input.mid(lastPos); return result; } QString MainWindow::restorePlaceholdersFromDeepl(const QString &input) { if (deeplPlaceholderMap.isEmpty()) return input; QRegularExpression rx(R"()"); QRegularExpressionMatchIterator it = rx.globalMatch(input); if (!it.hasNext()) return input; QString result; int lastPos = 0; while (it.hasNext()) { QRegularExpressionMatch m = it.next(); int start = m.capturedStart(); int len = m.capturedLength(); result += input.mid(lastPos, start - lastPos); bool ok; int id = m.captured(1).toInt(&ok); if (ok && id >= 0 && id < deeplPlaceholderMap.size()) { result += deeplPlaceholderMap[id]; } else { result += m.captured(0); // fallback: leave tag as-is } lastPos = start + len; } result += input.mid(lastPos); // clear mapping after restore to avoid stale values deeplPlaceholderMap.clear(); return result; } void MainWindow::countAndShowUntranslated() { int untranslatedCount = 0; std::function countUntranslated = [&](QTreeWidgetItem *parentItem) { int untranslatedInSectionCount = 0; for (int i = 0; i < parentItem->childCount(); ++i) { QTreeWidgetItem *childItem = parentItem->child(i); if (!childItem->text(2).isEmpty() && childItem->text(3).isEmpty()) { untranslatedCount++; untranslatedInSectionCount++; } countUntranslated(childItem); } return untranslatedInSectionCount; }; for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) { QTreeWidgetItem *topLevelItem = ui->treeWidget->topLevelItem(i); int sectionCount = countUntranslated(topLevelItem); topLevelItem->setText(1, QString("%1").arg(sectionCount)); } ui->untranslatedLabel->setText(QString("%1").arg(untranslatedCount)); } void MainWindow::on_searchButton_clicked() { bool ok; QString query = QInputDialog::getText(this, "Search", "Search for:", QLineEdit::Normal, "", &ok); if (ok && !query.isEmpty()) { searchQuery = query; ui->treeWidget->setCurrentItem(NULL); searchNext(); } } void MainWindow::on_searchNextButton_clicked() { if (searchQuery.isEmpty()) { return; } qDebug() << "clicked"; searchNext(); } void MainWindow::searchNext() { auto currentItem = ui->treeWidget->currentItem(); for (int fileItemPos = 0; fileItemPos < ui->treeWidget->invisibleRootItem()->childCount(); ++fileItemPos) { auto fileItem {ui->treeWidget->invisibleRootItem()->child(fileItemPos)}; for (int linePos = 0; linePos < fileItem->childCount(); ++linePos) { auto line {fileItem->child(linePos)}; if (currentItem == NULL && (line->text(2).contains(searchQuery, Qt::CaseInsensitive) || line->text(3).contains(searchQuery, Qt::CaseInsensitive))) { ui->treeWidget->setCurrentItem(line); ui->treeWidget->scrollToItem(line); return; } if (currentItem == line) { currentItem = NULL; } } } } void MainWindow::setConfigValue(const QString &key, const QString &value) { if (noConfigChange) { return; } configuration[key] = value; QString configPath = QDir::homePath() + "/.renpytranslate.conf"; QJsonDocument doc(configuration); QFile configFile(configPath); if (configFile.open(QIODevice::WriteOnly)) { configFile.write(doc.toJson()); configFile.close(); } else { qWarning() << "Failed to open config file for writing:" << configPath; } } void MainWindow::on_deeplApiKey_editingFinished() { setConfigValue("deepl-key", ui->deeplApiKey->text()); loadDeeplTranslationPossibilities(); } void MainWindow::on_copyButton_clicked() { QGuiApplication::clipboard()->setText(ui->originalTextEdit->text()); }