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

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