Füge Unterstützung für LibreTranslate hinzu und verbessere die Benutzeroberfläche

This commit is contained in:
Torsten Schulz (local)
2026-09-16 12:01:39 +02:00
parent 701720f029
commit 414c218839
4 changed files with 354 additions and 426 deletions

77
docs/libretranslate.md Normal file
View File

@@ -0,0 +1,77 @@
# Lokaler LibreTranslate-Server
LibreTranslate stellt eine HTTP-API auf dem eigenen Rechner bereit. Es werden keine
Texte an einen externen Übersetzungsdienst gesendet. Die Standardadresse ist
`http://localhost:5000`.
## Installation unter Linux
Voraussetzung: Python 3.8 oder neuer. In einem Terminal:
```bash
python3 -m venv ~/.local/share/libretranslate-venv
~/.local/share/libretranslate-venv/bin/pip install --upgrade pip libretranslate
~/.local/share/libretranslate-venv/bin/libretranslate
```
Beim ersten Start werden Sprachmodelle geladen. Für weniger Speicherbedarf kann man
nur benötigte Sprachen laden, beispielsweise Englisch und Deutsch:
```bash
~/.local/share/libretranslate-venv/bin/libretranslate --load-only en,de
```
Der Server bleibt im Terminal aktiv. Anschließend in Renpy Translation Helper im
Auswahlfeld **Provider** `LibreTranslate (local)` wählen. Die voreingestellte lokale
URL `http://localhost:5000` übernehmen oder anpassen. Die verfügbaren Sprachpaare
werden automatisch geladen.
## Nur Englisch und Deutsch nachinstallieren
Wurde ein erster Start ohne `--load-only` abgebrochen, können bereits einige
unpassende Modelle installiert sein. Den Server mit `Strg+C` beenden und die zwei
benötigten Richtungen explizit installieren:
```bash
~/.local/share/libretranslate-venv/bin/argospm update
~/.local/share/libretranslate-venv/bin/argospm install translate-en_de
~/.local/share/libretranslate-venv/bin/argospm install translate-de_en
~/.local/share/libretranslate-venv/bin/libretranslate --load-only en,de
```
Mit `curl http://127.0.0.1:5000/languages` prüfen, ob sowohl `en` mit Ziel `de`
als auch `de` mit Ziel `en` aufgelistet wird. Erst dann kann die App diese
Sprachrichtung anbieten.
## Nutzung durch andere Anwendungen
Sprachen abfragen:
```bash
curl http://localhost:5000/languages
```
Einen Text übersetzen:
```bash
curl -X POST http://localhost:5000/translate \
-H 'Content-Type: application/json' \
-d '{"q":"Hello world","source":"en","target":"de","format":"text"}'
```
Für mehrere Texte kann `q` ein Array sein:
```bash
curl -X POST http://localhost:5000/translate \
-H 'Content-Type: application/json' \
-d '{"q":["Hello","Good night"],"source":"en","target":"de","format":"text"}'
```
Die Antwort enthält `translatedText` bei mehreren Eingaben ebenfalls als Array.
## Hinweis zu AMD-Grafikkarten
LibreTranslate/Argos Translate verwendet regulär die CPU. Die offiziell dokumentierte
GPU-Beschleunigung setzt CUDA voraus und ist damit für Nvidia vorgesehen. Eine
AMD-GPU wird in diesem Standard-Setup nicht genutzt; für normale Ren'Py-Dialoge ist
der CPU-Betrieb dennoch brauchbar.

View File

@@ -22,6 +22,7 @@
#include <QDialogButtonBox>
#include <QFrame>
#include <QLabel>
#include <QLocale>
#include <QMenu>
#include <QPlainTextEdit>
#include <QScrollArea>
@@ -154,6 +155,13 @@ MainWindow::MainWindow(QWidget *parent)
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());
crawlProject();
@@ -164,6 +172,67 @@ MainWindow::MainWindow(QWidget *parent)
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->label_9->setVisible(!libreTranslate);
ui->deeplApiKey->setVisible(!libreTranslate);
ui->checkDeeplUsageButton->setVisible(!libreTranslate);
ui->libreTranslateUrlLabel->setVisible(libreTranslate);
ui->libreTranslateUrl->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;
@@ -559,6 +628,9 @@ void MainWindow::renderDeeplSources()
}
void MainWindow::onDeeplTranslationPossibilitiesLoaded(QNetworkReply *reply) {
if (isLibreTranslateProvider()) {
return;
}
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
@@ -638,7 +710,8 @@ void MainWindow::on_deeplTranslateFrom_currentTextChanged(const QString &sourceL
}
void MainWindow::on_autoTranslateButton_clicked() {
if (ui->deeplApiKey->text().isEmpty()) {
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;
}
@@ -648,21 +721,29 @@ void MainWindow::on_autoTranslateButton_clicked() {
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";
// Use XML tag handling so our <x id="N"/> placeholders are preserved
jsonData["tag_handling"] = "xml";
jsonData["tag_handling_version"] = "v2";
QUrl url("https://api-free.deepl.com/v2/translate");
}
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](QNetworkReply *reply) mutable {
[this, manager, jsonData, request, libreTranslate](QNetworkReply *reply) mutable {
if (reply->error() != QNetworkReply::NoError) {
qDebug() << "Translation request failed:" << reply->errorString();
qDebug() << "Attempt:" << attempt;
@@ -675,7 +756,7 @@ void MainWindow::on_autoTranslateButton_clicked() {
}
} else {
attempt = 1;
translationRequestFinished(reply);
translationRequestFinished(reply, libreTranslate);
}
reply->deleteLater();
manager->deleteLater();
@@ -686,7 +767,8 @@ void MainWindow::on_autoTranslateButton_clicked() {
void MainWindow::on_bulkAutoTranslateButton_clicked()
{
if (ui->deeplApiKey->text().isEmpty()) {
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;
}
@@ -718,6 +800,54 @@ void MainWindow::on_bulkAutoTranslateButton_clicked()
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()) {
@@ -744,16 +874,25 @@ void MainWindow::requestNextBulkTranslationBatch()
}
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("https://api-free.deepl.com/v2/translate"));
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) {
@@ -769,13 +908,21 @@ void MainWindow::handleBulkTranslationReply(QNetworkReply *reply, int firstItem,
if (reply->error() != QNetworkReply::NoError) {
ui->bulkAutoTranslateButton->setEnabled(true);
ui->statusbar->clearMessage();
QMessageBox::critical(this, tr("DeepL error"), tr("Die Sammelübersetzung ist fehlgeschlagen: %1").arg(reply->errorString()));
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 QJsonArray translations = response.object().value("translations").toArray();
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();
@@ -785,7 +932,9 @@ void MainWindow::handleBulkTranslationReply(QNetworkReply *reply, int firstItem,
}
for (int index = 0; index < translations.size(); ++index) {
QString translatedText = translations.at(index).toObject().value("text").toString();
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('"', '\'');
@@ -915,16 +1064,17 @@ void MainWindow::showBulkTranslationDialog()
bulkTranslations.clear();
}
void MainWindow::translationRequestFinished(QNetworkReply *reply) {
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 (!translationsArray.isEmpty()) {
QJsonObject translationObject = translationsArray[0].toObject();
QString translatedText = translationObject["text"].toString();
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("\"", "'");

View File

@@ -42,6 +42,9 @@ private slots:
void on_deeplTranslateFrom_currentTextChanged(const QString &sourceLanguage);
void on_autoTranslateButton_clicked();
void on_bulkAutoTranslateButton_clicked();
void on_checkDeeplUsageButton_clicked();
void on_translationProviderCombo_currentIndexChanged(int index);
void on_libreTranslateUrl_editingFinished();
void on_searchButton_clicked();
void on_searchNextButton_clicked();
void on_deeplApiKey_editingFinished();
@@ -83,7 +86,10 @@ private:
QString findNewText(QTreeWidgetItem *fileItem, const QString &lineNumber, const QString &originalText, const QString &speaker);
void loadDeeplTranslationPossibilities();
void renderDeeplSources();
void translationRequestFinished(QNetworkReply *reply);
void translationRequestFinished(QNetworkReply *reply, bool isLibreTranslate = false);
void loadLibreTranslateLanguages();
bool isLibreTranslateProvider() const;
QString libreTranslateUrl(const QString &path) const;
void requestNextBulkTranslationBatch();
void handleBulkTranslationReply(QNetworkReply *reply, int firstItem, int itemCount);
void showBulkTranslationDialog();
@@ -98,5 +104,6 @@ private:
QString restorePlaceholdersFromDeepl(const QString &input);
QVector<BulkTranslationItem> bulkTranslations;
int nextBulkTranslationIndex{0};
bool bulkUsesLibreTranslate{false};
};
#endif // MAINWINDOW_H

466
mainwindow.ui Executable file → Normal file
View File

@@ -2,412 +2,106 @@
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1266</width>
<height>774</height>
</rect>
</property>
<property name="windowTitle">
<string>Renpy Translation Helper</string>
</property>
<property name="geometry"><rect><x>0</x><y>0</y><width>1280</width><height>820</height></rect></property>
<property name="windowTitle"><string>Ren'Py Translation Helper</string></property>
<widget class="QWidget" name="centralwidget">
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>1233</width>
<height>732</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="mainLayout">
<property name="spacing"><number>10</number></property>
<property name="leftMargin"><number>12</number></property>
<property name="topMargin"><number>12</number></property>
<property name="rightMargin"><number>12</number></property>
<property name="bottomMargin"><number>12</number></property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Project Directory</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="projectDir">
<property name="minimumSize">
<size>
<width>500</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="selectProjectDirButton">
<property name="maximumSize">
<size>
<width>27</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="reloadTranslationsButton">
<property name="text">
<string>Reload</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="languageCombo">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="reloadFilesButton">
<property name="text">
<string>Reload</string>
</property>
</widget>
</item>
<widget class="QGroupBox" name="projectGroup">
<property name="title"><string>Projekt</string></property>
<layout class="QHBoxLayout" name="projectLayout">
<item><widget class="QLabel" name="projectDirectoryLabel"><property name="text"><string>Projektordner</string></property></widget></item>
<item><widget class="QLineEdit" name="projectDir"><property name="minimumSize"><size><width>480</width><height>0</height></size></property></widget></item>
<item><widget class="QPushButton" name="selectProjectDirButton"><property name="toolTip"><string>Projektordner auswählen</string></property><property name="text"><string>…</string></property></widget></item>
<item><widget class="QPushButton" name="reloadTranslationsButton"><property name="text"><string>Übersetzungen laden</string></property></widget></item>
<item><spacer name="projectSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>16</width><height>20</height></size></property></spacer></item>
<item><widget class="QLabel" name="languageLabel"><property name="text"><string>Sprache</string></property></widget></item>
<item><widget class="QComboBox" name="languageCombo"><property name="minimumSize"><size><width>180</width><height>0</height></size></property></widget></item>
<item><widget class="QPushButton" name="reloadFilesButton"><property name="text"><string>Dateien neu laden</string></property></widget></item>
</layout>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="treeWidget">
<property name="minimumSize">
<size>
<width>1231</width>
<height>400</height>
</size>
</property>
<column>
<property name="text">
<string>File</string>
</property>
</column>
<column>
<property name="text">
<string>Speaker</string>
</property>
</column>
<column>
<property name="text">
<string>Original</string>
</property>
</column>
<column>
<property name="text">
<string>Translation</string>
</property>
</column>
<item>
<property name="text">
<string>All items</string>
</property>
<property name="text">
<string/>
</property>
<property name="text">
<string/>
</property>
<property name="text">
<string/>
</property>
</item>
<property name="minimumSize"><size><width>0</width><height>300</height></size></property>
<property name="alternatingRowColors"><bool>true</bool></property>
<property name="uniformRowHeights"><bool>true</bool></property>
<column><property name="text"><string>Datei / Zeile</string></property></column>
<column><property name="text"><string>Sprecher</string></property></column>
<column><property name="text"><string>Original</string></property></column>
<column><property name="text"><string>Übersetzung</string></property></column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<widget class="QGroupBox" name="editorGroup">
<property name="title"><string>Übersetzung bearbeiten</string></property>
<layout class="QVBoxLayout" name="editorLayout">
<item>
<widget class="QPushButton" name="nextUntranslatedButton">
<property name="text">
<string>Jump to next untranslated item</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="maximumSize">
<size>
<width>137</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Deepl Translate from</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="deeplTranslateFrom"/>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>to</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="deeplTranslateTo"/>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>Deepl API Key</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="deeplApiKey">
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="autoTranslateButton">
<property name="text">
<string>Auto translate</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bulkAutoTranslateButton">
<property name="toolTip">
<string>Translate all untranslated texts and review the suggestions before applying them.</string>
</property>
<property name="text">
<string>Translate all open texts</string>
</property>
</widget>
</item>
<layout class="QHBoxLayout" name="navigationLayout">
<item><widget class="QPushButton" name="nextUntranslatedButton"><property name="text"><string>Nächster offener Text</string></property></widget></item>
<item><widget class="QToolButton" name="searchButton"><property name="text"><string>Suchen</string></property></widget></item>
<item><widget class="QToolButton" name="searchNextButton"><property name="text"><string>Weiter</string></property></widget></item>
<item><spacer name="navigationSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QToolButton" name="searchButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="searchNextButton">
<property name="text">
<string>Next</string>
</property>
</widget>
</item>
<layout class="QFormLayout" name="textFormLayout">
<item row="0" column="0"><widget class="QLabel" name="originalTextLabel"><property name="text"><string>Originaltext</string></property></widget></item>
<item row="0" column="1"><widget class="QLineEdit" name="originalTextEdit"/></item>
<item row="0" column="2"><widget class="QPushButton" name="copyButton"><property name="text"><string>Kopieren</string></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="translationLabel"><property name="text"><string>Übersetzung</string></property></widget></item>
<item row="1" column="1" colspan="2"><widget class="QLineEdit" name="translationEdit"/></item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Original Text:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="originalTextEdit">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="copyButton">
<property name="text">
<string>Copy</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Translation:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="translationEdit"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QPushButton" name="setTranslationButton">
<property name="text">
<string>Set translation</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="setTranslationAndJumpNextButton">
<property name="text">
<string>Set translation and jump to next untranslated</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="setAndNextAndAutoTranslate">
<property name="text">
<string>Set translation, jump to next untranslated and auto translate</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Enter action:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="enterActionCombo">
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<item>
<property name="text">
<string>Set translation, jump to next untranslated and auto translate</string>
</property>
</item>
<item>
<property name="text">
<string>Set translation and jump to next untranslated</string>
</property>
</item>
<item>
<property name="text">
<string>Set translation</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Untranslated:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="untranslatedLabel">
<property name="text">
<string>0</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QPushButton" name="saveButton">
<property name="text">
<string>Save translations</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cleanupAndSaveButton">
<property name="text">
<string>Delete backups and save translations</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="cleanupButton">
<property name="text">
<string>Delete backups</string>
</property>
</widget>
</item>
<layout class="QHBoxLayout" name="translationActionLayout">
<item><widget class="QPushButton" name="setTranslationButton"><property name="text"><string>Übernehmen</string></property></widget></item>
<item><widget class="QPushButton" name="setTranslationAndJumpNextButton"><property name="text"><string>Übernehmen &amp;&amp; weiter</string></property></widget></item>
<item><widget class="QPushButton" name="setAndNextAndAutoTranslate"><property name="text"><string>Übernehmen, weiter &amp;&amp; automatisch übersetzen</string></property></widget></item>
<item><spacer name="translationActionSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="providerGroup">
<property name="title"><string>Automatische Übersetzung</string></property>
<layout class="QGridLayout" name="providerLayout">
<item row="0" column="0"><widget class="QLabel" name="translationProviderLabel"><property name="text"><string>Anbieter</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="translationProviderCombo"><item><property name="text"><string>DeepL</string></property></item><item><property name="text"><string>LibreTranslate (lokal)</string></property></item></widget></item>
<item row="0" column="2"><widget class="QLabel" name="label_4"><property name="text"><string>Von</string></property></widget></item>
<item row="0" column="3"><widget class="QComboBox" name="deeplTranslateFrom"/></item>
<item row="0" column="4"><widget class="QLabel" name="label_5"><property name="text"><string>Nach</string></property></widget></item>
<item row="0" column="5"><widget class="QComboBox" name="deeplTranslateTo"/></item>
<item row="0" column="6"><widget class="QPushButton" name="autoTranslateButton"><property name="text"><string>Aktuellen Text übersetzen</string></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="label_9"><property name="text"><string>DeepL-API-Key</string></property></widget></item>
<item row="1" column="1" colspan="3"><widget class="QLineEdit" name="deeplApiKey"/></item>
<item row="1" column="4"><widget class="QPushButton" name="checkDeeplUsageButton"><property name="text"><string>Kontingent prüfen</string></property></widget></item>
<item row="1" column="5" colspan="2"><widget class="QPushButton" name="bulkAutoTranslateButton"><property name="text"><string>Alle offenen Texte übersetzen</string></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="libreTranslateUrlLabel"><property name="text"><string>Lokale URL</string></property></widget></item>
<item row="2" column="1" colspan="5"><widget class="QLineEdit" name="libreTranslateUrl"><property name="text"><string>http://localhost:5000</string></property></widget></item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="footerLayout">
<item><widget class="QLabel" name="label_7"><property name="text"><string>Eingabetaste</string></property></widget></item>
<item><widget class="QComboBox" name="enterActionCombo"><property name="minimumSize"><size><width>360</width><height>0</height></size></property><item><property name="text"><string>Übernehmen, weiter &amp;&amp; automatisch übersetzen</string></property></item><item><property name="text"><string>Übernehmen &amp;&amp; weiter</string></property></item><item><property name="text"><string>Nur übernehmen</string></property></item></widget></item>
<item><spacer name="footerSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
<item><widget class="QLabel" name="label_8"><property name="text"><string>Offen</string></property></widget></item>
<item><widget class="QLabel" name="untranslatedLabel"><property name="text"><string>0</string></property></widget></item>
<item><widget class="QPushButton" name="saveButton"><property name="text"><string>Übersetzungen speichern</string></property></widget></item>
<item><widget class="QPushButton" name="cleanupAndSaveButton"><property name="text"><string>Backups löschen &amp;&amp; speichern</string></property></widget></item>
<item><widget class="QPushButton" name="cleanupButton"><property name="text"><string>Backups löschen</string></property></widget></item>
</layout>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>