#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 #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 replacementCursor = textCursor(); if (!replacementCursor.hasSelection()) { replacementCursor = cursorForPosition(event->pos()); replacementCursor.select(QTextCursor::WordUnderCursor); } if (!replacementCursor.hasSelection()) { QPlainTextEdit::contextMenuEvent(event); return; } const int wordStart = replacementCursor.selectionStart(); const int wordEnd = replacementCursor.selectionEnd(); QMenu *menu = createStandardContextMenu(); QMenu *replacementMenu = menu->addMenu(tr("Durch Wort aus Original ersetzen")); QSet seenWords; 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("libretranslate-url")) { ui->libreTranslateUrl->setText(configuration["libretranslate-url"].toString()); } if (configuration.contains("translation-provider")) { ui->translationProviderCombo->setCurrentText(configuration["translation-provider"].toString()); } on_translationProviderCombo_currentIndexChanged(ui->translationProviderCombo->currentIndex()); if (configuration.contains("last-dir") && (QDir()).exists(configuration["last-dir"].toString())) { ui->projectDir->setText(configuration["last-dir"].toString()); // Let the main window become visible before scanning potentially large projects. QTimer::singleShot(0, this, [this] { const QString savedLanguage = configuration.value("last-language").toString(); QProgressDialog startupProgress(tr("Projekt wird vorbereitet …"), QString(), 0, 2, this); startupProgress.setWindowTitle(tr("Bitte warten")); startupProgress.setCancelButton(nullptr); startupProgress.setMinimumDuration(0); startupProgress.setWindowModality(Qt::ApplicationModal); startupProgress.show(); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); crawlProject(); startupProgress.setValue(1); startupProgress.setLabelText(tr("Letzte Sprache und Übersetzungsdateien werden geladen …")); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); if (!savedLanguage.isEmpty() && ui->languageCombo->findText(savedLanguage) >= 0) { const QSignalBlocker blockLanguageSelection(ui->languageCombo); ui->languageCombo->setCurrentText(savedLanguage); on_languageCombo_currentTextChanged(savedLanguage); } startupProgress.setValue(2); }); } noConfigChange = false; } bool MainWindow::isLibreTranslateProvider() const { return ui->translationProviderCombo->currentIndex() == 1; } QString MainWindow::libreTranslateUrl(const QString &path) const { return ui->libreTranslateUrl->text().trimmed().replace(QRegularExpression("/+$/"), "") + path; } void MainWindow::on_translationProviderCombo_currentIndexChanged(int) { const bool libreTranslate = isLibreTranslateProvider(); ui->deeplSettingsWidget->setVisible(!libreTranslate); ui->libreSettingsWidget->setVisible(libreTranslate); setConfigValue("translation-provider", ui->translationProviderCombo->currentText()); if (libreTranslate) { loadLibreTranslateLanguages(); } else { translationsMap.clear(); loadDeeplTranslationPossibilities(); } } void MainWindow::on_libreTranslateUrl_editingFinished() { setConfigValue("libretranslate-url", ui->libreTranslateUrl->text().trimmed()); if (isLibreTranslateProvider()) { loadLibreTranslateLanguages(); } } void MainWindow::loadLibreTranslateLanguages() { QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, [this, manager](QNetworkReply *reply) { if (!isLibreTranslateProvider()) { // The provider changed while this request was in flight. } else if (reply->error() != QNetworkReply::NoError) { ui->statusbar->showMessage(tr("LibreTranslate nicht erreichbar: %1").arg(reply->errorString()), 6000); } else { translationsMap.clear(); const QJsonArray languages = QJsonDocument::fromJson(reply->readAll()).array(); for (const QJsonValue &value : languages) { const QJsonObject language = value.toObject(); const QString source = language.value("code").toString(); for (const QJsonValue &target : language.value("targets").toArray()) { translationsMap[source].push_back(target.toString()); } } renderDeeplSources(); } reply->deleteLater(); manager->deleteLater(); }); manager->get(QNetworkRequest(QUrl(libreTranslateUrl("/languages")))); } 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; } const QSignalBlocker blockLanguageSelection(ui->languageCombo); 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); QStringList filePaths; while (it.hasNext()) { filePaths.append(it.next()); } QProgressDialog progress(tr("Übersetzungsdateien werden geladen …"), QString(), 0, filePaths.size(), this); progress.setWindowTitle(tr("Bitte warten")); progress.setCancelButton(nullptr); progress.setMinimumDuration(0); progress.setWindowModality(Qt::ApplicationModal); progress.show(); QApplication::setOverrideCursor(Qt::WaitCursor); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); for (int fileIndex = 0; fileIndex < filePaths.size(); ++fileIndex) { const QString &filePath = filePaths.at(fileIndex); QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { progress.setValue(fileIndex + 1); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); continue; } QTextStream in(&file); QString fileContent = in.readAll(); file.close(); fileContentsMap[filePath] = fileContent; progress.setValue(fileIndex + 1); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } progress.setLabelText(tr("Übersetzungsliste wird vorbereitet …")); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); populateTreeWidgetFromMap(); QApplication::restoreOverrideCursor(); } 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); // 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); 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 ")) { const int firstQuote = line.indexOf('"'); const int lastQuote = line.lastIndexOf('"'); originalText = firstQuote >= 0 && lastQuote > firstQuote ? line.mid(firstQuote + 1, lastQuote - firstQuote - 1) : QString(); identifier = ""; } else if ((line.startsWith(" ") && !line.startsWith(" # ")) || line.startsWith(" new ")) { const QString translationLine = line.startsWith(" ") ? line.mid(4) : line; const int firstQuote = translationLine.indexOf('"'); const int lastQuote = translationLine.lastIndexOf('"'); if (firstQuote < 0 || lastQuote <= firstQuote) { continue; } identifier = translationLine.startsWith("new ") || translationLine.startsWith('"') ? QString() : translationLine.left(firstQuote).trimmed(); translatedText = translationLine.mid(firstQuote + 1, lastQuote - firstQuote - 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() { if (ui->languageCombo->currentIndex() < 0) { return; } bool hasUnsavedChanges = false; for (int index = 0; index < ui->treeWidget->topLevelItemCount(); ++index) { if (ui->treeWidget->topLevelItem(index)->data(0, Qt::UserRole).toBool()) { hasUnsavedChanges = true; break; } } if (hasUnsavedChanges && QMessageBox::warning( this, tr("Ungespeicherte Änderungen"), tr("Durch das Neuladen werden ungespeicherte Änderungen verworfen. Fortfahren?"), QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel) != QMessageBox::Yes) { return; } on_languageCombo_currentTextChanged(ui->languageCombo->currentText()); } 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) { for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) { QTreeWidgetItem *fileItem = ui->treeWidget->topLevelItem(i); if (fileItem->data(0, Qt::UserRole).toBool()) { QString errorMessage; if (!saveFile(fileItem, errorMessage)) { failedFiles << errorMessage; } } } } bool MainWindow::saveFile(QTreeWidgetItem *fileItem, QString &errorMessage) { const QString projectDir = ui->projectDir->text(); const QString fileName = fileItem->text(0); 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; } if (!editAndSaveFile(fileItem, backupFileName, filePath)) { errorMessage = filePath + ": Failed to edit and save file"; 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; 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\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; 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); // The replacement can have a different length. Continue after the new // block, otherwise later translations in this file may be skipped. offset = match.capturedStart(0) + replacedBlock.size(); 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.capturedStart(0) + replacedBlock.size(); 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); } if (isLibreTranslateProvider() && ui->deeplTranslateFrom->findText("en") >= 0) { ui->deeplTranslateFrom->setCurrentText("en"); } } void MainWindow::onDeeplTranslationPossibilitiesLoaded(QNetworkReply *reply) { if (isLibreTranslateProvider()) { return; } 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); } if (isLibreTranslateProvider() && ui->deeplTranslateTo->findText("de") >= 0) { ui->deeplTranslateTo->setCurrentText("de"); } } } void MainWindow::on_autoTranslateButton_clicked() { const bool libreTranslate = isLibreTranslateProvider(); if (!libreTranslate && 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); if (libreTranslate) { jsonData["q"] = protectedText; jsonData["source"] = sourceLang; jsonData["target"] = targetLang; jsonData["format"] = "text"; } else { jsonData["text"] = QJsonArray::fromStringList({protectedText}); jsonData["source_lang"] = sourceLang; jsonData["target_lang"] = targetLang; jsonData["formality"] = "less"; jsonData["tag_handling"] = "xml"; jsonData["tag_handling_version"] = "v2"; } QUrl url(libreTranslate ? libreTranslateUrl("/translate") : QStringLiteral("https://api-free.deepl.com/v2/translate")); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); if (!libreTranslate) { 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, libreTranslate](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, libreTranslate); } reply->deleteLater(); manager->deleteLater(); }); 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(); if (!bulkUsesLibreTranslate && 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::on_checkDeeplUsageButton_clicked() { if (ui->deeplApiKey->text().isEmpty()) { QMessageBox::information(this, tr("DeepL error"), tr("Bitte hinterlege zuerst einen DeepL-API-Key.")); return; } ui->checkDeeplUsageButton->setEnabled(false); ui->statusbar->showMessage(tr("DeepL-Kontingent wird geprüft …")); QNetworkRequest request(QUrl("https://api-free.deepl.com/v2/usage")); request.setRawHeader("Authorization", "DeepL-Auth-Key " + ui->deeplApiKey->text().toUtf8()); QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, [this, manager](QNetworkReply *reply) { ui->checkDeeplUsageButton->setEnabled(true); ui->statusbar->clearMessage(); const QByteArray responseBody = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { QString details; const QJsonDocument response = QJsonDocument::fromJson(responseBody); if (response.isObject()) { details = response.object().value("message").toString(); } QMessageBox::critical(this, tr("DeepL error"), tr("Das DeepL-Kontingent konnte nicht abgefragt werden: %1") .arg(details.isEmpty() ? reply->errorString() : details)); } else { const QJsonObject usage = QJsonDocument::fromJson(responseBody).object(); const qint64 usedCharacters = usage.value("character_count").toVariant().toLongLong(); const qint64 characterLimit = usage.value("character_limit").toVariant().toLongLong(); if (characterLimit <= 0) { QMessageBox::critical(this, tr("DeepL error"), tr("DeepL hat keine gültigen Kontingentdaten geliefert.")); } else { const qint64 remainingCharacters = characterLimit - usedCharacters; const double usedPercentage = 100.0 * usedCharacters / characterLimit; QMessageBox::information(this, tr("DeepL-Kontingent"), tr("Verbraucht: %1 von %2 Zeichen (%3 %).\nVerfügbar: %4 Zeichen.") .arg(QLocale().toString(usedCharacters), QLocale().toString(characterLimit), QLocale().toString(usedPercentage, 'f', 1), QLocale().toString(remainingCharacters))); } } reply->deleteLater(); manager->deleteLater(); }); manager->get(request); } 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; if (bulkUsesLibreTranslate) { payload["q"] = QJsonArray::fromStringList(texts); payload["source"] = ui->deeplTranslateFrom->currentText(); payload["target"] = ui->deeplTranslateTo->currentText(); payload["format"] = "text"; } else { 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(bulkUsesLibreTranslate ? libreTranslateUrl("/translate") : QStringLiteral("https://api-free.deepl.com/v2/translate"))); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); if (!bulkUsesLibreTranslate) { 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(); const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const QString errorMessage = httpStatus == 456 ? tr("Das DeepL-Zeichenkontingent für diesen API-Key ist ausgeschöpft. " "Bei API Free wird es im nächsten Abrechnungszeitraum zurückgesetzt; bei API Pro prüfe bitte das Cost-Control-Limit.") : tr("Die Sammelübersetzung ist fehlgeschlagen: %1").arg(reply->errorString()); QMessageBox::critical(this, tr("DeepL error"), errorMessage); bulkTranslations.clear(); return; } const QJsonDocument response = QJsonDocument::fromJson(reply->readAll()); const QJsonObject responseObject = response.object(); const QJsonArray translations = bulkUsesLibreTranslate ? responseObject.value("translatedText").toArray() : responseObject.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 = bulkUsesLibreTranslate ? translations.at(index).toString() : 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()) { item.translatedText.clear(); } } 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; QVector> fileSections; const bool dialogUsesLibreTranslate = bulkUsesLibreTranslate; const QString dialogSourceLanguage = ui->deeplTranslateFrom->currentText(); const QString dialogTargetLanguage = ui->deeplTranslateTo->currentText(); QSet savedBulkFiles; edits.reserve(bulkTranslations.size()); QTreeWidgetItem *currentFile = nullptr; QVBoxLayout *fileLayout = nullptr; for (int bulkIndex = 0; bulkIndex < bulkTranslations.size(); ++bulkIndex) { BulkTranslationItem &item = bulkTranslations[bulkIndex]; QTreeWidgetItem *fileItem = item.item->parent(); if (fileItem != currentFile) { if (currentFile) { auto *separator = new QFrame(content); separator->setFrameShape(QFrame::HLine); separator->setFrameShadow(QFrame::Sunken); contentLayout->addWidget(separator); } currentFile = fileItem; auto *fileSection = new QWidget(content); fileLayout = new QVBoxLayout(fileSection); fileLayout->setContentsMargins(6, 6, 6, 6); fileLayout->setSpacing(8); auto *fileLabel = new QLabel(tr("Datei: %1").arg(fileItem->text(0)), fileSection); fileLabel->setStyleSheet(QStringLiteral("font-size: 15px; font-weight: 700; color: palette(highlight);")); fileLayout->addWidget(fileLabel); contentLayout->addWidget(fileSection); fileSections.append(qMakePair(fileSection, fileItem)); } 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;")); originalRowLayout->addWidget(originalLabel, 1); auto *translationEdit = new OriginalWordReplacementEdit(item.originalText, content); translationEdit->setPlainText(item.translatedText); translationEdit->setPlaceholderText(tr("Keine Übersetzung erhalten – später erneut übersetzen oder manuell eintragen.")); auto *retryButton = new QPushButton(tr("Neu übersetzen"), originalRow); retryButton->setToolTip(tr("Fordert für diesen einzelnen Text einen neuen Vorschlag an.")); originalRowLayout->addWidget(retryButton); connect(retryButton, &QPushButton::clicked, &dialog, [this, retryButton, translationEdit, bulkIndex, dialogUsesLibreTranslate, dialogSourceLanguage, dialogTargetLanguage, &dialog] { BulkTranslationItem &retryItem = bulkTranslations[bulkIndex]; QVector placeholders; const QString protectedText = protectBulkPlaceholders(retryItem.originalText, placeholders); retryButton->setEnabled(false); QJsonObject payload; if (dialogUsesLibreTranslate) { payload["q"] = protectedText; payload["source"] = dialogSourceLanguage; payload["target"] = dialogTargetLanguage; payload["format"] = "text"; } else { payload["text"] = QJsonArray::fromStringList({protectedText}); payload["source_lang"] = dialogSourceLanguage; payload["target_lang"] = dialogTargetLanguage; payload["formality"] = "less"; payload["tag_handling"] = "xml"; payload["tag_handling_version"] = "v2"; } QNetworkRequest request(QUrl(dialogUsesLibreTranslate ? libreTranslateUrl("/translate") : QStringLiteral("https://api-free.deepl.com/v2/translate"))); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); if (!dialogUsesLibreTranslate) { request.setRawHeader("Authorization", "DeepL-Auth-Key " + ui->deeplApiKey->text().toUtf8()); } auto *manager = new QNetworkAccessManager(&dialog); connect(manager, &QNetworkAccessManager::finished, &dialog, [this, manager, retryButton, translationEdit, bulkIndex, placeholders, dialogUsesLibreTranslate](QNetworkReply *reply) { retryButton->setEnabled(true); if (reply->error() != QNetworkReply::NoError) { QMessageBox::warning(this, tr("Übersetzung fehlgeschlagen"), reply->errorString()); } else { const QJsonObject response = QJsonDocument::fromJson(reply->readAll()).object(); const QJsonArray translations = response.value("translations").toArray(); QString translatedText = dialogUsesLibreTranslate ? response.value("translatedText").toString() : (!translations.isEmpty() ? translations.at(0).toObject().value("text").toString() : QString()); translatedText = restoreBulkPlaceholders(translatedText, placeholders); translatedText.replace('"', '\''); bulkTranslations[bulkIndex].translatedText = translatedText; translationEdit->setPlainText(translatedText); } reply->deleteLater(); manager->deleteLater(); }); manager->post(request, QJsonDocument(payload).toJson()); }); fileLayout->addWidget(originalRow); fileLayout->addWidget(translationEdit); edits.append(translationEdit); } contentLayout->addStretch(); scrollArea->setWidget(content); layout->addWidget(scrollArea); auto applyTranslations = [this, &edits, &savedBulkFiles] { for (int index = 0; index < bulkTranslations.size(); ++index) { QTreeWidgetItem *item = bulkTranslations[index].item; if (savedBulkFiles.contains(item->parent())) { continue; } item->setText(3, edits[index]->toPlainText()); if (QTreeWidgetItem *parent = item->parent()) { parent->setData(0, Qt::UserRole, true); } } countAndShowUntranslated(); }; int currentFileSection = 0; QSet promptedFiles; connect(scrollArea->verticalScrollBar(), &QScrollBar::valueChanged, &dialog, [this, &fileSections, ¤tFileSection, &promptedFiles, &savedBulkFiles, applyTranslations](int scrollPosition) { int visibleFileSection = currentFileSection; for (int index = 0; index < fileSections.size(); ++index) { if (fileSections[index].first->y() <= scrollPosition + 12) { visibleFileSection = index; } else { break; } } if (visibleFileSection <= currentFileSection) { return; } const int completedFileSection = currentFileSection; QTreeWidgetItem *completedFile = fileSections[completedFileSection].second; currentFileSection = visibleFileSection; if (promptedFiles.contains(completedFile)) { return; } promptedFiles.insert(completedFile); const QMessageBox::StandardButton choice = QMessageBox::question( this, tr("Datei speichern?"), tr("Du bist bei einer neuen Datei angekommen. Soll die abgeschlossene Datei \"%1\" jetzt gespeichert werden?") .arg(completedFile->text(0)), QMessageBox::Save | QMessageBox::No, QMessageBox::Save); if (choice == QMessageBox::Save) { applyTranslations(); QString errorMessage; if (!saveFile(completedFile, errorMessage)) { QMessageBox::critical(this, tr("Error"), errorMessage); } else { savedBulkFiles.insert(completedFile); fileSections[completedFileSection].first->hide(); } } }); 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, bool isLibreTranslate) { 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 (isLibreTranslate || !translationsArray.isEmpty()) { QString translatedText = isLibreTranslate ? jsonObject["translatedText"].toString() : translationsArray[0].toObject()["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()); }