Files
renpy-translator/mainwindow.cpp

1000 lines
39 KiB
C++
Executable File

#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include <utility>
#include <QFileDialog>
#include <QMessageBox>
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QTextStream>
#include <QDebug>
#include <QDateTime>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QDialogButtonBox>
#include <QInputDialog>
#include <QClipboard>
#include <QTimer>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QMenu>
#include <QPlainTextEdit>
#include <QScrollArea>
#include <QSet>
#include <QVBoxLayout>
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<QString> 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<QString> &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("<x id=\"%1\"/>").arg(placeholders.size() - 1);
previousEnd = match.capturedEnd();
}
return protectedText + input.mid(previousEnd);
}
QString restoreBulkPlaceholders(const QString &input, const QVector<QString> &placeholders)
{
const QRegularExpression expression(QStringLiteral("<x\\s+id=\"(\\d+)\"\\s*/>"));
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::TranslationItem> MainWindow::parseTextBlock(const QString& input) {
QVector<TranslationItem> 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 <x id="N"/> 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<OriginalWordReplacementEdit *> 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("<x id=\"%1\"/>").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"(<x\s+id=\"(\d+)\"\s*/>)");
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<int(QTreeWidgetItem *)> 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());
}