5#include "ui_machinetranslationdialog.h"
8#include "auto-translation/machinetranslator.h"
9#include "auto-translation/translationsettings.h"
12#include <QtCore/qsettings.h>
13#include <QtWidgets/qmessagebox.h>
17using namespace Qt::Literals::StringLiterals;
24 const QLatin1String displayName;
25 const QLatin1String defaultUrl;
30constexpr ApiTypeInfo s_apiTypes[] = {
31 { TranslationApiType::Ollama,
"Ollama"_L1,
"http://localhost:11434"_L1,
false },
32 { TranslationApiType::OpenAICompatible,
"OpenAI Compatible"_L1,
"http://localhost:8080"_L1,
38 for (
const auto &info : s_apiTypes) {
39 if (info.type == type)
49static constexpr std::array<
const char *, 3> toolBoxTexts {
50 QT_TRANSLATE_NOOP(
"MachineTranslationDialog",
"Configuration"),
51 QT_TRANSLATE_NOOP(
"MachineTranslationDialog",
"Selection"),
52 QT_TRANSLATE_NOOP(
"MachineTranslationDialog",
"Progress")
57 m_ui(std::make_unique<Ui::MachineTranslationDialog>()),
58 m_translator(std::make_unique<MachineTranslator>())
65 m_ui->statusLabel->setWordWrap(
true);
66 m_ui->statusLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
67 connect(m_ui->translateButton, &QPushButton::clicked,
this,
69 connect(m_ui->filesComboBox, &QComboBox::currentIndexChanged,
this, [
this] {
70 m_ui->filterComboBox->setCurrentIndex(0);
73 connect(m_ui->groupListWidget, &QListWidget::itemSelectionChanged,
this,
74 [
this] { updateStatus(); });
75 connect(m_translator.get(), &MachineTranslator::batchTranslated,
this,
76 &MachineTranslationDialog::onBatchTranslated);
77 connect(m_translator.get(), &MachineTranslator::translationFailed,
this,
78 &MachineTranslationDialog::onTranslationFailed);
79 connect(m_ui->doneButton, &QPushButton::clicked,
this, [
this] {
80 if (discardTranslations())
83 connect(m_ui->cancelButton, &QPushButton::clicked,
this, [
this] {
84 if (discardTranslations())
87 connect(m_ui->applyButton, &QPushButton::clicked,
this,
90 connect(m_ui->stopButton, &QPushButton::clicked,
this, &MachineTranslationDialog::stop);
91 connect(m_ui->connectButton, &QPushButton::clicked,
this,
93 connect(m_translator.get(), &MachineTranslator::modelsReceived,
this,
94 &MachineTranslationDialog::onModelsReceived);
95 connect(m_ui->modelComboBox, &QComboBox::currentTextChanged,
this, [](
const QString &text) {
97 QSettings().setValue(settingPath(selectedModelSettingsKey), text);
100 connect(m_ui->filterComboBox, &QComboBox::currentIndexChanged,
this,
102 connect(m_ui->serverText, &QLineEdit::textChanged,
this,
104 connect(m_ui->apiKeyEdit, &QLineEdit::textChanged,
this, [
this] {
105 if (m_connectionState == ConnectionState::Connected)
106 setConnectionState(ConnectionState::Modified);
108 connect(m_ui->apiTypeComboBox, &QComboBox::currentIndexChanged,
this,
110 connect(m_ui->logButton, &QPushButton::clicked,
this, [
this] {
111 if (m_ui->logButton->isChecked())
112 connect(m_translator.get(), &MachineTranslator::debugLog,
this,
113 &MachineTranslationDialog::onNewDebugMessage);
115 disconnect(m_translator.get(), &MachineTranslator::debugLog,
this,
116 &MachineTranslationDialog::onNewDebugMessage);
118 connect(m_ui->nextButton, &QPushButton::clicked,
this,
119 [
this] { m_ui->toolBox->setCurrentIndex(1); });
121 setConnectionState(ConnectionState::NotConnected);
124 m_ui->apiTypeComboBox->clear();
125 for (
const auto &info : s_apiTypes) {
126 m_ui->apiTypeComboBox->addItem(QString::fromLatin1(info.displayName),
127 QVariant::fromValue(info.type));
132 const int savedApiType = config.value(settingPath(selectedApiTypeSettingsKey),
135 for (
int i = 0; i < m_ui->apiTypeComboBox->count(); ++i) {
136 if (m_ui->apiTypeComboBox->itemData(i).toInt() == savedApiType) {
137 m_ui->apiTypeComboBox->setCurrentIndex(i);
143 loadAdvancedSettings();
144 validateAdvancedSettings();
146 connect(m_ui->advancedSettingsToggle, &QPushButton::toggled,
this,
148 connect(m_ui->applySettingsButton, &QPushButton::clicked,
this,
150 connect(m_ui->resetSettingsButton, &QPushButton::clicked,
this,
154 connect(m_ui->maxRetriesSpinBox, &QSpinBox::valueChanged,
this,
156 connect(m_ui->maxJsonFormatTriesSpinBox, &QSpinBox::valueChanged,
this,
160 connect(m_ui->toolBox, &QToolBox::currentChanged,
this,
161 [
this](
int) { m_ui->advancedSettingsToggle->setChecked(
false); });
162 connect(
this, &QDialog::finished,
this,
163 [
this] { m_ui->advancedSettingsToggle->setChecked(
false); });
168 const int count = m_ui->toolBox->count();
169 Q_ASSERT(
unsigned(count) == toolBoxTexts.size());
170 const int index = m_ui->toolBox->currentIndex();
172 for (
int i = 0; i < count; ++i) {
173 const QString baseText = MachineTranslationDialog::tr(toolBoxTexts[i]);
174 m_ui->toolBox->setItemText(i, (i == index ?
"- "_L1 :
"+ "_L1) + baseText);
187 m_ui->toolBox->setCurrentIndex(0);
188 m_ui->filesComboBox->clear();
189 m_ui->filesComboBox->addItems(m_dataModel->srcFileNames());
190 m_ui->filesComboBox->setCurrentIndex(0);
191 m_ui->translationLog->setText(tr(
"Translation Log"));
192 m_ui->translateButton->setEnabled(
true);
193 m_ui->stopButton->setEnabled(
false);
197 m_failedTranslations = 0;
198 m_receivedTranslations.clear();
199 m_ongoingTranslations.clear();
200 m_ui->applyButton->setEnabled(
false);
201 m_ui->progressBar->setVisible(
false);
202 m_translator->start();
207 const qsizetype receivedCount = m_receivedTranslations.size();
208 m_ui->statusLabel->setText(
209 tr(
"Translation status: %1/%2 source texts translated, %3/%2 failed.")
212 .arg(m_failedTranslations));
214 m_sentTexts > 0 ? (receivedCount + m_failedTranslations) * 100 / m_sentTexts : 0;
215 m_ui->progressBar->setValue(progress);
216 if (!table.empty()) {
217 QString html =
"<hr/><table cellpadding=\"4\""
222 for (
const QStringList &row : table) {
224 for (
const QString &col : row)
225 html +=
"<td>%1</td>"_L1.arg(col);
228 html +=
"</table>"_L1;
229 m_ui->translationLog->append(html);
232 if (receivedCount + m_failedTranslations == m_sentTexts) {
233 m_ui->translationLog->append(
234 tr(
"<hr/><b>Translation completed: %1/%2 translated, %3/%2 failed.</b>")
237 .arg(m_failedTranslations));
238 m_ui->translateButton->setEnabled(
true);
239 m_ui->stopButton->setEnabled(
false);
240 m_ui->applyButton->setEnabled(
true);
241 m_ui->progressBar->setVisible(
false);
243 m_ui->translateButton->setEnabled(
false);
244 m_ui->stopButton->setEnabled(
true);
245 m_ui->progressBar->setVisible(
true);
251 m_ui->translationLog->append(
"<hr/>"_L1);
252 m_ui->translationLog->append(info);
257 m_ui->translationLog->append(
258 "<span style=\"color:orange;\">%1</span>"_L1.arg(warning.toHtmlEscaped()));
263 m_ui->translationLog->append(
"<hr/>"_L1);
264 m_ui->translationLog->append(
265 "<span style=\"color:red; font-weight: bold; \">%1</span>"_L1.arg(error));
270 return (m_receivedTranslations.empty()
271 || QMessageBox::warning(
272 this, tr(
"Qt Linguist"),
273 tr(
"%n translated item(s) will be discarded. Continue?", 0,
274 m_receivedTranslations.size()),
275 QMessageBox::Yes | QMessageBox::No)
276 == QMessageBox::Yes);
281 m_translator->stop();
282 m_ui->stopButton->setEnabled(
false);
283 m_ui->translateButton->setEnabled(
true);
285 m_failedTranslations = 0;
286 m_ongoingTranslations.clear();
287 m_translator->start();
288 m_ui->applyButton->setEnabled(!m_receivedTranslations.empty());
289 m_ui->progressBar->setVisible(
false);
290 logError(tr(
"Translation Stopped."));
295 m_ui->toolBox->setCurrentIndex(2);
296 const QString model = m_ui->modelComboBox->currentText();
297 const int id = m_ui->filesComboBox->currentIndex();
298 if (model.isEmpty()) {
299 logError(tr(
"Please verify the service URL is valid "
300 "and a translation model is selected."));
304 logError(tr(
"Please select a file for translation."));
307 if (!discardTranslations())
311 const int filter = m_ui->filterComboBox->currentIndex();
315 QMutexLocker lock(&m_mutex);
318 if (tm->translation().isEmpty()) {
319 messages.items.append(tm);
320 m_ongoingTranslations[tm] =
321 MultiDataIndex{ it.translationType(), id, it.group(), it.message() };
326 if (tm->translation().isEmpty()) {
327 messages.items.append(tm);
328 m_ongoingTranslations[tm] =
329 MultiDataIndex{ it.translationType(), id, it.group(), it.message() };
333 const QList<QListWidgetItem *> selectedItems = m_ui->groupListWidget->selectedItems();
335 if (selectedItems.isEmpty()) {
336 logError(tr(
"Please select at least one context/label to translate."));
340 QMutexLocker lock(&m_mutex);
342 for (QListWidgetItem *item : selectedItems) {
343 const int groupIdx = item->data(Qt::UserRole).toInt();
344 const GroupItem *g = dm->groupItem(groupIdx, type);
345 for (
int i = 0; i < g->messageCount(); i++) {
346 const TranslatorMessage *tm = &g->messageItem(i)->message();
347 if (tm->translation().isEmpty()) {
348 messages.items.append(tm);
349 m_ongoingTranslations[tm] = MultiDataIndex{ type, id, groupIdx, i };
355 if (messages.items.isEmpty()) {
356 logInfo(tr(
"No items to translate. All selected messages already have translations."));
360 messages.srcLang = QLocale::languageToString(dm->sourceLanguage());
361 messages.tgtLang = QLocale::languageToString(dm->language());
363 m_sentTexts += messages.items.size();
364 m_translator->activateTranslationModel(model);
365 m_translator->translate(messages, m_ui->contextEdit->toPlainText().trimmed());
366 logInfo(tr(
"Translation Started"));
371 QHash<
const TranslatorMessage *, QStringList> translations)
373 QList<QStringList> log;
374 QList<QString> warnings;
375 log.reserve(translations.size());
376 QMutexLocker lock(&m_mutex);
378 const int id = m_ui->filesComboBox->currentIndex();
379 const int expectedForms = m_dataModel
->model(id
)->numerusForms().size();
381 for (
const auto &[msg, translationList] : translations.asKeyValueRange()) {
382 const QString displayTranslation = translationList.size() == 1
383 ? translationList.first().simplified()
384 : translationList.join(
" | "_L1);
385 log.append({ msg->sourceText().simplified(), displayTranslation });
386 if (msg->isPlural() && translationList.size() != expectedForms)
387 warnings.append(tr(
"Plural count expected %1, got %2 for \"%3\".")
389 .arg(translationList.size())
390 .arg(msg->sourceText()));
391 m_receivedTranslations.append(
392 std::make_pair(m_ongoingTranslations.take(msg), translationList));
394 logInfo(tr(
"Translation Batch:"));
396 for (
const QString &warning : std::as_const(warnings))
402 const QString color = isDarkMode() ?
"yellow"_L1 :
"orange"_L1;
403 const QString from = fromLlm ?
"LLM:"_L1 :
"Qt Linguist:"_L1;
405 "<p style=\"color:red; font-weight:bold; margin:0;\">%1</p>"
406 "<p style=\"color:%2; font-weight:normal; font-size:small; margin:0;\">%3</p>"
407 "<hr/>"_L1.arg(from, color, QString::fromUtf8(message).toHtmlEscaped());
408 m_ui->translationLog->append(log);
413 m_ui->groupLabel->setEnabled(id != 0);
414 m_ui->groupListWidget->setEnabled(id != 0);
415 m_ui->groupListWidget->clear();
416 int modelId = m_ui->filesComboBox->currentIndex();
420 QList<QPair<QString,
int>> groupsWithIndices;
423 groupsWithIndices.append(
425 }
else if (id == 2) {
427 groupsWithIndices.append(
431 std::sort(groupsWithIndices.begin(), groupsWithIndices.end(),
432 [](
const QPair<QString,
int> &a,
const QPair<QString,
int> &b) {
433 return a.first.compare(b.first, Qt::CaseInsensitive) < 0;
436 for (
const auto &group : groupsWithIndices) {
437 QListWidgetItem *item =
new QListWidgetItem(group.first);
438 item->setData(Qt::UserRole, group.second);
439 m_ui->groupListWidget->addItem(item);
445 QMutexLocker lock(&m_mutex);
446 for (
const auto &[item, translations] : std::as_const(m_receivedTranslations))
447 m_dataModel->setTranslations(item, translations);
449 logInfo(tr(
"Translations Applied."));
455 QList<QStringList> log;
456 log.reserve(failed.size() + 1);
458 QMutexLocker lock(&m_mutex);
459 m_failedTranslations += failed.size();
460 for (
const TranslatorMessage *m : std::as_const(failed)) {
461 log << QStringList{ m->sourceText().simplified() };
462 m_ongoingTranslations.remove(m);
464 logError(tr(
"Failed Translation(s):"));
470 const int model = m_ui->filesComboBox->currentIndex();
471 const int filter = m_ui->filterComboBox->currentIndex();
473 QList<QListWidgetItem *> selectedItems;
475 selectedItems = m_ui->groupListWidget->selectedItems();
477 if (model < 0 || filter < 0 || (filter > 0 && selectedItems.isEmpty())) {
479 m_ui->selectionLabel->setText(tr(
"Selection status: -"));
480 }
else if (filter == 0) {
489 m_ui->selectionLabel->setText(tr(
"Selected %n item(s).", 0, count));
490 }
else if (!selectedItems.isEmpty()) {
493 for (QListWidgetItem *item : std::as_const(selectedItems)) {
494 const int groupIdx = item->data(Qt::UserRole).toInt();
495 const GroupItem *g = m_dataModel->model(model)->groupItem(groupIdx, type);
496 for (
int i = 0; i < g->messageCount(); i++)
497 if (g->messageItem(i)->message().translation().isEmpty())
500 m_ui->selectionLabel->setText(
501 tr(
"Selected %n item(s) in %1 group(s).", 0, count).arg(selectedItems.size()));
507 if (m_ui->serverText->text().isEmpty()) {
508 setConnectionState(ConnectionState::NotConnected);
511 setConnectionState(ConnectionState::Connecting);
512 m_translator->setUrl(m_ui->serverText->text());
513 m_translator->setApiKey(m_ui->apiKeyEdit->text());
514 m_translator->requestModels();
519 const QVariant data = m_ui->apiTypeComboBox->itemData(index);
524 const ApiTypeInfo *info = findApiTypeInfo(apiType);
529 config.setValue(settingPath(selectedApiTypeSettingsKey),
static_cast<
int>(apiType));
531 m_translator->setApiType(apiType);
532 m_ui->serverText->setText(QString::fromLatin1(info->defaultUrl));
533 m_ui->modelComboBox->clear();
536 m_ui->apiKeyLabel->setVisible(info->showApiKeyField);
537 m_ui->apiKeyEdit->setVisible(info->showApiKeyField);
544 if (m_connectionState == ConnectionState::Connected
545 && m_ui->serverText->text() != m_lastConnectedUrl) {
546 setConnectionState(ConnectionState::Modified);
552 if (models.isEmpty()) {
553 setConnectionState(ConnectionState::Failed);
555 m_lastConnectedUrl = m_ui->serverText->text();
556 setConnectionState(ConnectionState::Connected);
559 QString savedModel = config.value(settingPath(selectedModelSettingsKey)).toString();
560 m_ui->modelComboBox->clear();
561 m_ui->modelComboBox->addItems(models);
564 if (!savedModel.isEmpty()) {
565 int index = m_ui->modelComboBox->findText(savedModel);
567 m_ui->modelComboBox->setCurrentIndex(index);
574 m_connectionState = state;
575 updateConnectionIndicator();
583 switch (m_connectionState) {
584 case ConnectionState::NotConnected:
585 statusText = tr(
"Not connected - click \"Connect\" to fetch models");
586 styleSheet =
"QLabel { color: gray; }"_L1;
588 case ConnectionState::Connecting:
589 statusText = tr(
"Connecting...");
590 styleSheet =
"QLabel { color: orange; }"_L1;
592 case ConnectionState::Connected:
593 statusText = tr(
"Connected");
594 styleSheet =
"QLabel { color: green;}"_L1;
596 case ConnectionState::Failed:
597 statusText = tr(
"Connection failed - verify server URL and click \"Connect\"");
598 styleSheet =
"QLabel { color: red; }"_L1;
600 case ConnectionState::Modified:
601 statusText = tr(
"URL modified - click \"Connect\" to apply");
602 styleSheet =
"QLabel { color: orange; }"_L1;
606 m_ui->connectionStatusLabel->setText(statusText);
607 m_ui->connectionStatusLabel->setStyleSheet(styleSheet);
612 m_ui->advancedSettingsWidget->setVisible(checked);
613 m_ui->advancedSettingsToggle->setText(checked ? tr(
"Advanced Settings \xe2\x96\xbc")
614 : tr(
"Advanced Settings \xe2\x96\xb6"));
616 loadAdvancedSettings();
617 validateAdvancedSettings();
623 m_ui->maxRetriesSpinBox->setValue(TranslationSettings::maxRetries());
624 m_ui->maxConcurrentBatchesSpinBox->setValue(TranslationSettings::maxConcurrentBatches());
625 m_ui->transferTimeoutSpinBox->setValue(TranslationSettings::transferTimeoutMs() / 1000);
626 m_ui->maxBatchSizeSpinBox->setValue(TranslationSettings::maxBatchSize());
627 m_ui->temperatureSpinBox->setValue(TranslationSettings::temperature());
628 m_ui->maxJsonFormatTriesSpinBox->setValue(TranslationSettings::maxJsonFormatTries());
629 m_ui->ollamaWakeUpTimeoutSpinBox->setValue(TranslationSettings::ollamaWakeUpTimeoutMs() / 1000);
646 loadAdvancedSettings();
647 validateAdvancedSettings();
652 const int maxRetries = m_ui->maxRetriesSpinBox->value();
653 const int maxJsonFormatTries = m_ui->maxJsonFormatTriesSpinBox->value();
655 QStringList warnings;
658 if (maxJsonFormatTries < 3) {
661 warnings << tr(
"\xe2\x9a\xa0 Maximum JSON Format Tries: Low value may cause unnecessary "
662 "format switching due to temporary errors. Recommended: 3 or higher.");
667 if (maxRetries < maxJsonFormatTries * 3) {
671 warnings << tr(
"\xe2\x9a\xa0 Maximum Retries: Should be at least 3\xc3\x97 'Maximum JSON "
672 "Format Tries' for full fallback coverage");
675 m_ui->settingsWarningLabel->setText(warnings.join(u'\n'));
DataModelIterator(TranslationType type, const DataModel *model=0, int groupNo=0, int messageNo=0)
MessageItem * current() const
GroupItem * groupItem(int index, TranslationType type) const
~MachineTranslationDialog()
void setDataModel(MultiDataModel *dm)
const TranslatorMessage & message() const
static void setMaxJsonFormatTries(int value)
static void setMaxBatchSize(int value)
static void setTemperature(double value)
static void setMaxConcurrentBatches(int value)
static void setTransferTimeoutMs(int value)
static void setMaxRetries(int value)
static void setOllamaWakeUpTimeoutMs(int value)
static void resetToDefaults()