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();
145 toggleAdvancedSettings(
false);
147 connect(m_ui->advancedSettingsToggle, &QPushButton::toggled,
this,
149 connect(m_ui->applySettingsButton, &QPushButton::clicked,
this,
151 connect(m_ui->resetSettingsButton, &QPushButton::clicked,
this,
155 connect(m_ui->maxRetriesSpinBox, &QSpinBox::valueChanged,
this,
157 connect(m_ui->maxJsonFormatTriesSpinBox, &QSpinBox::valueChanged,
this,
161 connect(m_ui->toolBox, &QToolBox::currentChanged,
this,
162 [
this](
int) { m_ui->advancedSettingsToggle->setChecked(
false); });
163 connect(
this, &QDialog::finished,
this,
164 [
this] { m_ui->advancedSettingsToggle->setChecked(
false); });
169 const int count = m_ui->toolBox->count();
170 Q_ASSERT(
unsigned(count) == toolBoxTexts.size());
171 const int index = m_ui->toolBox->currentIndex();
173 for (
int i = 0; i < count; ++i) {
174 const QString baseText = MachineTranslationDialog::tr(toolBoxTexts[i]);
175 m_ui->toolBox->setItemText(i, (i == index ?
"- "_L1 :
"+ "_L1) + baseText);
188 m_ui->toolBox->setCurrentIndex(0);
189 m_ui->filesComboBox->clear();
190 m_ui->filesComboBox->addItems(m_dataModel->srcFileNames());
191 m_ui->filesComboBox->setCurrentIndex(0);
192 m_ui->translationLog->setText(tr(
"Translation Log"));
193 m_ui->translateButton->setEnabled(
true);
194 m_ui->stopButton->setEnabled(
false);
198 m_failedTranslations = 0;
199 m_receivedTranslations.clear();
200 m_ongoingTranslations.clear();
201 m_ui->applyButton->setEnabled(
false);
202 m_ui->progressBar->setVisible(
false);
203 m_translator->start();
208 const qsizetype receivedCount = m_receivedTranslations.size();
209 m_ui->statusLabel->setText(
210 tr(
"Translation status: %1/%2 source texts translated, %3/%2 failed.")
213 .arg(m_failedTranslations));
215 m_sentTexts > 0 ? (receivedCount + m_failedTranslations) * 100 / m_sentTexts : 0;
216 m_ui->progressBar->setValue(progress);
217 if (!table.empty()) {
218 QString html =
"<hr/><table cellpadding=\"4\""
223 for (
const QStringList &row : table) {
225 for (
const QString &col : row)
226 html +=
"<td>%1</td>"_L1.arg(col);
229 html +=
"</table>"_L1;
230 m_ui->translationLog->append(html);
233 if (receivedCount + m_failedTranslations == m_sentTexts) {
234 m_ui->translationLog->append(
235 tr(
"<hr/><b>Translation completed: %1/%2 translated, %3/%2 failed.</b>")
238 .arg(m_failedTranslations));
239 m_ui->translateButton->setEnabled(
true);
240 m_ui->stopButton->setEnabled(
false);
241 m_ui->applyButton->setEnabled(
true);
242 m_ui->progressBar->setVisible(
false);
244 m_ui->translateButton->setEnabled(
false);
245 m_ui->stopButton->setEnabled(
true);
246 m_ui->progressBar->setVisible(
true);
252 m_ui->translationLog->append(
"<hr/>"_L1);
253 m_ui->translationLog->append(info);
258 m_ui->translationLog->append(
259 "<span style=\"color:orange;\">%1</span>"_L1.arg(warning.toHtmlEscaped()));
264 m_ui->translationLog->append(
"<hr/>"_L1);
265 m_ui->translationLog->append(
266 "<span style=\"color:red; font-weight: bold; \">%1</span>"_L1.arg(error));
271 return (m_receivedTranslations.empty()
272 || QMessageBox::warning(
273 this, tr(
"Qt Linguist"),
274 tr(
"%n translated item(s) will be discarded. Continue?", 0,
275 m_receivedTranslations.size()),
276 QMessageBox::Yes | QMessageBox::No)
277 == QMessageBox::Yes);
282 m_translator->stop();
283 m_ui->stopButton->setEnabled(
false);
284 m_ui->translateButton->setEnabled(
true);
286 m_failedTranslations = 0;
287 m_ongoingTranslations.clear();
288 m_translator->start();
289 m_ui->applyButton->setEnabled(!m_receivedTranslations.empty());
290 m_ui->progressBar->setVisible(
false);
291 logError(tr(
"Translation Stopped."));
296 m_ui->toolBox->setCurrentIndex(2);
297 const QString model = m_ui->modelComboBox->currentText();
298 const int id = m_ui->filesComboBox->currentIndex();
299 if (model.isEmpty()) {
300 logError(tr(
"Please verify the service URL is valid "
301 "and a translation model is selected."));
305 logError(tr(
"Please select a file for translation."));
308 if (!discardTranslations())
312 const int filter = m_ui->filterComboBox->currentIndex();
316 QMutexLocker lock(&m_mutex);
319 if (tm->translation().isEmpty() && !tm->sourceText().isEmpty()) {
320 messages.items.append(tm);
321 m_ongoingTranslations[tm] =
322 MultiDataIndex{ it.translationType(), id, it.group(), it.message() };
327 if (tm->translation().isEmpty() && !tm->sourceText().isEmpty()) {
328 messages.items.append(tm);
329 m_ongoingTranslations[tm] =
330 MultiDataIndex{ it.translationType(), id, it.group(), it.message() };
334 const QList<QListWidgetItem *> selectedItems = m_ui->groupListWidget->selectedItems();
336 if (selectedItems.isEmpty()) {
337 logError(tr(
"Please select at least one context/label to translate."));
341 QMutexLocker lock(&m_mutex);
343 for (QListWidgetItem *item : selectedItems) {
344 const int groupIdx = item->data(Qt::UserRole).toInt();
345 const GroupItem *g = dm->groupItem(groupIdx, type);
346 for (
int i = 0; i < g->messageCount(); i++) {
347 const TranslatorMessage *tm = &g->messageItem(i)->message();
348 if (tm->translation().isEmpty() && !tm->sourceText().isEmpty()) {
349 messages.items.append(tm);
350 m_ongoingTranslations[tm] = MultiDataIndex{ type, id, groupIdx, i };
356 if (messages.items.isEmpty()) {
357 logInfo(tr(
"No items to translate. All selected messages already have translations."));
361 messages.srcLang = QLocale::languageToString(dm->sourceLanguage());
362 messages.tgtLang = QLocale::languageToString(dm->language());
364 m_sentTexts += messages.items.size();
365 m_translator->activateTranslationModel(model);
366 m_translator->translate(messages, m_ui->contextEdit->toPlainText().trimmed());
367 logInfo(tr(
"Translation Started"));
372 QHash<
const TranslatorMessage *, QStringList> translations)
374 QList<QStringList> log;
375 QList<QString> warnings;
376 log.reserve(translations.size());
377 QMutexLocker lock(&m_mutex);
379 const int id = m_ui->filesComboBox->currentIndex();
380 const int expectedForms = m_dataModel
->model(id
)->numerusForms().size();
382 for (
const auto &[msg, translationList] : translations.asKeyValueRange()) {
383 const QString displayTranslation = translationList.size() == 1
384 ? translationList.first().simplified()
385 : translationList.join(
" | "_L1);
386 log.append({ msg->sourceText().simplified(), displayTranslation });
387 if (msg->isPlural() && translationList.size() != expectedForms)
388 warnings.append(tr(
"Plural count expected %1, got %2 for \"%3\".")
390 .arg(translationList.size())
391 .arg(msg->sourceText()));
392 m_receivedTranslations.append(
393 std::make_pair(m_ongoingTranslations.take(msg), translationList));
395 logInfo(tr(
"Translation Batch:"));
397 for (
const QString &warning : std::as_const(warnings))
403 const QString color = isDarkMode() ?
"yellow"_L1 :
"orange"_L1;
404 const QString from = fromLlm ?
"LLM:"_L1 :
"Qt Linguist:"_L1;
406 "<p style=\"color:red; font-weight:bold; margin:0;\">%1</p>"
407 "<p style=\"color:%2; font-weight:normal; font-size:small; margin:0;\">%3</p>"
408 "<hr/>"_L1.arg(from, color, QString::fromUtf8(message).toHtmlEscaped());
409 m_ui->translationLog->append(log);
414 m_ui->groupLabel->setEnabled(id != 0);
415 m_ui->groupListWidget->setEnabled(id != 0);
416 m_ui->groupListWidget->clear();
417 int modelId = m_ui->filesComboBox->currentIndex();
421 QList<QPair<QString,
int>> groupsWithIndices;
424 groupsWithIndices.append(
426 }
else if (id == 2) {
428 groupsWithIndices.append(
432 std::sort(groupsWithIndices.begin(), groupsWithIndices.end(),
433 [](
const QPair<QString,
int> &a,
const QPair<QString,
int> &b) {
434 return a.first.compare(b.first, Qt::CaseInsensitive) < 0;
437 for (
const auto &group : groupsWithIndices) {
438 QListWidgetItem *item =
new QListWidgetItem(group.first);
439 item->setData(Qt::UserRole, group.second);
440 m_ui->groupListWidget->addItem(item);
446 QMutexLocker lock(&m_mutex);
447 for (
const auto &[item, translations] : std::as_const(m_receivedTranslations))
448 m_dataModel->setTranslations(item, translations);
450 logInfo(tr(
"Translations Applied."));
456 QList<QStringList> log;
457 log.reserve(failed.size() + 1);
459 QMutexLocker lock(&m_mutex);
460 m_failedTranslations += failed.size();
461 for (
const TranslatorMessage *m : std::as_const(failed)) {
462 log << QStringList{ m->sourceText().simplified() };
463 m_ongoingTranslations.remove(m);
465 logError(tr(
"Failed Translation(s):"));
471 const int model = m_ui->filesComboBox->currentIndex();
472 const int filter = m_ui->filterComboBox->currentIndex();
474 QList<QListWidgetItem *> selectedItems;
476 selectedItems = m_ui->groupListWidget->selectedItems();
478 if (model < 0 || filter < 0 || (filter > 0 && selectedItems.isEmpty())) {
480 m_ui->selectionLabel->setText(tr(
"Selection status: -"));
481 }
else if (filter == 0) {
492 m_ui->selectionLabel->setText(tr(
"Selected %n item(s).", 0, count));
493 }
else if (!selectedItems.isEmpty()) {
496 for (QListWidgetItem *item : std::as_const(selectedItems)) {
497 const int groupIdx = item->data(Qt::UserRole).toInt();
498 const GroupItem *g = m_dataModel->model(model)->groupItem(groupIdx, type);
499 for (
int i = 0; i < g->messageCount(); i++)
500 if (g->messageItem(i)->message().translation().isEmpty()
501 && !g->messageItem(i)->message().sourceText().isEmpty())
504 m_ui->selectionLabel->setText(
505 tr(
"Selected %n item(s) in %1 group(s).", 0, count).arg(selectedItems.size()));
511 if (m_ui->serverText->text().isEmpty()) {
512 setConnectionState(ConnectionState::NotConnected);
515 setConnectionState(ConnectionState::Connecting);
516 m_translator->setUrl(m_ui->serverText->text());
517 m_translator->setApiKey(m_ui->apiKeyEdit->text());
518 m_translator->requestModels();
523 const QVariant data = m_ui->apiTypeComboBox->itemData(index);
528 const ApiTypeInfo *info = findApiTypeInfo(apiType);
533 config.setValue(settingPath(selectedApiTypeSettingsKey),
static_cast<
int>(apiType));
535 m_translator->setApiType(apiType);
536 m_ui->serverText->setText(QString::fromLatin1(info->defaultUrl));
537 m_ui->modelComboBox->clear();
540 m_ui->apiKeyLabel->setVisible(info->showApiKeyField);
541 m_ui->apiKeyEdit->setVisible(info->showApiKeyField);
548 if (m_connectionState == ConnectionState::Connected
549 && m_ui->serverText->text() != m_lastConnectedUrl) {
550 setConnectionState(ConnectionState::Modified);
556 if (models.isEmpty()) {
557 setConnectionState(ConnectionState::Failed);
559 m_lastConnectedUrl = m_ui->serverText->text();
560 setConnectionState(ConnectionState::Connected);
563 QString savedModel = config.value(settingPath(selectedModelSettingsKey)).toString();
564 m_ui->modelComboBox->clear();
565 m_ui->modelComboBox->addItems(models);
568 if (!savedModel.isEmpty()) {
569 int index = m_ui->modelComboBox->findText(savedModel);
571 m_ui->modelComboBox->setCurrentIndex(index);
578 m_connectionState = state;
579 updateConnectionIndicator();
587 switch (m_connectionState) {
588 case ConnectionState::NotConnected:
589 statusText = tr(
"Not connected - click \"Connect\" to fetch models");
590 styleSheet =
"QLabel { color: gray; }"_L1;
592 case ConnectionState::Connecting:
593 statusText = tr(
"Connecting...");
594 styleSheet =
"QLabel { color: orange; }"_L1;
596 case ConnectionState::Connected:
597 statusText = tr(
"Connected");
598 styleSheet =
"QLabel { color: green;}"_L1;
600 case ConnectionState::Failed:
601 statusText = tr(
"Connection failed - verify server URL and click \"Connect\"");
602 styleSheet =
"QLabel { color: red; }"_L1;
604 case ConnectionState::Modified:
605 statusText = tr(
"URL modified - click \"Connect\" to apply");
606 styleSheet =
"QLabel { color: orange; }"_L1;
610 m_ui->connectionStatusLabel->setText(statusText);
611 m_ui->connectionStatusLabel->setStyleSheet(styleSheet);
616 m_ui->advancedSettingsWidget->setVisible(checked);
617 m_ui->advancedSettingsToggle->setText(tr(
"Advanced Settings") + (checked ?
" -"_L1 :
" +"_L1));
619 loadAdvancedSettings();
620 validateAdvancedSettings();
626 m_ui->maxRetriesSpinBox->setValue(TranslationSettings::maxRetries());
627 m_ui->maxConcurrentBatchesSpinBox->setValue(TranslationSettings::maxConcurrentBatches());
628 m_ui->transferTimeoutSpinBox->setValue(TranslationSettings::transferTimeoutMs() / 1000);
629 m_ui->maxBatchSizeSpinBox->setValue(TranslationSettings::maxBatchSize());
630 m_ui->temperatureSpinBox->setValue(TranslationSettings::temperature());
631 m_ui->maxJsonFormatTriesSpinBox->setValue(TranslationSettings::maxJsonFormatTries());
632 m_ui->ollamaWakeUpTimeoutSpinBox->setValue(TranslationSettings::ollamaWakeUpTimeoutMs() / 1000);
649 loadAdvancedSettings();
650 validateAdvancedSettings();
655 const int maxRetries = m_ui->maxRetriesSpinBox->value();
656 const int maxJsonFormatTries = m_ui->maxJsonFormatTriesSpinBox->value();
658 QStringList warnings;
661 if (maxJsonFormatTries < 3) {
664 warnings << tr(
"Warning: Maximum JSON Format Tries: Low value may cause unnecessary "
665 "format switching due to temporary errors. Recommended: 3 or higher.");
670 if (maxRetries < maxJsonFormatTries * 3) {
674 warnings << tr(
"Warning: Maximum Retries: Should be at least 3x 'Maximum JSON "
675 "Format Tries' for full fallback coverage");
678 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()