Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
machinetranslationdialog.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
5#include "ui_machinetranslationdialog.h"
6
7#include "messagemodel.h"
8#include "auto-translation/machinetranslator.h"
9#include "globals.h"
10
11#include <QtCore/qsettings.h>
12#include <QtWidgets/qmessagebox.h>
13
14using namespace Qt::Literals::StringLiterals;
15
16QT_BEGIN_NAMESPACE
17
18MachineTranslationDialog::MachineTranslationDialog(QWidget *parent)
19 : QDialog(parent),
20 m_ui(std::make_unique<Ui::MachineTranslationDialog>()),
21 m_translator(std::make_unique<MachineTranslator>())
22{
23 m_ui->setupUi(this);
24
25 connect(m_ui->toolBox, &QToolBox::currentChanged, this, [this](int index) {
26 for (int i = 0; i < m_ui->toolBox->count(); ++i) {
27 const QString baseText = m_ui->toolBox->itemText(i).mid(2);
28 m_ui->toolBox->setItemText(i, (i == index ? "- "_L1 : "+ "_L1) + baseText);
29 }
30 });
31
32 m_ui->statusLabel->setWordWrap(true);
33 m_ui->statusLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
34 connect(m_ui->translateButton, &QPushButton::clicked, this,
35 &MachineTranslationDialog::translateSelection);
36 connect(m_ui->filesComboBox, &QComboBox::currentIndexChanged, this, [this] {
37 m_ui->filterComboBox->setCurrentIndex(0);
38 updateStatus();
39 });
40 connect(m_ui->groupListWidget, &QListWidget::itemSelectionChanged, this,
41 [this] { updateStatus(); });
42 connect(m_translator.get(), &MachineTranslator::batchTranslated, this,
43 &MachineTranslationDialog::onBatchTranslated);
44 connect(m_translator.get(), &MachineTranslator::translationFailed, this,
45 &MachineTranslationDialog::onTranslationFailed);
46 connect(m_ui->doneButton, &QPushButton::clicked, this, [this] {
47 if (discardTranslations())
48 accept();
49 });
50 connect(m_ui->cancelButton, &QPushButton::clicked, this, [this] {
51 if (discardTranslations())
52 reject();
53 });
54 connect(m_ui->applyButton, &QPushButton::clicked, this,
55 &MachineTranslationDialog::applyTranslations);
56
57 connect(m_ui->stopButton, &QToolButton::clicked, this, &MachineTranslationDialog::stop);
58 connect(m_ui->connectButton, &QPushButton::clicked, this,
59 &MachineTranslationDialog::connectToOllama);
60 connect(m_translator.get(), &MachineTranslator::modelsReceived, this,
61 [this](const QStringList &models) {
62 QSettings config;
63 QString savedModel = config.value(settingPath(selectedModelSettingsKey)).toString();
64 m_ui->modelComboBox->clear();
65 m_ui->modelComboBox->addItems(models);
66
67 // Restore saved selection if found
68 if (!savedModel.isEmpty()) {
69 int index = m_ui->modelComboBox->findText(savedModel);
70 if (index >= 0)
71 m_ui->modelComboBox->setCurrentIndex(index);
72 }
73 });
74 connect(m_ui->modelComboBox, &QComboBox::currentTextChanged, this, [](const QString &text) {
75 if (!text.isEmpty())
76 QSettings().setValue(settingPath(selectedModelSettingsKey), text);
77 });
78 connect(this, &QDialog::finished, m_translator.get(), &MachineTranslator::stop);
79 connect(m_ui->filterComboBox, &QComboBox::currentIndexChanged, this,
80 &MachineTranslationDialog::onFilterChanged);
81}
82
84{
85 m_dataModel = dm;
86 refresh(true);
87}
88
89void MachineTranslationDialog::refresh(bool init)
90{
91 if (init) {
92 m_ui->toolBox->setCurrentIndex(0);
93 m_ui->filesComboBox->clear();
94 m_ui->filesComboBox->addItems(m_dataModel->srcFileNames());
95 m_ui->filesComboBox->setCurrentIndex(0);
96 m_ui->translationLog->setText(tr("Translation Log"));
97 m_ui->translateButton->setEnabled(true);
98 m_ui->stopButton->setEnabled(false);
99 connectToOllama();
100 }
101 m_sentTexts = 0;
102 m_failedTranslations = 0;
103 m_receivedTranslations.clear();
104 m_ongoingTranslations.clear();
105 m_ui->applyButton->setEnabled(false);
106 m_ui->progressBar->setVisible(false);
107 m_translator->start();
108}
109
110void MachineTranslationDialog::logProgress(const QList<QStringList> &table)
111{
112 const qsizetype receivedCount = m_receivedTranslations.size();
113 m_ui->statusLabel->setText(
114 tr("Translation status: %1/%2 source texts translated, %3/%2 failed.")
115 .arg(receivedCount)
116 .arg(m_sentTexts)
117 .arg(m_failedTranslations));
118 m_ui->progressBar->setValue((receivedCount + m_failedTranslations) * 100 / m_sentTexts);
119 if (!table.empty()) {
120 QString html = "<hr/><table cellpadding=\"4\""
121 "style=\""
122 "width:100%; "
123 "margin-left:10px; "
124 "\">"_L1;
125 for (const QStringList &row : table) {
126 html += "<tr>"_L1;
127 for (const QString &col : row)
128 html += "<td>%1</td>"_L1.arg(col);
129 html += "</tr>"_L1;
130 }
131 html += "</table>"_L1;
132 m_ui->translationLog->append(html);
133 }
134
135 if (receivedCount + m_failedTranslations == m_sentTexts) {
136 m_ui->translationLog->append(
137 tr("<hr/><b>Translation completed: %1/%2 translated, %3/%2 failed.</b>")
138 .arg(receivedCount)
139 .arg(m_sentTexts)
140 .arg(m_failedTranslations));
141 m_ui->translateButton->setEnabled(true);
142 m_ui->stopButton->setEnabled(false);
143 m_ui->applyButton->setEnabled(true);
144 m_ui->progressBar->setVisible(false);
145 } else {
146 m_ui->translateButton->setEnabled(false);
147 m_ui->stopButton->setEnabled(true);
148 m_ui->progressBar->setVisible(true);
149 }
150}
151
152void MachineTranslationDialog::logInfo(const QString &info)
153{
154 m_ui->translationLog->append("<hr/>"_L1);
155 m_ui->translationLog->append(info);
156}
157
158void MachineTranslationDialog::logError(const QString &error)
159{
160 m_ui->translationLog->append("<hr/>"_L1);
161 m_ui->translationLog->append(
162 "<span style=\"color:red; font-weight: bold; \">%1</span>"_L1.arg(error));
163}
164
165bool MachineTranslationDialog::discardTranslations()
166{
167 return (m_receivedTranslations.empty()
168 || QMessageBox::warning(
169 this, tr("Qt Linguist"),
170 tr("The already %n translated item(s) will be discarded. Continue?", 0,
171 m_receivedTranslations.size()),
172 QMessageBox::Yes | QMessageBox::No)
173 == QMessageBox::Yes);
174}
175
176void MachineTranslationDialog::stop()
177{
178 m_translator->stop();
179 m_ui->stopButton->setEnabled(false);
180 m_ui->translateButton->setEnabled(true);
181 refresh(false);
182 logError(tr("Translation Stopped."));
183}
184
185void MachineTranslationDialog::translateSelection()
186{
187 m_ui->toolBox->setCurrentIndex(2);
188 const QString model = m_ui->modelComboBox->currentText();
189 const int id = m_ui->filesComboBox->currentIndex();
190 if (model.isEmpty()) {
191 logError(tr("Please verify the service URL is valid, "
192 "then select a translation model."));
193 return;
194 }
195 if (id < 0) {
196 logError(tr("Please select a file for translation."));
197 return;
198 }
199 if (!discardTranslations())
200 return;
201 refresh(false);
202
203 const int filter = m_ui->filterComboBox->currentIndex();
204 const DataModel *dm = m_dataModel->model(id);
205 Messages messages;
206 if (filter == 0) {
207 QMutexLocker lock(&m_mutex);
210 if (tm->translation().isEmpty()) {
211 messages.items.append(tm);
212 m_ongoingTranslations[tm] =
213 MultiDataIndex{ it.translationType(), id, it.group(), it.message() };
214 }
215 }
216 for (DataModelIterator it(IDBASED, dm); it.isValid(); ++it) {
218 if (tm->translation().isEmpty()) {
219 messages.items.append(tm);
220 m_ongoingTranslations[tm] =
221 MultiDataIndex{ it.translationType(), id, it.group(), it.message() };
222 }
223 }
224 } else {
225 const QList<QListWidgetItem *> selectedItems = m_ui->groupListWidget->selectedItems();
226
227 if (selectedItems.isEmpty()) {
228 logError(tr("Please select at least one context/label to translate."));
229 return;
230 }
231
232 QMutexLocker lock(&m_mutex);
233 const auto type = (filter == 1) ? TEXTBASED : IDBASED;
234 for (QListWidgetItem *item : selectedItems) {
235 const int groupIdx = item->data(Qt::UserRole).toInt();
236 const GroupItem *g = dm->groupItem(groupIdx, type);
237 for (int i = 0; i < g->messageCount(); i++) {
238 const TranslatorMessage *tm = &g->messageItem(i)->message();
239 if (tm->translation().isEmpty()) {
240 messages.items.append(tm);
241 m_ongoingTranslations[tm] = MultiDataIndex{ type, id, groupIdx, i };
242 }
243 }
244 }
245 }
246 messages.srcLang = QLocale::languageToString(dm->sourceLanguage());
247 messages.tgtLang = QLocale::languageToString(dm->language());
248 m_sentTexts += messages.items.size();
249 m_translator->activateTranslationModel(model);
250 m_translator->translate(messages, m_ui->contextEdit->toPlainText().trimmed());
251 logInfo(tr("Translation Started"));
252 logProgress({});
253}
254
255void MachineTranslationDialog::onBatchTranslated(
256 QHash<const TranslatorMessage *, QString> translations)
257{
258 QList<QStringList> log;
259 log.reserve(translations.size());
260 QMutexLocker lock(&m_mutex);
261 for (const auto &[msg, translation] : translations.asKeyValueRange()) {
262 log.append({ msg->sourceText().simplified(), translation.simplified() });
263 m_receivedTranslations.append(std::make_pair(m_ongoingTranslations.take(msg), translation));
264 }
265 logInfo(tr("Translation Batch:"));
266 logProgress(log);
267}
268
269void MachineTranslationDialog::onFilterChanged(int id)
270{
271 m_ui->groupLabel->setEnabled(id != 0);
272 m_ui->groupListWidget->setEnabled(id != 0);
273 m_ui->groupListWidget->clear();
274 int modelId = m_ui->filesComboBox->currentIndex();
275 if (modelId < 0)
276 return;
277
278 QList<QPair<QString, int>> groupsWithIndices;
279 if (id == 1) {
280 for (int i = 0; i < m_dataModel->model(modelId)->contextCount(); i++)
281 groupsWithIndices.append(
282 { m_dataModel->model(modelId)->groupItem(i, TEXTBASED)->group(), i });
283 } else if (id == 2) {
284 for (int i = 0; i < m_dataModel->model(modelId)->labelCount(); i++)
285 groupsWithIndices.append(
286 { m_dataModel->model(modelId)->groupItem(i, IDBASED)->group(), i });
287 }
288
289 std::sort(groupsWithIndices.begin(), groupsWithIndices.end(),
290 [](const QPair<QString, int> &a, const QPair<QString, int> &b) {
291 return a.first.compare(b.first, Qt::CaseInsensitive) < 0;
292 });
293
294 for (const auto &group : groupsWithIndices) {
295 QListWidgetItem *item = new QListWidgetItem(group.first);
296 item->setData(Qt::UserRole, group.second);
297 m_ui->groupListWidget->addItem(item);
298 }
299}
300
301void MachineTranslationDialog::applyTranslations()
302{
303 QMutexLocker lock(&m_mutex);
304 for (const auto &[item, translation] : std::as_const(m_receivedTranslations))
305 m_dataModel->setTranslation(item, translation);
306 refresh(false);
307 logInfo(tr("Translations Applied."));
308}
309
310void MachineTranslationDialog::onTranslationFailed(QList<const TranslatorMessage *> failed)
311{
312 QList<QStringList> log;
313 log.reserve(failed.size() + 1);
314
315 QMutexLocker lock(&m_mutex);
316 m_failedTranslations += failed.size();
317 for (const TranslatorMessage *m : failed) {
318 log << QStringList{ m->sourceText().simplified() };
319 m_ongoingTranslations.remove(m);
320 }
321 logError(tr("Failed Translation(s):"));
322 logProgress(log);
323}
324
325void MachineTranslationDialog::updateStatus()
326{
327 const int model = m_ui->filesComboBox->currentIndex();
328 const int filter = m_ui->filterComboBox->currentIndex();
329
330 QList<QListWidgetItem *> selectedItems;
331 if (filter > 0)
332 selectedItems = m_ui->groupListWidget->selectedItems();
333
334 if (model < 0 || filter < 0 || (filter > 0 && selectedItems.isEmpty())) {
335 m_ui->selectionLabel->setText(tr("Selection status: -"));
336 } else if (filter == 0) {
337 int count = 0;
338 for (DataModelIterator it(IDBASED, m_dataModel->model(model)); it.isValid(); ++it)
339 if (it.current()->translation().isEmpty())
340 count++;
341 for (DataModelIterator it(TEXTBASED, m_dataModel->model(model)); it.isValid(); ++it)
342 if (it.current()->translation().isEmpty())
343 count++;
344
345 m_ui->selectionLabel->setText(tr("Selected %n item(s).", 0, count));
346 } else if (!selectedItems.isEmpty()) {
347 const auto type = (filter == 1) ? TEXTBASED : IDBASED;
348 int count = 0;
349 for (QListWidgetItem *item : std::as_const(selectedItems)) {
350 const int groupIdx = item->data(Qt::UserRole).toInt();
351 const GroupItem *g = m_dataModel->model(model)->groupItem(groupIdx, type);
352 for (int i = 0; i < g->messageCount(); i++)
353 if (g->messageItem(i)->message().translation().isEmpty())
354 count++;
355 }
356 m_ui->selectionLabel->setText(
357 tr("Selected %n item(s) in %1 group(s).", 0, count).arg(selectedItems.size()));
358 }
359}
360
361void MachineTranslationDialog::connectToOllama()
362{
363 if (m_ui->serverText->text().isEmpty())
364 return;
365 m_translator->setUrl(m_ui->serverText->text());
366 m_translator->requestModels();
367}
368
370
371QT_END_NAMESPACE
bool isValid() const
DataModelIterator(TranslationType type, const DataModel *model=0, int groupNo=0, int messageNo=0)
MessageItem * current() const
GroupItem * groupItem(int index, TranslationType type) const
int labelCount() const
int contextCount() const
void setDataModel(MultiDataModel *dm)
const TranslatorMessage & message() const
DataModel * model(int i)
@ IDBASED
@ TEXTBASED