17#include <QtCore/QDebug>
19#include <QtCore/QFile>
20#include <QtCore/QFileInfo>
21#include <QtCore/QLocale>
22#include <QtCore/QTextStream>
24#include <private/qtranslator_p.h>
28using namespace Qt::Literals::StringLiterals;
31static bool usesCRLF(
const QString &filename)
34 if (!file.open(QIODevice::ReadOnly))
36 return file.read(4096).contains(
"\r\n"_ba);
42 QString f = str.toLower();
43 static QRegularExpression re(
"[.,:;!?()-]"_L1);
44 f.replace(re,
" "_L1);
46 return f.simplified();
58 QList<Translator::FileFormat> &formats = registeredFileFormats();
59 for (
int i = 0; i < formats.size(); ++i)
60 if (format.fileType == formats[i].fileType && format
.priority < formats[i].priority) {
61 formats.insert(i, format);
64 formats.append(format);
67QList<Translator::FileFormat> &
Translator::registeredFileFormats()
69 static QList<Translator::FileFormat> theFormats;
76 m_msgIdx[TMMKey(msg)] = idx;
77 if (!msg.id().isEmpty())
78 m_idMsgIdx[msg.id()] = idx;
86 m_msgIdx.remove(TMMKey(msg));
87 if (!msg.id().isEmpty())
88 m_idMsgIdx.remove(msg.id());
98 for (
int i = 0; i < m_messages.size(); i++)
99 addIndex(i, m_messages.at(i));
110 m_messages[index] = msg;
111 addIndex(index, msg);
117 return id.size() <= len ? id : id.left(len - 5) +
"[...]"_L1;
122 QString id = msg.context() +
"//"_L1 + elidedId(msg.sourceText(), 100);
123 if (!msg.comment().isEmpty())
124 id +=
"//"_L1 + elidedId(msg.comment(), 30);
135 if (emsg.sourceText().isEmpty()) {
137 emsg.setSourceText(msg.sourceText());
138 addIndex(index, msg);
139 }
else if (!msg.sourceText().isEmpty() && emsg.sourceText() != msg.sourceText()) {
141 "Contradicting source strings for message with id '%1'."_L1.arg(emsg.id()));
147 cd.appendError(
"Contradicting meta data for %1."_L1.arg(
148 !emsg.id().isEmpty() ?
"message with id '%1'"_L1.arg(emsg.id())
149 :
"message '%1'"_L1.arg(makeMsgId(msg))));
153 if (!msg.extraComment().isEmpty()) {
154 QString cmt = emsg.extraComment();
155 if (!cmt.isEmpty()) {
156 QStringList cmts = cmt.split(
"\n----------\n"_L1);
157 if (!cmts.contains(msg.extraComment())) {
158 cmts.append(msg.extraComment());
159 cmt = cmts.join(
"\n----------\n"_L1);
162 cmt = msg.extraComment();
164 emsg.setExtraComment(cmt);
166 if (emsg.label().isEmpty())
167 emsg.setLabel(msg.label());
168 else if (!msg.label().isEmpty() && emsg.label() != msg.label())
169 cd.appendError(
"Contradicting label for message with id %1"_L1.arg(emsg.id()));
176 if (idx == m_messages.size())
181 m_messages.insert(idx, msg);
186 insert(m_messages.size(), msg);
208 for (
const TranslatorMessage &mit : std::as_const(m_messages)) {
209 bool sameFile = mit.fileName() == msg.fileName() && mit.context() == msg.context();
211 if (sameFile && (curLine = mit.lineNumber()) >= prevLine) {
212 if (msgLine >= prevLine && msgLine < curLine) {
214 thisScore = thisSize ? 2 : 1;
224 if (thisScore > bestScore || (thisScore == bestScore && thisSize > bestSize)) {
226 bestScore = thisScore;
230 thisSize = sameFile ? 1 : 0;
236 if (thisSize && !thisScore) {
240 if (thisScore > bestScore || (thisScore == bestScore && thisSize > bestSize))
241 insert(thisIdx, msg);
243 insert(bestIdx, msg);
250 if (format !=
"auto"_L1)
253 for (
const Translator::FileFormat &fmt : std::as_const(Translator::registeredFileFormats())) {
254 if (filename.endsWith(u'.' + fmt.extension, Qt::CaseInsensitive))
255 return fmt.extension;
265 const QString file = QFileInfo(filename).fileName();
266 const QString fmt = guessFormat(file, format);
268 if (file.endsWith(u'.' + fmt))
269 return file.chopped(fmt.size() + 1);
277 cd.m_sourceDir = QFileInfo(filename).absoluteDir();
278 cd.m_sourceFileName = filename;
281 if (filename.isEmpty() || filename ==
"-"_L1) {
284 ::_setmode(0, _O_BINARY);
286 if (!file.open(stdin, QIODevice::ReadOnly)) {
287 cd.appendError(QString::fromLatin1(
"Cannot open stdin!? (%1)")
288 .arg(file.errorString()));
292 file.setFileName(filename);
293 if (!file.open(QIODevice::ReadOnly)) {
294 cd.appendError(QString::fromLatin1(
"Cannot open %1: %2")
295 .arg(filename, file.errorString()));
300 QString fmt = guessFormat(filename, format);
302 for (
const FileFormat &format : std::as_const(registeredFileFormats())) {
303 if (fmt == format.extension) {
305 return (*format.loader)(*
this, file, cd);
306 cd.appendError(QString(
"No loader for format %1 found"_L1).arg(fmt));
311 cd.appendError(QString(
"Unknown format %1 for file %2"_L1).arg(format, filename));
319 if (filename.isEmpty() || filename ==
"-"_L1) {
322 ::_setmode(1, _O_BINARY);
324 if (!file.open(stdout, QIODevice::WriteOnly)) {
325 cd.appendError(QString::fromLatin1(
"Cannot open stdout!? (%1)")
326 .arg(file.errorString()));
330 file.setFileName(filename);
331 QIODevice::OpenMode mode = QIODevice::WriteOnly;
334 if (usesCRLF(filename))
335 mode |= QIODevice::Text;
337 if (!file.open(mode)) {
338 cd.appendError(QString::fromLatin1(
"Cannot create %1: %2")
339 .arg(filename, file.errorString()));
344 QString fmt = guessFormat(filename, format);
345 cd.m_targetDir = QFileInfo(filename).absoluteDir();
347 for (
const FileFormat &format : std::as_const(registeredFileFormats())) {
348 if (fmt == format.extension) {
350 if (fmt != u"ts" && m_locationsType == RelativeLocations)
351 std::cerr <<
"Warning: relative locations are not supported for non TS files. "
353 << qPrintable(filename)
354 <<
" will be generated with the "
355 "default location type."
357 return (*format.saver)(*
this, file, cd);
359 cd.appendError(QString(
"Cannot save %1 files"_L1).arg(fmt));
364 cd.appendError(QString(
"Unknown format %1 for file %2"_L1).arg(format, filename));
368QString
Translator::makeLanguageCode(QLocale::Language language, QLocale::Territory territory)
370 QString result = QLocale::languageToCode(language);
371 if (language != QLocale::C && territory != QLocale::AnyTerritory) {
373 result.append(QLocale::territoryToCode(territory));
378void Translator::languageAndTerritory(QStringView languageCode, QLocale::Language *langPtr,
379 QLocale::Territory *territoryPtr)
381 QLocale::Language language = QLocale::AnyLanguage;
382 QLocale::Territory territory = QLocale::AnyTerritory;
383 auto separator = languageCode.indexOf(u'_');
384 if (separator == -1) {
386 separator = languageCode.indexOf(u'-');
388 if (separator != -1) {
389 language = QLocale::codeToLanguage(languageCode.left(separator));
390 territory = QLocale::codeToTerritory(languageCode.mid(separator + 1));
392 language = QLocale::codeToLanguage(languageCode);
393 territory = QLocale(language).territory();
399 *territoryPtr = territory;
405 if (msg.id().isEmpty())
406 return m_msgIdx.value(TMMKey(msg), -1);
407 int i = m_idMsgIdx.value(msg.id(), -1);
410 i = m_msgIdx.value(TMMKey(msg), -1);
412 return i >= 0 && m_messages.at(i).id().isEmpty() ? i : -1;
418 if (!refs.isEmpty()) {
419 for (
auto it = m_messages.cbegin(), end = m_messages.cend(); it != end; ++it) {
420 if (it->context() == context && it->comment() == comment) {
421 for (
const auto &itref : it->allReferences()) {
422 for (
const auto &ref : refs) {
424 return it - m_messages.cbegin();
435 for (
auto it = m_messages.begin(); it != m_messages.end(); )
437 it = m_messages.erase(it);
445 for (
auto it = m_messages.begin(); it != m_messages.end(); )
447 it = m_messages.erase(it);
455 for (
auto it = m_messages.begin(); it != m_messages.end(); )
456 if (!it->isTranslated())
457 it = m_messages.erase(it);
465 return std::any_of(m_messages.cbegin(), m_messages.cend(),
466 [](
const auto &m) {
return m.isTranslated(); });
471 return std::any_of(m_messages.cbegin(), m_messages.cend(),
472 [](
const auto &m) {
return m.type() == TranslatorMessage::Unfinished; });
477 for (
auto it = m_messages.begin(); it != m_messages.end(); )
479 it = m_messages.erase(it);
487 for (
auto it = m_messages.begin(); it != m_messages.end(); )
489 it = m_messages.erase(it);
497 for (
auto it = m_messages.begin(); it != m_messages.end(); ) {
499 if (it->translations().size() == 1 && it->translation() == it->sourceText())
500 it = m_messages.erase(it);
509 for (
auto &message : m_messages) {
510 if (message.type() == TranslatorMessage::Finished)
511 message.setType(TranslatorMessage::Unfinished);
512 message.setTranslation(QString());
518 const QString uiXt =
".ui"_L1;
519 const QString juiXt =
".jui"_L1;
520 for (
auto &message : m_messages) {
521 QHash<QString,
int> have;
522 QList<TranslatorMessage::Reference> refs;
523 for (
const auto &itref : message.allReferences()) {
524 const QString &fn = itref.fileName();
525 if (fn.endsWith(uiXt) || fn.endsWith(juiXt)) {
527 refs.append(TranslatorMessage::Reference(fn, -1, -1, -1));
532 message.setReferences(refs);
563 return qHash(tmp->id(), seed);
568 return tmp1->id() == tmp2->id();
581 size_t hash = qHash(tmp->context(), seed) ^ qHash(tmp->sourceText(), seed);
582 if (!tmp->sourceText().isEmpty())
584 hash ^= qHash(tmp->comment(), seed);
590 if (tmp1->context() != tmp2->context() || tmp1->sourceText() != tmp2->sourceText())
593 if (tmp1->sourceText().isEmpty())
595 return tmp1->comment() == tmp2->comment();
601 QSet<TranslatorMessageIdPtr> idRefs;
602 QSet<TranslatorMessageContentPtr> contentRefs;
603 for (
int i = 0; i < m_messages.size();) {
607 DuplicateEntries *pDup;
608 if (!msg.id().isEmpty()) {
610 if (it != idRefs.constEnd()) {
611 oi = it->messageIndex;
612 omsg = &m_messages[oi];
619 if (it != contentRefs.constEnd()) {
620 oi = it->messageIndex;
621 omsg = &m_messages[oi];
622 if (msg.id().isEmpty() || omsg->id().isEmpty()) {
623 if (!msg.id().isEmpty() && omsg->id().isEmpty()) {
624 omsg->setId(msg.id());
627 pDup = &dups.byContents;
633 if (!msg.id().isEmpty())
641 omsg->setTranslations(msg.translations());
643 m_messages.removeAt(i);
649 const QString &fileName,
bool verbose)
651 if (!dupes.byId.isEmpty() || !dupes.byContents.isEmpty()) {
652 std::cerr <<
"Warning: dropping duplicate messages in '" << qPrintable(fileName);
654 std::cerr <<
"'\n(try -verbose for more info).\n";
657 for (
auto it = dupes.byId.begin(); it != dupes.byId.end(); ++it) {
659 std::cerr <<
"\n* ID: " << qPrintable(msg.id()) <<
std::endl;
660 reportDuplicatesLines(msg, it.value());
662 for (
auto it = dupes.byContents.begin(); it != dupes.byContents.end(); ++it) {
664 std::cerr <<
"\n* Context: " << qPrintable(msg.context())
665 <<
"\n* Source: " << qPrintable(msg.sourceText()) <<
std::endl;
666 if (!msg.comment().isEmpty())
667 std::cerr <<
"* Comment: " << qPrintable(msg.comment()) <<
std::endl;
668 reportDuplicatesLines(msg, it.value());
676 const DuplicateEntries::value_type &dups)
const
680 for (
int tsLineNumber : dups) {
681 if (tsLineNumber >= 0)
682 std::cerr <<
"* Duplicate at line: " << tsLineNumber << std::endl;
690 for (
auto &msg : m_messages) {
691 const TranslatorMessage::References refs = msg.allReferences();
692 msg.setReferences(TranslatorMessage::References());
693 for (
const TranslatorMessage::Reference &ref : refs) {
694 QString fileName = ref.fileName();
695 QFileInfo fi (fileName);
697 fileName = originalPath.absoluteFilePath(fileName);
698 msg.addReference(fileName, ref.lineNumber(), ref.startOffset(), ref.endOffset());
710 QStringList translations = msg.translations();
711 int numTranslations = msg
.isPlural() ? numPlurals : 1;
715 if (translations.size() > numTranslations) {
716 for (
int i = translations.size(); i > numTranslations; --i)
717 translations.removeLast();
718 }
else if (translations.size() < numTranslations) {
719 for (
int i = translations.size(); i < numTranslations; ++i)
720 translations.append(QString());
727 bool truncated =
false;
729 QLocale::Territory c;
730 languageAndTerritory(languageCode(), &l, &c);
732 if (l != QLocale::C) {
734 if (getNumerusInfo(l, c, 0, &forms, 0))
735 numPlurals = forms.size();
737 for (
int i = 0; i < m_messages.size(); ++i) {
739 QStringList tlns = msg.translations();
741 if (tlns.size() != ccnt) {
742 while (tlns.size() < ccnt)
743 tlns.append(QString());
744 while (tlns.size() > ccnt) {
748 m_messages[i].setTranslations(tlns);
752 cd.appendError(QLatin1String(
753 "Removed plural forms as the target language has less "
754 "forms.\nIf this sounds wrong, possibly the target language is "
755 "not set or recognized."));
758QString
Translator::guessLanguageCodeFromFileName(
const QString &filename)
760 QString str = filename;
761 for (
const FileFormat &format : std::as_const(registeredFileFormats())) {
762 if (str.endsWith(format.extension)) {
763 str = str.left(str.size() - format.extension.size() - 1);
767 static QRegularExpression re(
"[\\._]"_L1);
771 if (locale.language() != QLocale::C) {
773 return locale.name();
775 int pos = str.indexOf(re);
778 str = str.mid(pos + 1);
786 QStringList mergeDeps;
787 for (
const QString &dep : dependencies) {
788 if (
const auto it = std::find(m_dependencies.cbegin(), m_dependencies.cend(), dep);
789 it == m_dependencies.cend()) {
790 mergeDeps.append(dep);
793 m_dependencies.append(mergeDeps);
796void Translator::satisfyDependency(
const QString &file,
const QString &format)
798 const auto dep = getDependencyName(file, format);
799 if (
const auto it =
std::find(m_dependencies.cbegin(), m_dependencies.cend(), dep);
800 it != m_dependencies.cend()) {
801 m_dependencies.erase(it);
807 return m_extra.contains(key);
815void Translator::setExtra(
const QString &key,
const QString &value)
817 m_extra[key] = value;
const TranslatorMessage * operator->() const
TranslatorMessagePtrBase(const Translator *tor, int messageIndex)
bool isTranslated() const
void setExtras(const ExtraData &extras)
QList< Reference > References
const ExtraData & extras() const
void stripObsoleteMessages()
bool unfinishedTranslationsExist() const
void stripEmptyContexts()
void stripUntranslatedMessages()
void stripFinishedMessages()
void replaceSorted(const TranslatorMessage &msg)
void stripNonPluralForms()
const TranslatorMessage & message(int i) const
static void registerFileFormat(const FileFormat &format)
void append(const TranslatorMessage &msg)
void stripIdenticalSourceTranslations()
void makeFileNamesAbsolute(const QDir &originalPath)
bool translationsExist() const
Duplicates resolveDuplicates()
void normalizeTranslations(ConversionData &cd)
void appendSorted(const TranslatorMessage &msg)
void reportDuplicatesLines(const TranslatorMessage &msg, const DuplicateEntries::value_type &dups) const
int find(const TranslatorMessage &msg) const
void appendDependencies(const QStringList &dependencies)
const QList< TranslatorMessage > & messages() const
void extend(const TranslatorMessage &msg, ConversionData &cd)
Q_DECLARE_TYPEINFO(TranslatorMessageIdPtr, Q_RELOCATABLE_TYPE)
static QString guessFormat(const QString &filename, const QString &format)
QString friendlyString(const QString &str)
size_t qHash(TranslatorMessageIdPtr tmp, size_t seed=0)
static QString elidedId(const QString &id, int len)
static QString makeMsgId(const TranslatorMessage &msg)
Q_DECLARE_TYPEINFO(TranslatorMessageContentPtr, Q_RELOCATABLE_TYPE)
size_t qHash(TranslatorMessageContentPtr tmp, size_t seed=0)
bool operator==(TranslatorMessageContentPtr tmp1, TranslatorMessageContentPtr tmp2)
static QString getDependencyName(const QString &filename, const QString &format)
bool operator==(TranslatorMessageIdPtr tmp1, TranslatorMessageIdPtr tmp2)