1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "translator.h"
5
6#include "simtexth.h"
7
8#include <iostream>
9
10#include <stdio.h>
11#ifdef Q_OS_WIN
12// required for _setmode, to avoid _O_TEXT streams...
13# include <io.h> // for _setmode
14# include <fcntl.h> // for _O_BINARY
15#endif
16
17#include <QtCore/QDebug>
18#include <QtCore/QDir>
19#include <QtCore/QFile>
20#include <QtCore/QFileInfo>
21#include <QtCore/QLocale>
22#include <QtCore/QTextStream>
23
24#include <private/qtranslator_p.h>
25
26QT_BEGIN_NAMESPACE
27
28using namespace Qt::Literals::StringLiterals;
29
30Translator::Translator() :
31 m_locationsType(AbsoluteLocations),
32 m_indexOk(true)
33{
34}
35
36void Translator::registerFileFormat(const FileFormat &format)
37{
38 //qDebug() << "Translator: Registering format " << format.extension;
39 QList<Translator::FileFormat> &formats = registeredFileFormats();
40 for (int i = 0; i < formats.size(); ++i)
41 if (format.fileType == formats[i].fileType && format.priority < formats[i].priority) {
42 formats.insert(i, t: format);
43 return;
44 }
45 formats.append(t: format);
46}
47
48QList<Translator::FileFormat> &Translator::registeredFileFormats()
49{
50 static QList<Translator::FileFormat> theFormats;
51 return theFormats;
52}
53
54void Translator::addIndex(int idx, const TranslatorMessage &msg) const
55{
56
57 m_msgIdx[TMMKey(msg)] = idx;
58 if (!msg.id().isEmpty())
59 m_idMsgIdx[msg.id()] = idx;
60
61}
62
63void Translator::delIndex(int idx) const
64{
65 const TranslatorMessage &msg = m_messages.at(i: idx);
66
67 m_msgIdx.remove(key: TMMKey(msg));
68 if (!msg.id().isEmpty())
69 m_idMsgIdx.remove(key: msg.id());
70
71}
72
73void Translator::ensureIndexed() const
74{
75 if (!m_indexOk) {
76 m_indexOk = true;
77 m_idMsgIdx.clear();
78 m_msgIdx.clear();
79 for (int i = 0; i < m_messages.size(); i++)
80 addIndex(idx: i, msg: m_messages.at(i));
81 }
82}
83
84void Translator::replaceSorted(const TranslatorMessage &msg)
85{
86 int index = find(msg);
87 if (index == -1) {
88 appendSorted(msg);
89 } else {
90 delIndex(idx: index);
91 m_messages[index] = msg;
92 addIndex(idx: index, msg);
93 }
94}
95
96static QString elidedId(const QString &id, int len)
97{
98 return id.size() <= len ? id : id.left(n: len - 5) + "[...]"_L1;
99}
100
101static QString makeMsgId(const TranslatorMessage &msg)
102{
103 QString id = msg.context() + "//"_L1 + elidedId(id: msg.sourceText(), len: 100);
104 if (!msg.comment().isEmpty())
105 id += "//"_L1 + elidedId(id: msg.comment(), len: 30);
106 return id;
107}
108
109void Translator::extend(const TranslatorMessage &msg, ConversionData &cd)
110{
111 int index = find(msg);
112 if (index == -1) {
113 append(msg);
114 } else {
115 TranslatorMessage &emsg = m_messages[index];
116 if (emsg.sourceText().isEmpty()) {
117 delIndex(idx: index);
118 emsg.setSourceText(msg.sourceText());
119 addIndex(idx: index, msg);
120 } else if (!msg.sourceText().isEmpty() && emsg.sourceText() != msg.sourceText()) {
121 cd.appendError(
122 error: "Contradicting source strings for message with id '%1'."_L1.arg(args: emsg.id()));
123 return;
124 }
125 if (emsg.extras().isEmpty()) {
126 emsg.setExtras(msg.extras());
127 } else if (!msg.extras().isEmpty() && emsg.extras() != msg.extras()) {
128 cd.appendError(error: "Contradicting meta data for %1."_L1.arg(
129 args: !emsg.id().isEmpty() ? "message with id '%1'"_L1.arg(args: emsg.id())
130 : "message '%1'"_L1.arg(args: makeMsgId(msg))));
131 return;
132 }
133 emsg.addReferenceUniq(fileName: msg.fileName(), lineNumber: msg.lineNumber());
134 if (!msg.extraComment().isEmpty()) {
135 QString cmt = emsg.extraComment();
136 if (!cmt.isEmpty()) {
137 QStringList cmts = cmt.split(sep: "\n----------\n"_L1);
138 if (!cmts.contains(str: msg.extraComment())) {
139 cmts.append(t: msg.extraComment());
140 cmt = cmts.join(sep: "\n----------\n"_L1);
141 }
142 } else {
143 cmt = msg.extraComment();
144 }
145 emsg.setExtraComment(cmt);
146 }
147 if (emsg.label().isEmpty())
148 emsg.setLabel(msg.label());
149 else if (!msg.label().isEmpty() && emsg.label() != msg.label())
150 cd.appendError(error: "Contradicting label for message with id %1"_L1.arg(args: emsg.id()));
151 }
152}
153
154void Translator::insert(int idx, const TranslatorMessage &msg)
155{
156 if (m_indexOk) {
157 if (idx == m_messages.size())
158 addIndex(idx, msg);
159 else
160 m_indexOk = false;
161 }
162 m_messages.insert(i: idx, t: msg);
163}
164
165void Translator::append(const TranslatorMessage &msg)
166{
167 insert(idx: m_messages.size(), msg);
168}
169
170void Translator::appendSorted(const TranslatorMessage &msg)
171{
172 int msgLine = msg.lineNumber();
173 if (msgLine < 0) {
174 append(msg);
175 return;
176 }
177
178 int bestIdx = 0; // Best insertion point found so far
179 int bestScore = 0; // Its category: 0 = no hit, 1 = pre or post, 2 = middle
180 int bestSize = 0; // The length of the region. Longer is better within one category.
181
182 // The insertion point to use should this region turn out to be the best one so far
183 int thisIdx = 0;
184 int thisScore = 0;
185 int thisSize = 0;
186 // Working vars
187 int prevLine = 0;
188 int curIdx = 0;
189 for (const TranslatorMessage &mit : std::as_const(t&: m_messages)) {
190 bool sameFile = mit.fileName() == msg.fileName() && mit.context() == msg.context();
191 int curLine;
192 if (sameFile && (curLine = mit.lineNumber()) >= prevLine) {
193 if (msgLine >= prevLine && msgLine < curLine) {
194 thisIdx = curIdx;
195 thisScore = thisSize ? 2 : 1;
196 }
197 ++thisSize;
198 prevLine = curLine;
199 } else {
200 if (thisSize) {
201 if (!thisScore) {
202 thisIdx = curIdx;
203 thisScore = 1;
204 }
205 if (thisScore > bestScore || (thisScore == bestScore && thisSize > bestSize)) {
206 bestIdx = thisIdx;
207 bestScore = thisScore;
208 bestSize = thisSize;
209 }
210 thisScore = 0;
211 thisSize = sameFile ? 1 : 0;
212 prevLine = 0;
213 }
214 }
215 ++curIdx;
216 }
217 if (thisSize && !thisScore) {
218 thisIdx = curIdx;
219 thisScore = 1;
220 }
221 if (thisScore > bestScore || (thisScore == bestScore && thisSize > bestSize))
222 insert(idx: thisIdx, msg);
223 else if (bestScore)
224 insert(idx: bestIdx, msg);
225 else
226 append(msg);
227}
228
229static QString guessFormat(const QString &filename, const QString &format)
230{
231 if (format != "auto"_L1)
232 return format;
233
234 for (const Translator::FileFormat &fmt : std::as_const(t&: Translator::registeredFileFormats())) {
235 if (filename.endsWith(s: u'.' + fmt.extension, cs: Qt::CaseInsensitive))
236 return fmt.extension;
237 }
238
239 // the default format.
240 // FIXME: change to something more widely distributed later.
241 return "ts"_L1;
242}
243
244static QString getDependencyName(const QString &filename, const QString &format)
245{
246 const QString file = QFileInfo(filename).fileName();
247 const QString fmt = guessFormat(filename: file, format);
248
249 if (file.endsWith(s: u'.' + fmt))
250 return file.chopped(n: fmt.size() + 1);
251
252 // no extension in the file name
253 return file;
254}
255
256bool Translator::load(const QString &filename, ConversionData &cd, const QString &format)
257{
258 cd.m_sourceDir = QFileInfo(filename).absoluteDir();
259 cd.m_sourceFileName = filename;
260
261 QFile file;
262 if (filename.isEmpty() || filename == "-"_L1) {
263#ifdef Q_OS_WIN
264 // QFile is broken for text files
265 ::_setmode(0, _O_BINARY);
266#endif
267 if (!file.open(stdin, ioFlags: QIODevice::ReadOnly)) {
268 cd.appendError(error: QString::fromLatin1(ba: "Cannot open stdin!? (%1)")
269 .arg(a: file.errorString()));
270 return false;
271 }
272 } else {
273 file.setFileName(filename);
274 if (!file.open(flags: QIODevice::ReadOnly)) {
275 cd.appendError(error: QString::fromLatin1(ba: "Cannot open %1: %2")
276 .arg(args: filename, args: file.errorString()));
277 return false;
278 }
279 }
280
281 QString fmt = guessFormat(filename, format);
282
283 for (const FileFormat &format : std::as_const(t&: registeredFileFormats())) {
284 if (fmt == format.extension) {
285 if (format.loader)
286 return (*format.loader)(*this, file, cd);
287 cd.appendError(error: QString("No loader for format %1 found"_L1).arg(a: fmt));
288 return false;
289 }
290 }
291
292 cd.appendError(error: QString("Unknown format %1 for file %2"_L1).arg(args: format, args: filename));
293 return false;
294}
295
296
297bool Translator::save(const QString &filename, ConversionData &cd, const QString &format) const
298{
299 QFile file;
300 if (filename.isEmpty() || filename == "-"_L1) {
301#ifdef Q_OS_WIN
302 // QFile is broken for text files
303 ::_setmode(1, _O_BINARY);
304#endif
305 if (!file.open(stdout, ioFlags: QIODevice::WriteOnly)) {
306 cd.appendError(error: QString::fromLatin1(ba: "Cannot open stdout!? (%1)")
307 .arg(a: file.errorString()));
308 return false;
309 }
310 } else {
311 file.setFileName(filename);
312 if (!file.open(flags: QIODevice::WriteOnly)) {
313 cd.appendError(error: QString::fromLatin1(ba: "Cannot create %1: %2")
314 .arg(args: filename, args: file.errorString()));
315 return false;
316 }
317 }
318
319 QString fmt = guessFormat(filename, format);
320 cd.m_targetDir = QFileInfo(filename).absoluteDir();
321
322 for (const FileFormat &format : std::as_const(t&: registeredFileFormats())) {
323 if (fmt == format.extension) {
324 if (format.saver) {
325 if (fmt != u"ts" && m_locationsType == RelativeLocations)
326 std::cerr << "Warning: relative locations are not supported for non TS files. "
327 "File "
328 << qPrintable(filename)
329 << " will be generated with the "
330 "default location type."
331 << std::endl;
332 return (*format.saver)(*this, file, cd);
333 }
334 cd.appendError(error: QString("Cannot save %1 files"_L1).arg(a: fmt));
335 return false;
336 }
337 }
338
339 cd.appendError(error: QString("Unknown format %1 for file %2"_L1).arg(a: format).arg(a: filename));
340 return false;
341}
342
343QString Translator::makeLanguageCode(QLocale::Language language, QLocale::Territory territory)
344{
345 QString result = QLocale::languageToCode(language);
346 if (language != QLocale::C && territory != QLocale::AnyTerritory) {
347 result.append(c: u'_');
348 result.append(s: QLocale::territoryToCode(territory));
349 }
350 return result;
351}
352
353void Translator::languageAndTerritory(QStringView languageCode, QLocale::Language *langPtr,
354 QLocale::Territory *territoryPtr)
355{
356 QLocale::Language language = QLocale::AnyLanguage;
357 QLocale::Territory territory = QLocale::AnyTerritory;
358 auto separator = languageCode.indexOf(c: u'_'); // "de_DE"
359 if (separator == -1) {
360 // compatibility with older .ts files
361 separator = languageCode.indexOf(c: u'-'); // "de-DE"
362 }
363 if (separator != -1) {
364 language = QLocale::codeToLanguage(languageCode: languageCode.left(n: separator));
365 territory = QLocale::codeToTerritory(territoryCode: languageCode.mid(pos: separator + 1));
366 } else {
367 language = QLocale::codeToLanguage(languageCode);
368 territory = QLocale(language).territory();
369 }
370
371 if (langPtr)
372 *langPtr = language;
373 if (territoryPtr)
374 *territoryPtr = territory;
375}
376
377int Translator::find(const TranslatorMessage &msg) const
378{
379 ensureIndexed();
380 if (msg.id().isEmpty())
381 return m_msgIdx.value(key: TMMKey(msg), defaultValue: -1);
382 int i = m_idMsgIdx.value(key: msg.id(), defaultValue: -1);
383 if (i >= 0)
384 return i;
385 i = m_msgIdx.value(key: TMMKey(msg), defaultValue: -1);
386 // If both have an id, then find only by id.
387 return i >= 0 && m_messages.at(i).id().isEmpty() ? i : -1;
388}
389
390int Translator::find(const QString &context,
391 const QString &comment, const TranslatorMessage::References &refs) const
392{
393 if (!refs.isEmpty()) {
394 for (auto it = m_messages.cbegin(), end = m_messages.cend(); it != end; ++it) {
395 if (it->context() == context && it->comment() == comment) {
396 for (const auto &itref : it->allReferences()) {
397 for (const auto &ref : refs) {
398 if (itref == ref)
399 return it - m_messages.cbegin();
400 }
401 }
402 }
403 }
404 }
405 return -1;
406}
407
408void Translator::stripObsoleteMessages()
409{
410 for (auto it = m_messages.begin(); it != m_messages.end(); )
411 if (it->type() == TranslatorMessage::Obsolete || it->type() == TranslatorMessage::Vanished)
412 it = m_messages.erase(pos: it);
413 else
414 ++it;
415 m_indexOk = false;
416}
417
418void Translator::stripFinishedMessages()
419{
420 for (auto it = m_messages.begin(); it != m_messages.end(); )
421 if (it->type() == TranslatorMessage::Finished)
422 it = m_messages.erase(pos: it);
423 else
424 ++it;
425 m_indexOk = false;
426}
427
428void Translator::stripUntranslatedMessages()
429{
430 for (auto it = m_messages.begin(); it != m_messages.end(); )
431 if (!it->isTranslated())
432 it = m_messages.erase(pos: it);
433 else
434 ++it;
435 m_indexOk = false;
436}
437
438bool Translator::translationsExist() const
439{
440 return std::any_of(first: m_messages.cbegin(), last: m_messages.cend(),
441 pred: [](const auto &m) { return m.isTranslated(); });
442}
443
444bool Translator::unfinishedTranslationsExist() const
445{
446 return std::any_of(first: m_messages.cbegin(), last: m_messages.cend(),
447 pred: [](const auto &m) { return m.type() == TranslatorMessage::Unfinished; });
448}
449
450void Translator::stripEmptyContexts()
451{
452 for (auto it = m_messages.begin(); it != m_messages.end(); )
453 if (it->sourceText() == QLatin1String(ContextComment))
454 it = m_messages.erase(pos: it);
455 else
456 ++it;
457 m_indexOk = false;
458}
459
460void Translator::stripNonPluralForms()
461{
462 for (auto it = m_messages.begin(); it != m_messages.end(); )
463 if (!it->isPlural())
464 it = m_messages.erase(pos: it);
465 else
466 ++it;
467 m_indexOk = false;
468}
469
470void Translator::stripIdenticalSourceTranslations()
471{
472 for (auto it = m_messages.begin(); it != m_messages.end(); ) {
473 // we need to have just one translation, and it be equal to the source
474 if (it->translations().size() == 1 && it->translation() == it->sourceText())
475 it = m_messages.erase(pos: it);
476 else
477 ++it;
478 }
479 m_indexOk = false;
480}
481
482void Translator::dropTranslations()
483{
484 for (auto &message : m_messages) {
485 if (message.type() == TranslatorMessage::Finished)
486 message.setType(TranslatorMessage::Unfinished);
487 message.setTranslation(QString());
488 }
489}
490
491void Translator::dropUiLines()
492{
493 const QString uiXt = ".ui"_L1;
494 const QString juiXt = ".jui"_L1;
495 for (auto &message : m_messages) {
496 QHash<QString, int> have;
497 QList<TranslatorMessage::Reference> refs;
498 for (const auto &itref : message.allReferences()) {
499 const QString &fn = itref.fileName();
500 if (fn.endsWith(s: uiXt) || fn.endsWith(s: juiXt)) {
501 if (++have[fn] == 1)
502 refs.append(t: TranslatorMessage::Reference(fn, -1));
503 } else {
504 refs.append(t: itref);
505 }
506 }
507 message.setReferences(refs);
508 }
509}
510
511class TranslatorMessagePtrBase
512{
513public:
514 explicit TranslatorMessagePtrBase(const Translator *tor, int messageIndex)
515 : tor(tor), messageIndex(messageIndex)
516 {
517 }
518
519 inline const TranslatorMessage *operator->() const
520 {
521 return &tor->message(i: messageIndex);
522 }
523
524 const Translator *tor;
525 const int messageIndex;
526};
527
528class TranslatorMessageIdPtr : public TranslatorMessagePtrBase
529{
530public:
531 using TranslatorMessagePtrBase::TranslatorMessagePtrBase;
532};
533
534Q_DECLARE_TYPEINFO(TranslatorMessageIdPtr, Q_RELOCATABLE_TYPE);
535
536inline size_t qHash(TranslatorMessageIdPtr tmp)
537{
538 return qHash(key: tmp->id());
539}
540
541inline bool operator==(TranslatorMessageIdPtr tmp1, TranslatorMessageIdPtr tmp2)
542{
543 return tmp1->id() == tmp2->id();
544}
545
546class TranslatorMessageContentPtr : public TranslatorMessagePtrBase
547{
548public:
549 using TranslatorMessagePtrBase::TranslatorMessagePtrBase;
550};
551
552Q_DECLARE_TYPEINFO(TranslatorMessageContentPtr, Q_RELOCATABLE_TYPE);
553
554inline size_t qHash(TranslatorMessageContentPtr tmp)
555{
556 size_t hash = qHash(key: tmp->context()) ^ qHash(key: tmp->sourceText());
557 if (!tmp->sourceText().isEmpty())
558 // Special treatment for context comments (empty source).
559 hash ^= qHash(key: tmp->comment());
560 return hash;
561}
562
563inline bool operator==(TranslatorMessageContentPtr tmp1, TranslatorMessageContentPtr tmp2)
564{
565 if (tmp1->context() != tmp2->context() || tmp1->sourceText() != tmp2->sourceText())
566 return false;
567 // Special treatment for context comments (empty source).
568 if (tmp1->sourceText().isEmpty())
569 return true;
570 return tmp1->comment() == tmp2->comment();
571}
572
573Translator::Duplicates Translator::resolveDuplicates()
574{
575 Duplicates dups;
576 QSet<TranslatorMessageIdPtr> idRefs;
577 QSet<TranslatorMessageContentPtr> contentRefs;
578 for (int i = 0; i < m_messages.size();) {
579 const TranslatorMessage &msg = m_messages.at(i);
580 TranslatorMessage *omsg;
581 int oi;
582 DuplicateEntries *pDup;
583 if (!msg.id().isEmpty()) {
584 const auto it = idRefs.constFind(value: TranslatorMessageIdPtr(this, i));
585 if (it != idRefs.constEnd()) {
586 oi = it->messageIndex;
587 omsg = &m_messages[oi];
588 pDup = &dups.byId;
589 goto gotDupe;
590 }
591 }
592 {
593 const auto it = contentRefs.constFind(value: TranslatorMessageContentPtr(this, i));
594 if (it != contentRefs.constEnd()) {
595 oi = it->messageIndex;
596 omsg = &m_messages[oi];
597 if (msg.id().isEmpty() || omsg->id().isEmpty()) {
598 if (!msg.id().isEmpty() && omsg->id().isEmpty()) {
599 omsg->setId(msg.id());
600 idRefs.insert(value: TranslatorMessageIdPtr(this, oi));
601 }
602 pDup = &dups.byContents;
603 goto gotDupe;
604 }
605 // This is really a content dupe, but with two distinct IDs.
606 }
607 }
608 if (!msg.id().isEmpty())
609 idRefs.insert(value: TranslatorMessageIdPtr(this, i));
610 contentRefs.insert(value: TranslatorMessageContentPtr(this, i));
611 ++i;
612 continue;
613 gotDupe:
614 (*pDup)[oi].append(t: msg.tsLineNumber());
615 if (!omsg->isTranslated() && msg.isTranslated())
616 omsg->setTranslations(msg.translations());
617 m_indexOk = false;
618 m_messages.removeAt(i);
619 }
620 return dups;
621}
622
623void Translator::reportDuplicates(const Duplicates &dupes,
624 const QString &fileName, bool verbose)
625{
626 if (!dupes.byId.isEmpty() || !dupes.byContents.isEmpty()) {
627 std::cerr << "Warning: dropping duplicate messages in '" << qPrintable(fileName);
628 if (!verbose) {
629 std::cerr << "'\n(try -verbose for more info).\n";
630 } else {
631 std::cerr << "':\n";
632 for (auto it = dupes.byId.begin(); it != dupes.byId.end(); ++it) {
633 const TranslatorMessage &msg = message(i: it.key());
634 std::cerr << "\n* ID: " << qPrintable(msg.id()) << std::endl;
635 reportDuplicatesLines(msg, dups: it.value());
636 }
637 for (auto it = dupes.byContents.begin(); it != dupes.byContents.end(); ++it) {
638 const TranslatorMessage &msg = message(i: it.key());
639 std::cerr << "\n* Context: " << qPrintable(msg.context())
640 << "\n* Source: " << qPrintable(msg.sourceText()) << std::endl;
641 if (!msg.comment().isEmpty())
642 std::cerr << "* Comment: " << qPrintable(msg.comment()) << std::endl;
643 reportDuplicatesLines(msg, dups: it.value());
644 }
645 std::cerr << std::endl;
646 }
647 }
648}
649
650void Translator::reportDuplicatesLines(const TranslatorMessage &msg,
651 const DuplicateEntries::value_type &dups) const
652{
653 if (msg.tsLineNumber() >= 0) {
654 std::cerr << "* Line in .ts file: " << msg.tsLineNumber() << std::endl;
655 for (int tsLineNumber : dups) {
656 if (tsLineNumber >= 0)
657 std::cerr << "* Duplicate at line: " << tsLineNumber << std::endl;
658 }
659 }
660}
661
662// Used by lupdate to be able to search using absolute paths during merging
663void Translator::makeFileNamesAbsolute(const QDir &originalPath)
664{
665 for (auto &msg : m_messages) {
666 const TranslatorMessage::References refs = msg.allReferences();
667 msg.setReferences(TranslatorMessage::References());
668 for (const TranslatorMessage::Reference &ref : refs) {
669 QString fileName = ref.fileName();
670 QFileInfo fi (fileName);
671 if (fi.isRelative())
672 fileName = originalPath.absoluteFilePath(fileName);
673 msg.addReference(fileName, lineNumber: ref.lineNumber());
674 }
675 }
676}
677
678const QList<TranslatorMessage> &Translator::messages() const
679{
680 return m_messages;
681}
682
683QStringList Translator::normalizedTranslations(const TranslatorMessage &msg, int numPlurals)
684{
685 QStringList translations = msg.translations();
686 int numTranslations = msg.isPlural() ? numPlurals : 1;
687
688 // make sure that the stringlist always have the size of the
689 // language's current numerus, or 1 if its not plural
690 if (translations.size() > numTranslations) {
691 for (int i = translations.size(); i > numTranslations; --i)
692 translations.removeLast();
693 } else if (translations.size() < numTranslations) {
694 for (int i = translations.size(); i < numTranslations; ++i)
695 translations.append(t: QString());
696 }
697 return translations;
698}
699
700void Translator::normalizeTranslations(ConversionData &cd)
701{
702 bool truncated = false;
703 QLocale::Language l;
704 QLocale::Territory c;
705 languageAndTerritory(languageCode: languageCode(), langPtr: &l, territoryPtr: &c);
706 int numPlurals = 1;
707 if (l != QLocale::C) {
708 QStringList forms;
709 if (getNumerusInfo(language: l, territory: c, rules: 0, forms: &forms, gettextRules: 0))
710 numPlurals = forms.size(); // includes singular
711 }
712 for (int i = 0; i < m_messages.size(); ++i) {
713 const TranslatorMessage &msg = m_messages.at(i);
714 QStringList tlns = msg.translations();
715 int ccnt = msg.isPlural() ? numPlurals : 1;
716 if (tlns.size() != ccnt) {
717 while (tlns.size() < ccnt)
718 tlns.append(t: QString());
719 while (tlns.size() > ccnt) {
720 tlns.removeLast();
721 truncated = true;
722 }
723 m_messages[i].setTranslations(tlns);
724 }
725 }
726 if (truncated)
727 cd.appendError(error: QLatin1String(
728 "Removed plural forms as the target language has less "
729 "forms.\nIf this sounds wrong, possibly the target language is "
730 "not set or recognized."));
731}
732
733QString Translator::guessLanguageCodeFromFileName(const QString &filename)
734{
735 QString str = filename;
736 for (const FileFormat &format : std::as_const(t&: registeredFileFormats())) {
737 if (str.endsWith(s: format.extension)) {
738 str = str.left(n: str.size() - format.extension.size() - 1);
739 break;
740 }
741 }
742 static QRegularExpression re("[\\._]"_L1);
743 while (true) {
744 QLocale locale(str);
745 //qDebug() << "LANGUAGE FROM " << str << "LANG: " << locale.language();
746 if (locale.language() != QLocale::C) {
747 //qDebug() << "FOUND " << locale.name();
748 return locale.name();
749 }
750 int pos = str.indexOf(re);
751 if (pos == -1)
752 break;
753 str = str.mid(position: pos + 1);
754 }
755 //qDebug() << "LANGUAGE GUESSING UNSUCCESSFUL";
756 return QString();
757}
758
759void Translator::appendDependencies(const QStringList &dependencies)
760{
761 QStringList mergeDeps;
762 for (const QString &dep : dependencies) {
763 if (const auto it = std::find(first: m_dependencies.cbegin(), last: m_dependencies.cend(), val: dep);
764 it == m_dependencies.cend()) {
765 mergeDeps.append(t: dep);
766 }
767 }
768 m_dependencies.append(l: mergeDeps);
769}
770
771void Translator::satisfyDependency(const QString &file, const QString &format)
772{
773 const auto dep = getDependencyName(filename: file, format);
774 if (const auto it = std::find(first: m_dependencies.cbegin(), last: m_dependencies.cend(), val: dep);
775 it != m_dependencies.cend()) {
776 m_dependencies.erase(pos: it);
777 }
778}
779
780bool Translator::hasExtra(const QString &key) const
781{
782 return m_extra.contains(key);
783}
784
785QString Translator::extra(const QString &key) const
786{
787 return m_extra[key];
788}
789
790void Translator::setExtra(const QString &key, const QString &value)
791{
792 m_extra[key] = value;
793}
794
795void Translator::dump() const
796{
797 for (int i = 0; i != messageCount(); ++i)
798 message(i).dump();
799}
800
801QT_END_NAMESPACE
802

source code of qttools/src/linguist/shared/translator.cpp