#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 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); } } } #include 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; jsonData["text"] = QJsonArray::fromStringList({originalText}); jsonData["source_lang"] = sourceLang; jsonData["target_lang"] = targetLang; 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::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(); translatedText.replace("\"", "'"); ui->translationEdit->setText(translatedText); } } } else { qDebug() << "Translation request failed:" << reply->errorString(); } reply->deleteLater(); } 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()); }