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
10#include <QtWidgets/qmessagebox.h>
11
12using namespace Qt::Literals::StringLiterals;
13
14QT_BEGIN_NAMESPACE
15
16MachineTranslationDialog::MachineTranslationDialog(QWidget *parent)
17 : QDialog(parent),
18 m_ui(std::make_unique<Ui::MachineTranslationDialog>()),
19 m_translator(std::make_unique<MachineTranslator>())
20{
21 m_ui->setupUi(this);
22 m_ui->statusLabel->setWordWrap(true);
23 m_ui->statusLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
24 connect(m_ui->translateButton, &QPushButton::clicked, this,
25 &MachineTranslationDialog::translateSelection);
26 connect(m_ui->filesComboBox, &QComboBox::currentIndexChanged, this, [this] {
27 m_ui->filterComboBox->setCurrentIndex(0);
28 updateStatus();
29 });
30 connect(m_translator.get(), &MachineTranslator::batchTranslated, this,
31 &MachineTranslationDialog::onBatchTranslated);
32 connect(m_translator.get(), &MachineTranslator::translationFailed, this,
33 &MachineTranslationDialog::onTranslationFailed);
34 connect(m_ui->groupComboBox, &QComboBox::currentIndexChanged, this, [this] { updateStatus(); });
35 connect(m_ui->doneButton, &QPushButton::clicked, this, [this] {
36 if (discardTranslations())
37 accept();
38 });
39 connect(m_ui->cancelButton, &QPushButton::clicked, this, [this] {
40 if (discardTranslations())
41 reject();
42 });
43 connect(m_ui->applyButton, &QPushButton::clicked, this,
44 &MachineTranslationDialog::applyTranslations);
45
46 connect(m_ui->stopButton, &QToolButton::clicked, this, &MachineTranslationDialog::stop);
47 connect(m_ui->connectButton, &QPushButton::clicked, this,
48 &MachineTranslationDialog::connectToOllama);
49 connect(m_translator.get(), &MachineTranslator::modelsReceived, this,
50 [this](const QStringList &models) {
51 m_ui->modelComboBox->clear();
52 m_ui->modelComboBox->addItems(models);
53 });
54 connect(this, &QDialog::finished, m_translator.get(), &MachineTranslator::stop);
55 connect(m_ui->filterComboBox, &QComboBox::currentIndexChanged, this,
56 &MachineTranslationDialog::onFilterChanged);
57}
58
60{
61 m_dataModel = dm;
62 refresh(true);
63}
64
65void MachineTranslationDialog::refresh(bool init)
66{
67 if (init) {
68 m_ui->filesComboBox->clear();
69 m_ui->filesComboBox->addItems(m_dataModel->srcFileNames());
70 m_ui->filesComboBox->setCurrentIndex(0);
71 m_ui->translationLog->setText(tr("Translation Log"));
72 m_ui->translateButton->setEnabled(true);
73 m_ui->stopButton->setEnabled(false);
74 connectToOllama();
75 }
76 m_sentTexts = 0;
77 m_failedTranslations = 0;
78 m_receivedTranslations.clear();
79 m_ongoingTranslations.clear();
80 m_ui->applyButton->setEnabled(false);
81 m_ui->progressBar->setVisible(false);
82 m_translator->start();
83}
84
85void MachineTranslationDialog::logProgress(const QList<QStringList> &table)
86{
87 const qsizetype receivedCount = m_receivedTranslations.size();
88 m_ui->statusLabel->setText(
89 tr("Translation status: %1/%2 source texts translated, %3/%2 failed.")
90 .arg(receivedCount)
91 .arg(m_sentTexts)
92 .arg(m_failedTranslations));
93 m_ui->progressBar->setValue((receivedCount + m_failedTranslations) * 100 / m_sentTexts);
94 if (!table.empty()) {
95 QString html = "<hr/><table cellpadding=\"4\""
96 "style=\""
97 "width:100%; "
98 "margin-left:10px; "
99 "\">"_L1;
100 for (const QStringList &row : table) {
101 html += "<tr>"_L1;
102 for (const QString &col : row)
103 html += "<td>%1</td>"_L1.arg(col);
104 html += "</tr>"_L1;
105 }
106 html += "</table>"_L1;
107 m_ui->translationLog->append(html);
108 }
109
110 if (receivedCount + m_failedTranslations == m_sentTexts) {
111 m_ui->translationLog->append(
112 tr("<hr/><b>Translation completed: %1/%2 translated, %3/%2 failed.</b>")
113 .arg(receivedCount)
114 .arg(m_sentTexts)
115 .arg(m_failedTranslations));
116 m_ui->translateButton->setEnabled(true);
117 m_ui->stopButton->setEnabled(false);
118 m_ui->applyButton->setEnabled(true);
119 m_ui->progressBar->setVisible(false);
120 } else {
121 m_ui->translateButton->setEnabled(false);
122 m_ui->stopButton->setEnabled(true);
123 m_ui->progressBar->setVisible(true);
124 }
125}
126
127void MachineTranslationDialog::logInfo(const QString &info)
128{
129 m_ui->translationLog->append("<hr/>"_L1);
130 m_ui->translationLog->append(info);
131}
132
133void MachineTranslationDialog::logError(const QString &error)
134{
135 m_ui->translationLog->append("<hr/>"_L1);
136 m_ui->translationLog->append(
137 "<span style=\"color:red; font-weight: bold; \">%1</span>"_L1.arg(error));
138}
139
140bool MachineTranslationDialog::discardTranslations()
141{
142 return (m_receivedTranslations.empty()
143 || QMessageBox::warning(
144 this, tr("Qt Linguist"),
145 tr("The already %n translated item(s) will be discarded. Continue?", 0,
146 m_receivedTranslations.size()),
147 QMessageBox::Yes | QMessageBox::No)
148 == QMessageBox::Yes);
149}
150
151void MachineTranslationDialog::stop()
152{
153 m_translator->stop();
154 m_ui->stopButton->setEnabled(false);
155 m_ui->translateButton->setEnabled(true);
156 refresh(false);
157 logError(tr("Translation Stopped."));
158}
159
160void MachineTranslationDialog::translateSelection()
161{
162 const QString model = m_ui->modelComboBox->currentText();
163 const int id = m_ui->filesComboBox->currentIndex();
164 if (model.isEmpty()) {
165 logError(tr("Please verify the service URL is valid, "
166 "then select a translation model."));
167 return;
168 }
169 if (id < 0) {
170 logError(tr("Please select a file for translation."));
171 return;
172 }
173 if (!discardTranslations())
174 return;
175 refresh(false);
176
177 const int filter = m_ui->filterComboBox->currentIndex();
178 const int group = m_ui->groupComboBox->currentIndex();
179 const DataModel *dm = m_dataModel->model(id);
180 Messages messages;
181 if (filter == 0) {
182 QMutexLocker lock(&m_mutex);
185 if (tm->translation().isEmpty()) {
186 messages.items.append(tm);
187 m_ongoingTranslations[tm] =
188 MultiDataIndex{ it.translationType(), id, it.group(), it.message() };
189 }
190 }
191 for (DataModelIterator it(IDBASED, dm); it.isValid(); ++it) {
193 if (tm->translation().isEmpty()) {
194 messages.items.append(tm);
195 m_ongoingTranslations[tm] =
196 MultiDataIndex{ it.translationType(), id, it.group(), it.message() };
197 }
198 }
199 } else {
200 QMutexLocker lock(&m_mutex);
201 const auto type = (filter == 1) ? TEXTBASED : IDBASED;
202 GroupItem *g = dm->groupItem(group, type);
203 for (int i = 0; i < g->messageCount(); i++) {
205 if (tm->translation().isEmpty()) {
206 messages.items.append(tm);
207 m_ongoingTranslations[tm] = MultiDataIndex{ type, id, group, i };
208 }
209 }
210 }
211 messages.srcLang = QLocale::languageToString(dm->sourceLanguage());
212 messages.tgtLang = QLocale::languageToString(dm->language());
213 m_sentTexts += messages.items.size();
214 m_translator->activateTranslationModel(model);
215 m_translator->translate(messages, m_ui->contextEdit->toPlainText().trimmed());
216 logInfo(tr("Translation Started"));
217 logProgress({});
218}
219
220void MachineTranslationDialog::onBatchTranslated(
221 QHash<const TranslatorMessage *, QString> translations)
222{
223 QList<QStringList> log;
224 log.reserve(translations.size());
225 QMutexLocker lock(&m_mutex);
226 for (const auto &[msg, translation] : translations.asKeyValueRange()) {
227 log.append({ msg->sourceText().simplified(), translation.simplified() });
228 m_receivedTranslations.append(std::make_pair(m_ongoingTranslations.take(msg), translation));
229 }
230 logInfo(tr("Translation Batch:"));
231 logProgress(log);
232}
233
234void MachineTranslationDialog::onFilterChanged(int id)
235{
236 m_ui->groupLabel->setEnabled(id != 0);
237 m_ui->groupComboBox->setEnabled(id != 0);
238 m_ui->groupComboBox->clear();
239 int modelId = m_ui->filesComboBox->currentIndex();
240 if (modelId < 0)
241 return;
242
243 if (id == 1) {
244 for (int i = 0; i < m_dataModel->model(modelId)->contextCount(); i++)
245 m_ui->groupComboBox->addItem(
246 m_dataModel->model(modelId)->groupItem(i, TEXTBASED)->group());
247 } else if (id == 2) {
248 for (int i = 0; i < m_dataModel->model(modelId)->labelCount(); i++)
249 m_ui->groupComboBox->addItem(
250 m_dataModel->model(modelId)->groupItem(i, IDBASED)->group());
251 }
252 m_ui->groupComboBox->setCurrentIndex(0);
253}
254
255void MachineTranslationDialog::applyTranslations()
256{
257 QMutexLocker lock(&m_mutex);
258 for (const auto &[item, translation] : std::as_const(m_receivedTranslations))
259 m_dataModel->setTranslation(item, translation);
260 refresh(false);
261 logInfo(tr("Translations Applied."));
262}
263
264void MachineTranslationDialog::onTranslationFailed(QList<const TranslatorMessage *> failed)
265{
266 QList<QStringList> log;
267 log.reserve(failed.size() + 1);
268
269 QMutexLocker lock(&m_mutex);
270 m_failedTranslations += failed.size();
271 for (const TranslatorMessage *m : failed) {
272 log << QStringList{ m->sourceText().simplified() };
273 m_ongoingTranslations.remove(m);
274 }
275 logError(tr("Failed Translation(s):"));
276 logProgress(log);
277}
278
279void MachineTranslationDialog::updateStatus()
280{
281 const int model = m_ui->filesComboBox->currentIndex();
282 const int filter = m_ui->filterComboBox->currentIndex();
283 const int group = m_ui->groupComboBox->currentIndex();
284 if (model < 0 || filter < 0 || (filter > 0 && group < 0)) {
285 m_ui->statusLabel->setText(tr("Translation status: -"));
286 } else if (filter == 0) {
287 int count = 0;
288 for (DataModelIterator it(IDBASED, m_dataModel->model(model)); it.isValid(); ++it)
289 if (it.current()->translation().isEmpty())
290 count++;
291 for (DataModelIterator it(TEXTBASED, m_dataModel->model(model)); it.isValid(); ++it)
292 if (it.current()->translation().isEmpty())
293 count++;
294
295 m_ui->statusLabel->setText(tr("Translation status: %n item(s). For best results, "
296 "translate in Context / Label batches.",
297 0, count));
298 } else if (group >= 0) {
299 const auto type = (filter == 1) ? TEXTBASED : IDBASED;
300 int count = 0;
301 GroupItem *g = m_dataModel->model(model)->groupItem(group, type);
302 for (int i = 0; i < g->messageCount(); i++)
303 if (g->messageItem(i)->message().translation().isEmpty())
304 count++;
305 m_ui->statusLabel->setText(tr("Translation status: %n item(s).", 0, count));
306 }
307}
308
309void MachineTranslationDialog::connectToOllama()
310{
311 if (m_ui->serverText->text().isEmpty())
312 return;
313 m_translator->setUrl(m_ui->serverText->text());
314 m_translator->requestModels();
315}
316
318
319QT_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
MessageItem * messageItem(int i) const
int messageCount() const
void setDataModel(MultiDataModel *dm)
const TranslatorMessage & message() const
DataModel * model(int i)
@ IDBASED
@ TEXTBASED