1480 lines
62 KiB
C++
Executable File
1480 lines
62 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 <QCheckBox>
|
||
#include <QCryptographicHash>
|
||
#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 (isOpenTranslation(childItem)) {
|
||
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 replacedBlock = block;
|
||
const int translatedTextOffset = match.capturedStart(6) - match.capturedStart(0);
|
||
replacedBlock.replace(translatedTextOffset, match.capturedLength(6),
|
||
replacedText.isEmpty() ? newText : replacedText);
|
||
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 replacedBlock = block;
|
||
const int translatedTextOffset = match.capturedStart(4) - match.capturedStart(0);
|
||
replacedBlock.replace(translatedTextOffset, match.capturedLength(4),
|
||
replacedText.isEmpty() ? newText : replacedText);
|
||
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_reviewIdenticalTranslationsButton_clicked()
|
||
{
|
||
QVector<QTreeWidgetItem *> candidates;
|
||
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(2).trimmed() == item->text(3).trimmed()
|
||
&& !isIdenticalTranslationApproved(item)) {
|
||
candidates.append(item);
|
||
}
|
||
}
|
||
}
|
||
if (candidates.isEmpty()) {
|
||
QMessageBox::information(this, tr("Gleiche Texte prüfen"), tr("Es gibt keine unbestätigten, originalgleichen Übersetzungen."));
|
||
return;
|
||
}
|
||
|
||
QDialog dialog(this);
|
||
dialog.setWindowTitle(tr("Originalgleiche Texte prüfen"));
|
||
dialog.resize(760, 620);
|
||
auto *layout = new QVBoxLayout(&dialog);
|
||
auto *description = new QLabel(tr("Markiere nur Texte, die absichtlich unverändert bleiben sollen. Nicht markierte Texte bleiben offen und werden bei der nächsten Sammelübersetzung erneut angefragt."), &dialog);
|
||
description->setWordWrap(true);
|
||
layout->addWidget(description);
|
||
auto *scrollArea = new QScrollArea(&dialog);
|
||
scrollArea->setWidgetResizable(true);
|
||
auto *content = new QWidget(scrollArea);
|
||
auto *contentLayout = new QVBoxLayout(content);
|
||
QVector<QCheckBox *> checks;
|
||
checks.reserve(candidates.size());
|
||
for (QTreeWidgetItem *item : std::as_const(candidates)) {
|
||
const QString text = tr("%1 — Zeile %2: %3").arg(item->parent()->text(0), item->text(0), item->text(2));
|
||
auto *check = new QCheckBox(text, content);
|
||
contentLayout->addWidget(check);
|
||
checks.append(check);
|
||
}
|
||
contentLayout->addStretch();
|
||
scrollArea->setWidget(content);
|
||
layout->addWidget(scrollArea);
|
||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Cancel, &dialog);
|
||
buttons->button(QDialogButtonBox::Apply)->setText(tr("Ausgewählte bestätigen"));
|
||
connect(buttons->button(QDialogButtonBox::Apply), &QPushButton::clicked, &dialog, &QDialog::accept);
|
||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||
layout->addWidget(buttons);
|
||
|
||
if (dialog.exec() != QDialog::Accepted) {
|
||
return;
|
||
}
|
||
QJsonObject approvals = configuration.value("approved-identical-translations").toObject();
|
||
for (int index = 0; index < candidates.size(); ++index) {
|
||
if (checks[index]->isChecked()) {
|
||
approvals[identicalTranslationApprovalKey(candidates[index])] = true;
|
||
}
|
||
}
|
||
configuration["approved-identical-translations"] = approvals;
|
||
saveConfiguration();
|
||
countAndShowUntranslated();
|
||
}
|
||
|
||
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 (isOpenTranslation(item)) {
|
||
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;
|
||
}
|
||
const QString translation = edits[index]->toPlainText();
|
||
// An unchanged source string is not an automatic translation. Leave
|
||
// it open so it can be retried with a working language pair.
|
||
item->setText(3, translation.trimmed() == bulkTranslations[index].originalText.trimmed()
|
||
? QString()
|
||
: translation);
|
||
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, ¤tFileSection, &promptedFiles, &savedBulkFiles, applyTranslations](int scrollPosition) {
|
||
int visibleFileSection = currentFileSection;
|
||
for (int index = 0; index < fileSections.size(); ++index) {
|
||
if (fileSections[index].first->y() <= scrollPosition + 12) {
|
||
visibleFileSection = index;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
if (visibleFileSection <= currentFileSection) {
|
||
return;
|
||
}
|
||
|
||
const int completedFileSection = currentFileSection;
|
||
QTreeWidgetItem *completedFile = fileSections[completedFileSection].second;
|
||
currentFileSection = visibleFileSection;
|
||
if (promptedFiles.contains(completedFile)) {
|
||
return;
|
||
}
|
||
promptedFiles.insert(completedFile);
|
||
const QMessageBox::StandardButton choice = QMessageBox::question(
|
||
this,
|
||
tr("Datei speichern?"),
|
||
tr("Du bist bei einer neuen Datei angekommen. Soll die abgeschlossene Datei \"%1\" jetzt gespeichert werden?")
|
||
.arg(completedFile->text(0)),
|
||
QMessageBox::Save | QMessageBox::No,
|
||
QMessageBox::Save);
|
||
if (choice == QMessageBox::Save) {
|
||
applyTranslations();
|
||
QString errorMessage;
|
||
if (!saveFile(completedFile, errorMessage)) {
|
||
QMessageBox::critical(this, tr("Error"), errorMessage);
|
||
} else {
|
||
savedBulkFiles.insert(completedFile);
|
||
fileSections[completedFileSection].first->hide();
|
||
}
|
||
}
|
||
});
|
||
|
||
QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Apply | QDialogButtonBox::Cancel, &dialog);
|
||
buttons->button(QDialogButtonBox::Save)->setText(tr("Zwischenspeichern"));
|
||
buttons->button(QDialogButtonBox::Apply)->setText(tr("Übernehmen und schließen"));
|
||
connect(buttons->button(QDialogButtonBox::Save), &QPushButton::clicked, &dialog, applyTranslations);
|
||
connect(buttons->button(QDialogButtonBox::Apply), &QPushButton::clicked, &dialog, [applyTranslations, &dialog] {
|
||
applyTranslations();
|
||
dialog.accept();
|
||
});
|
||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||
layout->addWidget(buttons);
|
||
|
||
dialog.exec();
|
||
bulkTranslations.clear();
|
||
}
|
||
|
||
void MainWindow::translationRequestFinished(QNetworkReply *reply, bool isLibreTranslate) {
|
||
if (reply->error() == QNetworkReply::NoError) {
|
||
QByteArray responseData = reply->readAll();
|
||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||
if (!jsonResponse.isNull() && jsonResponse.isObject()) {
|
||
QJsonObject jsonObject = jsonResponse.object();
|
||
QJsonArray translationsArray = jsonObject["translations"].toArray();
|
||
if (isLibreTranslate || !translationsArray.isEmpty()) {
|
||
QString translatedText = isLibreTranslate
|
||
? jsonObject["translatedText"].toString()
|
||
: translationsArray[0].toObject()["text"].toString();
|
||
// Restore any placeholders that were protected before the request
|
||
QString restored = restorePlaceholdersFromDeepl(translatedText);
|
||
restored.replace("\"", "'");
|
||
ui->translationEdit->setText(restored);
|
||
}
|
||
}
|
||
} else {
|
||
qDebug() << "Translation request failed:" << reply->errorString();
|
||
}
|
||
reply->deleteLater();
|
||
}
|
||
|
||
QString MainWindow::protectPlaceholdersForDeepl(const QString &input) {
|
||
deeplPlaceholderMap.clear();
|
||
QRegularExpression rx(R"(\{[^}]+\})");
|
||
QRegularExpressionMatchIterator it = rx.globalMatch(input);
|
||
if (!it.hasNext()) return input;
|
||
QString result;
|
||
int lastPos = 0;
|
||
int idx = 0;
|
||
while (it.hasNext()) {
|
||
QRegularExpressionMatch m = it.next();
|
||
int start = m.capturedStart();
|
||
int len = m.capturedLength();
|
||
result += input.mid(lastPos, start - lastPos);
|
||
deeplPlaceholderMap.append(m.captured(0));
|
||
result += QString("<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 (isOpenTranslation(childItem)) {
|
||
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));
|
||
}
|
||
|
||
bool MainWindow::isOpenTranslation(const QTreeWidgetItem *item) const
|
||
{
|
||
if (!item || item->text(2).isEmpty()) {
|
||
return false;
|
||
}
|
||
const QString original = item->text(2).trimmed();
|
||
const QString translation = item->text(3).trimmed();
|
||
return translation.isEmpty() || (translation == original && !isIdenticalTranslationApproved(item));
|
||
}
|
||
|
||
QString MainWindow::identicalTranslationApprovalKey(const QTreeWidgetItem *item) const
|
||
{
|
||
const QTreeWidgetItem *fileItem = item ? item->parent() : nullptr;
|
||
const QString filePath = fileItem ? fileItem->data(0, Qt::UserRole + 1).toString() : QString();
|
||
const QString rawKey = filePath + '\n' + (item ? item->text(0) : QString()) + '\n' + (item ? item->text(2) : QString());
|
||
return QString::fromLatin1(QCryptographicHash::hash(rawKey.toUtf8(), QCryptographicHash::Sha256).toHex());
|
||
}
|
||
|
||
bool MainWindow::isIdenticalTranslationApproved(const QTreeWidgetItem *item) const
|
||
{
|
||
return configuration.value("approved-identical-translations").toObject()
|
||
.value(identicalTranslationApprovalKey(item)).toBool();
|
||
}
|
||
|
||
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;
|
||
saveConfiguration();
|
||
}
|
||
|
||
void MainWindow::saveConfiguration()
|
||
{
|
||
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());
|
||
}
|