Files
renpy-translator/mainwindow.cpp

1382 lines
57 KiB
C++
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 <QApplication>
#include <QCoreApplication>
#include <QTimer>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFrame>
#include <QLabel>
#include <QLocale>
#include <QMenu>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QProgressDialog>
#include <QScrollArea>
#include <QSet>
#include <QSignalBlocker>
#include <QScrollBar>
#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 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<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("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::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 ")) {
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<OriginalWordReplacementEdit *> edits;
QVector<QPair<QWidget *, QTreeWidgetItem *>> fileSections;
const bool dialogUsesLibreTranslate = bulkUsesLibreTranslate;
const QString dialogSourceLanguage = ui->deeplTranslateFrom->currentText();
const QString dialogTargetLanguage = ui->deeplTranslateTo->currentText();
QSet<QTreeWidgetItem *> 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<QString> 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<QTreeWidgetItem *> promptedFiles;
connect(scrollArea->verticalScrollBar(), &QScrollBar::valueChanged, &dialog,
[this, &fileSections, &currentFileSection, &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("<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());
}