1// Copyright (C) 2016 The Qt Company Ltd.
2// Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Marc Mutz <marc.mutz@kdab.com>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
4
5#include "lupdate.h"
6#if QT_CONFIG(clangcpp)
7#include "cpp_clang.h"
8#endif
9
10#include <profileutils.h>
11#include <projectdescriptionreader.h>
12#include <qrcreader.h>
13#include <runqttool.h>
14#include <translator.h>
15
16#include <QtCore/QCoreApplication>
17#include <QtCore/QDir>
18#include <QtCore/QFile>
19#include <QtCore/QFileInfo>
20#include <QtCore/QLibraryInfo>
21#include <QtCore/QString>
22#include <QtCore/QStringList>
23#include <QtCore/QTranslator>
24
25#include <iostream>
26
27using namespace Qt::StringLiterals;
28
29bool useClangToParseCpp = false;
30QString commandLineCompilationDatabaseDir; // for the path to the json file passed as a command line argument.
31 // Has priority over what is in the .pro file and passed to the project.
32QStringList rootDirs;
33
34// Can't have an array of QStaticStringData<N> for different N, so
35// use QString, which requires constructor calls. Doesn't matter
36// much, since this is in an app, not a lib:
37static const QString defaultTrFunctionNames[] = {
38// MSVC can't handle the lambda in this array if QStringLiteral expands
39// to a lambda. In that case, use a QString instead.
40#if defined(Q_CC_MSVC) && defined(Q_COMPILER_LAMBDA)
41#define STRINGLITERAL(F) QLatin1String(#F),
42#else
43#define STRINGLITERAL(F) QStringLiteral(#F),
44#endif
45 LUPDATE_FOR_EACH_TR_FUNCTION(STRINGLITERAL)
46#undef STRINGLITERAL
47};
48Q_STATIC_ASSERT((TrFunctionAliasManager::NumTrFunctions == sizeof defaultTrFunctionNames / sizeof *defaultTrFunctionNames));
49
50static int trFunctionByDefaultName(const QString &trFunctionName)
51{
52 for (int i = 0; i < TrFunctionAliasManager::NumTrFunctions; ++i)
53 if (trFunctionName == defaultTrFunctionNames[i])
54 return i;
55 return -1;
56}
57
58TrFunctionAliasManager::TrFunctionAliasManager()
59 : m_trFunctionAliases()
60{
61 for (int i = 0; i < NumTrFunctions; ++i)
62 m_trFunctionAliases[i].push_back(t: defaultTrFunctionNames[i]);
63}
64
65TrFunctionAliasManager::~TrFunctionAliasManager() {}
66
67int TrFunctionAliasManager::trFunctionByName(const QString &trFunctionName) const
68{
69 ensureTrFunctionHashUpdated();
70 // this function needs to be fast
71 const auto it = m_nameToTrFunctionMap.constFind(key: trFunctionName);
72 return it == m_nameToTrFunctionMap.cend() ? -1 : *it;
73}
74
75void TrFunctionAliasManager::modifyAlias(int trFunction, const QString &alias, Operation op)
76{
77 QList<QString> &list = m_trFunctionAliases[trFunction];
78 if (op == SetAlias)
79 list.clear();
80 list.push_back(t: alias);
81 m_nameToTrFunctionMap.clear();
82}
83
84void TrFunctionAliasManager::ensureTrFunctionHashUpdated() const
85{
86 if (!m_nameToTrFunctionMap.empty())
87 return;
88
89 NameToTrFunctionMap nameToTrFunctionMap;
90 for (int i = 0; i < NumTrFunctions; ++i)
91 for (const QString &alias : m_trFunctionAliases[i])
92 nameToTrFunctionMap[alias] = TrFunction(i);
93 // commit:
94 m_nameToTrFunctionMap.swap(other&: nameToTrFunctionMap);
95}
96
97const TrFunctionAliasManager::NameToTrFunctionMap &TrFunctionAliasManager::nameToTrFunctionMap() const
98{
99 ensureTrFunctionHashUpdated();
100 return m_nameToTrFunctionMap;
101}
102
103static QStringList availableFunctions()
104{
105 QStringList result;
106 result.reserve(asize: TrFunctionAliasManager::NumTrFunctions);
107 for (int i = 0; i < TrFunctionAliasManager::NumTrFunctions; ++i)
108 result.push_back(t: defaultTrFunctionNames[i]);
109 return result;
110}
111
112QStringList TrFunctionAliasManager::availableFunctionsWithAliases() const
113{
114 QStringList result;
115 result.reserve(asize: NumTrFunctions);
116 for (int i = 0; i < NumTrFunctions; ++i)
117 result.push_back(t: defaultTrFunctionNames[i] +
118 QLatin1String(" (=") +
119 m_trFunctionAliases[i].join(sep: QLatin1Char('=')) +
120 QLatin1Char(')'));
121 return result;
122}
123
124QStringList TrFunctionAliasManager::listAliases() const
125{
126 QStringList result;
127 result.reserve(asize: NumTrFunctions);
128 for (int i = 0; i < NumTrFunctions; ++i) {
129 for (int ii = 1; ii < m_trFunctionAliases[i].size() ; ii++) {
130 // ii = 0 is the default name. Not listed here
131 result.push_back(t: m_trFunctionAliases[i][ii]);
132 }
133 }
134 return result;
135}
136
137TrFunctionAliasManager trFunctionAliasManager;
138
139QString ParserTool::transcode(const QString &str)
140{
141 static const char tab[] = "abfnrtv";
142 static const char backTab[] = "\a\b\f\n\r\t\v";
143 // This function has to convert back to bytes, as C's \0* sequences work at that level.
144 const QByteArray in = str.toUtf8();
145 QByteArray out;
146
147 out.reserve(asize: in.size());
148 for (int i = 0; i < in.size();) {
149 uchar c = in[i++];
150 if (c == '\\') {
151 if (i >= in.size())
152 break;
153 c = in[i++];
154
155 if (c == '\n')
156 continue;
157
158 if (c == 'x' || c == 'u' || c == 'U') {
159 const bool unicode = (c != 'x');
160 QByteArray hex;
161 while (i < in.size() && isxdigit((c = in[i]))) {
162 hex += c;
163 i++;
164 }
165 if (unicode)
166 out += QString(QChar(hex.toUInt(ok: nullptr, base: 16))).toUtf8();
167 else
168 out += hex.toUInt(ok: nullptr, base: 16);
169 } else if (c >= '0' && c < '8') {
170 QByteArray oct;
171 int n = 0;
172 oct += c;
173 while (n < 2 && i < in.size() && (c = in[i]) >= '0' && c < '8') {
174 i++;
175 n++;
176 oct += c;
177 }
178 out += oct.toUInt(ok: 0, base: 8);
179 } else {
180 const char *p = strchr(s: tab, c: c);
181 out += !p ? c : backTab[p - tab];
182 }
183 } else {
184 out += c;
185 }
186 }
187 return QString::fromUtf8(utf8: out.constData(), size: out.size());
188}
189
190static QString m_defaultExtensions;
191
192static void printOut(const QString & out)
193{
194 std::cout << qPrintable(out);
195}
196
197static void printErr(const QString & out)
198{
199 std::cerr << qPrintable(out);
200}
201
202static void recursiveFileInfoList(const QDir &dir,
203 const QSet<QString> &nameFilters, QDir::Filters filter,
204 QFileInfoList *fileinfolist)
205{
206 for (const QFileInfo &fi : dir.entryInfoList(filters: filter))
207 if (fi.isDir())
208 recursiveFileInfoList(dir: QDir(fi.absoluteFilePath()), nameFilters, filter, fileinfolist);
209 else if (nameFilters.contains(value: fi.suffix()))
210 fileinfolist->append(t: fi);
211}
212
213static void printUsage()
214{
215 printOut(QStringLiteral(
216 "Usage:\n"
217 " lupdate [options] [project-file]...\n"
218 " lupdate [options] [source-file|path|@lst-file]... -ts ts-files|@lst-file\n\n"
219 "lupdate is part of Qt's Linguist tool chain. It extracts translatable\n"
220 "messages from Qt UI files, C++, Java and JavaScript/QtScript source code.\n"
221 "Extracted messages are stored in textual translation source files (typically\n"
222 "Qt TS XML). New and modified messages can be merged into existing TS files.\n\n"
223 "Passing .pro files to lupdate is deprecated.\n"
224 "Please use the lupdate-pro tool instead.\n\n"
225 "Options:\n"
226 " -help Display this information and exit.\n"
227 " -no-obsolete\n"
228 " Drop all obsolete and vanished strings.\n"
229 " -extensions <ext>[,<ext>]...\n"
230 " Process files with the given extensions only.\n"
231 " The extension list must be separated with commas, not with whitespace.\n"
232 " Default: '%1'.\n"
233 " -pluralonly\n"
234 " Only include plural form messages.\n"
235 " -silent\n"
236 " Do not explain what is being done.\n"
237 " -no-sort\n"
238 " Do not sort contexts in TS files.\n"
239 " -no-recursive\n"
240 " Do not recursively scan directories.\n"
241 " -recursive\n"
242 " Recursively scan directories (default).\n"
243 " -I <includepath> or -I<includepath>\n"
244 " Additional location to look for include files.\n"
245 " May be specified multiple times.\n"
246 " -locations {absolute|relative|none}\n"
247 " Specify/override how source code references are saved in TS files.\n"
248 " absolute: Source file path is relative to target file. Absolute line\n"
249 " number is stored.\n"
250 " relative: Source file path is relative to target file. Line number is\n"
251 " relative to other entries in the same source file.\n"
252 " none: no information about source location is stored.\n"
253 " Guessed from existing TS files if not specified.\n"
254 " Default is absolute for new files.\n"
255 " -no-ui-lines\n"
256 " Do not record line numbers in references to UI files.\n"
257 " -disable-heuristic {sametext|similartext|number}\n"
258 " Disable the named merge heuristic. Can be specified multiple times.\n"
259 " -project <filename>\n"
260 " Name of a file containing the project's description in JSON format.\n"
261 " Such a file may be generated from a .pro file using the lprodump tool.\n"
262 " -pro <filename>\n"
263 " Name of a .pro file. Useful for files with .pro file syntax but\n"
264 " different file suffix. Projects are recursed into and merged.\n"
265 " This option is deprecated. Use the lupdate-pro tool instead.\n"
266 " -pro-out <directory>\n"
267 " Virtual output directory for processing subsequent .pro files.\n"
268 " -pro-debug\n"
269 " Trace processing .pro files. Specify twice for more verbosity.\n"
270 " -source-language <language>[_<region>]\n"
271 " Specify the language of the source strings for new files.\n"
272 " Defaults to POSIX if not specified.\n"
273 " -target-language <language>[_<region>]\n"
274 " Specify the language of the translations for new files.\n"
275 " Guessed from the file name if not specified.\n"
276 " -tr-function-alias <function>{+=,=}<alias>[,<function>{+=,=}<alias>]...\n"
277 " With +=, recognize <alias> as an alternative spelling of <function>.\n"
278 " With =, recognize <alias> as the only spelling of <function>.\n"
279 " Available <function>s (with their currently defined aliases) are:\n"
280 " %2\n"
281 " -ts <ts-file>...\n"
282 " Specify the output file(s). This will override the TRANSLATIONS.\n"
283 " -version\n"
284 " Display the version of lupdate and exit.\n"
285 " -clang-parser [compilation-database-dir]\n"
286 " Use clang to parse cpp files. Otherwise a custom parser is used.\n"
287 " This option needs a clang compilation database (compile_commands.json)\n"
288 " for the files that needs to be parsed.\n"
289 " The path to the directory containing this file can be specified on the \n"
290 " command line, directly after the -clang-parser option, or in the .pro file\n"
291 " by setting the variable LUPDATE_COMPILE_COMMANDS_PATH.\n"
292 " A directory specified on the command line takes precedence.\n"
293 " If no path is given, the compilation database will be searched\n"
294 " in all parent paths of the first input file.\n"
295 " -project-roots <directory>...\n"
296 " Specify one or more project root directories.\n"
297 " Only files below a project root are considered for translation when using\n"
298 " the -clang-parser option.\n"
299 " @lst-file\n"
300 " Read additional file names (one per line) or includepaths (one per\n"
301 " line, and prefixed with -I) from lst-file.\n"
302 ).arg(args&: m_defaultExtensions,
303 args: trFunctionAliasManager.availableFunctionsWithAliases()
304 .join(sep: QLatin1String("\n "))));
305}
306
307static bool handleTrFunctionAliases(const QString &arg)
308{
309 for (const QString &pair : arg.split(sep: QLatin1Char(','), behavior: Qt::SkipEmptyParts)) {
310 const int equalSign = pair.indexOf(c: QLatin1Char('='));
311 if (equalSign < 0) {
312 printErr(QStringLiteral("tr-function mapping '%1' in -tr-function-alias is missing the '='.\n").arg(a: pair));
313 return false;
314 }
315 const bool plusEqual = equalSign > 0 && pair[equalSign-1] == QLatin1Char('+');
316 const int trFunctionEnd = plusEqual ? equalSign-1 : equalSign;
317 const QString trFunctionName = pair.left(n: trFunctionEnd).trimmed();
318 const QString alias = pair.mid(position: equalSign+1).trimmed();
319 const int trFunction = trFunctionByDefaultName(trFunctionName);
320 if (trFunction < 0) {
321 printErr(QStringLiteral("Unknown tr-function '%1' in -tr-function-alias option.\n"
322 "Available tr-functions are: %2")
323 .arg(args: trFunctionName, args: availableFunctions().join(sep: QLatin1Char(','))));
324 return false;
325 }
326 if (alias.isEmpty()) {
327 printErr(QStringLiteral("Empty alias for tr-function '%1' in -tr-function-alias option.\n")
328 .arg(a: trFunctionName));
329 return false;
330 }
331 trFunctionAliasManager.modifyAlias(trFunction, alias,
332 op: plusEqual ? TrFunctionAliasManager::AddAlias : TrFunctionAliasManager::SetAlias);
333 }
334 return true;
335}
336
337static void updateTsFiles(const Translator &fetchedTor, const QStringList &tsFileNames,
338 const QStringList &alienFiles,
339 const QString &sourceLanguage, const QString &targetLanguage,
340 UpdateOptions options, bool *fail)
341{
342 for (int i = 0; i < fetchedTor.messageCount(); i++) {
343 const TranslatorMessage &msg = fetchedTor.constMessage(i);
344 if (!msg.id().isEmpty() && msg.sourceText().isEmpty())
345 printErr(QStringLiteral("lupdate warning: Message with id '%1' has no source.\n")
346 .arg(a: msg.id()));
347 }
348
349 QList<Translator> aliens;
350 for (const QString &fileName : alienFiles) {
351 ConversionData cd;
352 Translator tor;
353 if (!tor.load(filename: fileName, err&: cd, format: QLatin1String("auto"))) {
354 printErr(out: cd.error());
355 *fail = true;
356 continue;
357 }
358 tor.resolveDuplicates();
359 aliens << tor;
360 }
361
362 QDir dir;
363 QString err;
364 for (const QString &fileName : tsFileNames) {
365 QString fn = dir.relativeFilePath(fileName);
366 ConversionData cd;
367 Translator tor;
368 cd.m_sortContexts = !(options & NoSort);
369 if (QFile(fileName).exists()) {
370 if (!tor.load(filename: fileName, err&: cd, format: QLatin1String("auto"))) {
371 printErr(out: cd.error());
372 *fail = true;
373 continue;
374 }
375 tor.resolveDuplicates();
376 cd.clearErrors();
377 if (!targetLanguage.isEmpty() && targetLanguage != tor.languageCode())
378 printErr(QStringLiteral("lupdate warning: Specified target language '%1' disagrees with"
379 " existing file's language '%2'. Ignoring.\n")
380 .arg(args: targetLanguage, args: tor.languageCode()));
381 if (!sourceLanguage.isEmpty() && sourceLanguage != tor.sourceLanguageCode())
382 printErr(QStringLiteral("lupdate warning: Specified source language '%1' disagrees with"
383 " existing file's language '%2'. Ignoring.\n")
384 .arg(args: sourceLanguage, args: tor.sourceLanguageCode()));
385 // If there is translation in the file, the language should be recognized
386 // (when the language is not recognized, plural translations are lost)
387 if (tor.translationsExist()) {
388 QLocale::Language l;
389 QLocale::Territory c;
390 tor.languageAndTerritory(languageCode: tor.languageCode(), langPtr: &l, territoryPtr: &c);
391 QStringList forms;
392 if (!getNumerusInfo(language: l, territory: c, rules: 0, forms: &forms, gettextRules: 0)) {
393 printErr(QStringLiteral("File %1 won't be updated: it contains translation but the"
394 " target language is not recognized\n").arg(a: fileName));
395 continue;
396 }
397 }
398 } else {
399 if (!targetLanguage.isEmpty())
400 tor.setLanguageCode(targetLanguage);
401 else
402 tor.setLanguageCode(Translator::guessLanguageCodeFromFileName(fileName));
403 if (!sourceLanguage.isEmpty())
404 tor.setSourceLanguageCode(sourceLanguage);
405 }
406 tor.makeFileNamesAbsolute(originalPath: QFileInfo(fileName).absoluteDir());
407 if (options & NoLocations)
408 tor.setLocationsType(Translator::NoLocations);
409 else if (options & RelativeLocations)
410 tor.setLocationsType(Translator::RelativeLocations);
411 else if (options & AbsoluteLocations)
412 tor.setLocationsType(Translator::AbsoluteLocations);
413 if (options & Verbose)
414 printOut(QStringLiteral("Updating '%1'...\n").arg(a: fn));
415
416 UpdateOptions theseOptions = options;
417 if (tor.locationsType() == Translator::NoLocations) // Could be set from file
418 theseOptions |= NoLocations;
419 Translator out = merge(tor, virginTor: fetchedTor, aliens, options: theseOptions, err);
420
421 if ((options & Verbose) && !err.isEmpty()) {
422 printOut(out: err);
423 err.clear();
424 }
425 if (options & PluralOnly) {
426 if (options & Verbose)
427 printOut(QStringLiteral("Stripping non plural forms in '%1'...\n").arg(a: fn));
428 out.stripNonPluralForms();
429 }
430 if (options & NoObsolete)
431 out.stripObsoleteMessages();
432 out.stripEmptyContexts();
433
434 out.normalizeTranslations(cd);
435 if (!cd.errors().isEmpty()) {
436 printErr(out: cd.error());
437 cd.clearErrors();
438 }
439 if (!out.save(filename: fileName, err&: cd, format: QLatin1String("auto"))) {
440 printErr(out: cd.error());
441 *fail = true;
442 }
443 }
444}
445
446static bool readFileContent(const QString &filePath, QByteArray *content, QString *errorString)
447{
448 QFile file(filePath);
449 if (!file.open(flags: QIODevice::ReadOnly)) {
450 *errorString = file.errorString();
451 return false;
452 }
453 *content = file.readAll();
454 return true;
455}
456
457static bool readFileContent(const QString &filePath, QString *content, QString *errorString)
458{
459 QByteArray ba;
460 if (!readFileContent(filePath, content: &ba, errorString))
461 return false;
462 *content = QString::fromLocal8Bit(ba);
463 return true;
464}
465
466static QStringList getResources(const QString &resourceFile)
467{
468 if (!QFile::exists(fileName: resourceFile))
469 return QStringList();
470 QString content;
471 QString errStr;
472 if (!readFileContent(filePath: resourceFile, content: &content, errorString: &errStr)) {
473 printErr(QStringLiteral("lupdate error: Can not read %1: %2\n").arg(args: resourceFile, args&: errStr));
474 return QStringList();
475 }
476 ReadQrcResult rqr = readQrcFile(resourceFile, content);
477 if (rqr.hasError()) {
478 printErr(QStringLiteral("lupdate error: %1:%2: %3\n")
479 .arg(args: resourceFile, args: QString::number(rqr.line), args&: rqr.errorString));
480 }
481 return rqr.files;
482}
483
484// Remove .qrc files from the project and return them as absolute paths.
485static QStringList extractQrcFiles(Project &project)
486{
487 auto it = project.sources.begin();
488 QStringList qrcFiles;
489 while (it != project.sources.end()) {
490 QFileInfo fi(*it);
491 QString fn = QDir::cleanPath(path: fi.absoluteFilePath());
492 if (fn.endsWith(s: QLatin1String(".qrc"), cs: Qt::CaseInsensitive)) {
493 qrcFiles += fn;
494 it = project.sources.erase(pos: it);
495 } else {
496 ++it;
497 }
498 }
499 return qrcFiles;
500}
501
502// Replace all .qrc files in the project with their content.
503static void expandQrcFiles(Project &project)
504{
505 for (const QString &qrcFile : extractQrcFiles(project))
506 project.sources << getResources(resourceFile: qrcFile);
507}
508
509static bool processTs(Translator &fetchedTor, const QString &file, ConversionData &cd)
510{
511 for (const Translator::FileFormat &fmt : std::as_const(t&: Translator::registeredFileFormats())) {
512 if (file.endsWith(s: QLatin1Char('.') + fmt.extension, cs: Qt::CaseInsensitive)) {
513 Translator tor;
514 if (tor.load(filename: file, err&: cd, format: fmt.extension)) {
515 for (TranslatorMessage msg : tor.messages()) {
516 msg.setType(TranslatorMessage::Unfinished);
517 msg.setTranslations(QStringList());
518 msg.setTranslatorComment(QString());
519 fetchedTor.extend(msg, cd);
520 }
521 }
522 return true;
523 }
524 }
525 return false;
526}
527
528static void processSources(Translator &fetchedTor,
529 const QStringList &sourceFiles, ConversionData &cd, bool *fail)
530{
531#ifdef QT_NO_QML
532 bool requireQmlSupport = false;
533#endif
534 QStringList sourceFilesCpp;
535 for (const auto &sourceFile : sourceFiles) {
536 if (sourceFile.endsWith(s: QLatin1String(".java"), cs: Qt::CaseInsensitive))
537 loadJava(translator&: fetchedTor, filename: sourceFile, cd);
538 else if (sourceFile.endsWith(s: QLatin1String(".ui"), cs: Qt::CaseInsensitive)
539 || sourceFile.endsWith(s: QLatin1String(".jui"), cs: Qt::CaseInsensitive))
540 loadUI(translator&: fetchedTor, filename: sourceFile, cd);
541#ifndef QT_NO_QML
542 else if (sourceFile.endsWith(s: QLatin1String(".js"), cs: Qt::CaseInsensitive)
543 || sourceFile.endsWith(s: QLatin1String(".qs"), cs: Qt::CaseInsensitive))
544 loadQScript(translator&: fetchedTor, filename: sourceFile, cd);
545 else if (sourceFile.endsWith(s: QLatin1String(".qml"), cs: Qt::CaseInsensitive))
546 loadQml(translator&: fetchedTor, filename: sourceFile, cd);
547#else
548 else if (sourceFile.endsWith(QLatin1String(".qml"), Qt::CaseInsensitive)
549 || sourceFile.endsWith(QLatin1String(".js"), Qt::CaseInsensitive)
550 || sourceFile.endsWith(QLatin1String(".qs"), Qt::CaseInsensitive))
551 requireQmlSupport = true;
552#endif // QT_NO_QML
553 else if (sourceFile.endsWith(s: u".py", cs: Qt::CaseInsensitive))
554 loadPython(translator&: fetchedTor, fileName: sourceFile, cd);
555 else if (!processTs(fetchedTor, file: sourceFile, cd))
556 sourceFilesCpp << sourceFile;
557 }
558
559#ifdef QT_NO_QML
560 if (requireQmlSupport)
561 printErr(QStringLiteral("lupdate warning: Some files have been ignored due to missing qml/javascript support\n"));
562#endif
563
564 if (useClangToParseCpp) {
565#if QT_CONFIG(clangcpp)
566 ClangCppParser::loadCPP(translator&: fetchedTor, filenames: sourceFilesCpp, cd, fail);
567#else
568 *fail = true;
569 printErr(QStringLiteral("lupdate error: lupdate was built without clang support."));
570#endif
571 }
572 else
573 loadCPP(translator&: fetchedTor, filenames: sourceFilesCpp, cd);
574
575 if (!cd.error().isEmpty())
576 printErr(out: cd.error());
577}
578
579static QSet<QString> projectRoots(const QString &projectFile, const QStringList &sourceFiles)
580{
581 const QString proPath = QFileInfo(projectFile).path();
582 QSet<QString> sourceDirs;
583 sourceDirs.insert(value: proPath + QLatin1Char('/'));
584 for (const QString &sf : sourceFiles)
585 sourceDirs.insert(value: sf.left(n: sf.lastIndexOf(c: QLatin1Char('/')) + 1));
586 QStringList rootList = sourceDirs.values();
587 rootList.sort();
588 for (int prev = 0, curr = 1; curr < rootList.size(); )
589 if (rootList.at(i: curr).startsWith(s: rootList.at(i: prev)))
590 rootList.removeAt(i: curr);
591 else
592 prev = curr++;
593 return QSet<QString>(rootList.cbegin(), rootList.cend());
594}
595
596class ProjectProcessor
597{
598public:
599 ProjectProcessor(const QString &sourceLanguage,
600 const QString &targetLanguage)
601 : m_sourceLanguage(sourceLanguage),
602 m_targetLanguage(targetLanguage)
603 {
604 }
605
606 void processProjects(bool topLevel, UpdateOptions options, const Projects &projects,
607 bool nestComplain, Translator *parentTor, bool *fail) const
608 {
609 for (const Project &prj : projects)
610 processProject(options, prj, topLevel, nestComplain, parentTor, fail);
611 }
612
613private:
614 void processProject(UpdateOptions options, const Project &prj, bool topLevel,
615 bool nestComplain, Translator *parentTor, bool *fail) const
616 {
617 QString codecForSource = prj.codec.toLower();
618 if (!codecForSource.isEmpty()) {
619 if (codecForSource == QLatin1String("utf-16")
620 || codecForSource == QLatin1String("utf16")) {
621 options |= SourceIsUtf16;
622 } else if (codecForSource == QLatin1String("utf-8")
623 || codecForSource == QLatin1String("utf8")) {
624 options &= ~SourceIsUtf16;
625 } else {
626 printErr(QStringLiteral("lupdate warning: Codec for source '%1' is invalid."
627 " Falling back to UTF-8.\n").arg(a: codecForSource));
628 options &= ~SourceIsUtf16;
629 }
630 }
631
632 const QString projectFile = prj.filePath;
633 const QStringList sources = prj.sources;
634 ConversionData cd;
635 cd.m_noUiLines = options & NoUiLines;
636 cd.m_projectRoots = projectRoots(projectFile, sourceFiles: sources);
637 QStringList projectRootDirs;
638 for (auto dir : cd.m_projectRoots)
639 projectRootDirs.append(t: dir);
640 cd.m_rootDirs = projectRootDirs;
641 cd.m_includePath = prj.includePaths;
642 cd.m_excludes = prj.excluded;
643 cd.m_sourceIsUtf16 = options & SourceIsUtf16;
644 if (commandLineCompilationDatabaseDir.isEmpty())
645 cd.m_compilationDatabaseDir = prj.compileCommands;
646 else
647 cd.m_compilationDatabaseDir = commandLineCompilationDatabaseDir;
648
649 QStringList tsFiles;
650 if (prj.translations) {
651 tsFiles = *prj.translations;
652 if (parentTor) {
653 if (topLevel) {
654 printErr(QStringLiteral("lupdate warning: TS files from command line "
655 "will override TRANSLATIONS in %1.\n").arg(a: projectFile));
656 goto noTrans;
657 } else if (nestComplain) {
658 printErr(QStringLiteral("lupdate warning: TS files from command line "
659 "prevent recursing into %1.\n").arg(a: projectFile));
660 return;
661 }
662 }
663 if (tsFiles.isEmpty()) {
664 // This might mean either a buggy PRO file or an intentional detach -
665 // we can't know without seeing the actual RHS of the assignment ...
666 // Just assume correctness and be silent.
667 return;
668 }
669 Translator tor;
670 processProjects(topLevel: false, options, projects: prj.subProjects, nestComplain: false, parentTor: &tor, fail);
671 processSources(fetchedTor&: tor, sourceFiles: sources, cd, fail);
672 updateTsFiles(fetchedTor: tor, tsFileNames: tsFiles, alienFiles: QStringList(), sourceLanguage: m_sourceLanguage, targetLanguage: m_targetLanguage,
673 options, fail);
674 return;
675 }
676
677 noTrans:
678 if (!parentTor) {
679 if (topLevel) {
680 printErr(QStringLiteral("lupdate warning: no TS files specified. Only diagnostics "
681 "will be produced for '%1'.\n").arg(a: projectFile));
682 }
683 Translator tor;
684 processProjects(topLevel: false, options, projects: prj.subProjects, nestComplain, parentTor: &tor, fail);
685 processSources(fetchedTor&: tor, sourceFiles: sources, cd, fail);
686 } else {
687 processProjects(topLevel: false, options, projects: prj.subProjects, nestComplain, parentTor, fail);
688 processSources(fetchedTor&: *parentTor, sourceFiles: sources, cd, fail);
689 }
690 }
691
692 QString m_sourceLanguage;
693 QString m_targetLanguage;
694};
695
696int main(int argc, char **argv)
697{
698 QCoreApplication app(argc, argv);
699#ifndef QT_BOOTSTRAPPED
700#ifndef Q_OS_WIN32
701 QTranslator translator;
702 QTranslator qtTranslator;
703 QString sysLocale = QLocale::system().name();
704 QString resourceDir = QLibraryInfo::path(p: QLibraryInfo::TranslationsPath);
705 if (translator.load(filename: QLatin1String("linguist_") + sysLocale, directory: resourceDir)
706 && qtTranslator.load(filename: QLatin1String("qt_") + sysLocale, directory: resourceDir)) {
707 app.installTranslator(messageFile: &translator);
708 app.installTranslator(messageFile: &qtTranslator);
709 }
710#endif // Q_OS_WIN32
711#endif
712
713 m_defaultExtensions = QLatin1String("java,jui,ui,c,c++,cc,cpp,cxx,ch,h,h++,hh,hpp,hxx,js,qs,qml,qrc");
714
715 QStringList args = app.arguments();
716 QStringList tsFileNames;
717 QStringList proFiles;
718 QString projectDescriptionFile;
719 QString outDir = QDir::currentPath();
720 QMultiHash<QString, QString> allCSources;
721 QSet<QString> projectRoots;
722 QStringList sourceFiles;
723 QStringList resourceFiles;
724 QStringList includePath;
725 QStringList alienFiles;
726 QString targetLanguage;
727 QString sourceLanguage;
728
729 UpdateOptions options =
730 Verbose | // verbose is on by default starting with Qt 4.2
731 HeuristicSameText | HeuristicSimilarText | HeuristicNumber;
732 int numFiles = 0;
733 bool metTsFlag = false;
734 bool metXTsFlag = false;
735 bool recursiveScan = true;
736
737 QString extensions = m_defaultExtensions;
738 QSet<QString> extensionsNameFilters;
739
740 for (int i = 1; i < args.size(); ++i) {
741 QString arg = args.at(i);
742 if (arg == QLatin1String("-help")
743 || arg == QLatin1String("--help")
744 || arg == QLatin1String("-h")) {
745 printUsage();
746 return 0;
747 } else if (arg == QLatin1String("-list-languages")) {
748 printOut(out: getNumerusInfoString());
749 return 0;
750 } else if (arg == QLatin1String("-pluralonly")) {
751 options |= PluralOnly;
752 continue;
753 } else if (arg == QLatin1String("-noobsolete")
754 || arg == QLatin1String("-no-obsolete")) {
755 options |= NoObsolete;
756 continue;
757 } else if (arg == QLatin1String("-silent")) {
758 options &= ~Verbose;
759 continue;
760 } else if (arg == QLatin1String("-pro-debug")) {
761 continue;
762 } else if (arg == QLatin1String("-project")) {
763 ++i;
764 if (i == argc) {
765 printErr(out: u"The option -project requires a parameter.\n"_s);
766 return 1;
767 }
768 if (!projectDescriptionFile.isEmpty()) {
769 printErr(out: u"The option -project must appear only once.\n"_s);
770 return 1;
771 }
772 projectDescriptionFile = args[i];
773 numFiles++;
774 continue;
775 } else if (arg == QLatin1String("-target-language")) {
776 ++i;
777 if (i == argc) {
778 printErr(out: u"The option -target-language requires a parameter.\n"_s);
779 return 1;
780 }
781 targetLanguage = args[i];
782 continue;
783 } else if (arg == QLatin1String("-source-language")) {
784 ++i;
785 if (i == argc) {
786 printErr(out: u"The option -source-language requires a parameter.\n"_s);
787 return 1;
788 }
789 sourceLanguage = args[i];
790 continue;
791 } else if (arg == QLatin1String("-disable-heuristic")) {
792 ++i;
793 if (i == argc) {
794 printErr(out: u"The option -disable-heuristic requires a parameter.\n"_s);
795 return 1;
796 }
797 arg = args[i];
798 if (arg == QLatin1String("sametext")) {
799 options &= ~HeuristicSameText;
800 } else if (arg == QLatin1String("similartext")) {
801 options &= ~HeuristicSimilarText;
802 } else if (arg == QLatin1String("number")) {
803 options &= ~HeuristicNumber;
804 } else {
805 printErr(out: u"Invalid heuristic name passed to -disable-heuristic.\n"_s);
806 return 1;
807 }
808 continue;
809 } else if (arg == QLatin1String("-locations")) {
810 ++i;
811 if (i == argc) {
812 printErr(out: u"The option -locations requires a parameter.\n"_s);
813 return 1;
814 }
815 if (args[i] == QLatin1String("none")) {
816 options |= NoLocations;
817 } else if (args[i] == QLatin1String("relative")) {
818 options |= RelativeLocations;
819 } else if (args[i] == QLatin1String("absolute")) {
820 options |= AbsoluteLocations;
821 } else {
822 printErr(out: u"Invalid parameter passed to -locations.\n"_s);
823 return 1;
824 }
825 continue;
826 } else if (arg == QLatin1String("-no-ui-lines")) {
827 options |= NoUiLines;
828 continue;
829 } else if (arg == QLatin1String("-verbose")) {
830 options |= Verbose;
831 continue;
832 } else if (arg == QLatin1String("-no-recursive")) {
833 recursiveScan = false;
834 continue;
835 } else if (arg == QLatin1String("-recursive")) {
836 recursiveScan = true;
837 continue;
838 } else if (arg == QLatin1String("-no-sort")
839 || arg == QLatin1String("-nosort")) {
840 options |= NoSort;
841 continue;
842 } else if (arg == QLatin1String("-version")) {
843 printOut(QStringLiteral("lupdate version %1\n").arg(a: QLatin1String(QT_VERSION_STR)));
844 return 0;
845 } else if (arg == QLatin1String("-ts")) {
846 metTsFlag = true;
847 metXTsFlag = false;
848 continue;
849 } else if (arg == QLatin1String("-xts")) {
850 metTsFlag = false;
851 metXTsFlag = true;
852 continue;
853 } else if (arg == QLatin1String("-extensions")) {
854 ++i;
855 if (i == argc) {
856 printErr(out: u"The -extensions option should be followed by an extension list.\n"_s);
857 return 1;
858 }
859 extensions = args[i];
860 continue;
861 } else if (arg == QLatin1String("-tr-function-alias")) {
862 ++i;
863 if (i == argc) {
864 printErr(out: u"The -tr-function-alias option should be followed by a list of function=alias mappings.\n"_s);
865 return 1;
866 }
867 if (!handleTrFunctionAliases(arg: args[i]))
868 return 1;
869 continue;
870 } else if (arg == QLatin1String("-pro")) {
871 ++i;
872 if (i == argc) {
873 printErr(out: u"The -pro option should be followed by a filename of .pro file.\n"_s);
874 return 1;
875 }
876 QString file = QDir::cleanPath(path: QFileInfo(args[i]).absoluteFilePath());
877 proFiles += file;
878 numFiles++;
879 continue;
880 } else if (arg == QLatin1String("-pro-out")) {
881 ++i;
882 if (i == argc) {
883 printErr(out: u"The -pro-out option should be followed by a directory name.\n"_s);
884 return 1;
885 }
886 outDir = QDir::cleanPath(path: QFileInfo(args[i]).absoluteFilePath());
887 continue;
888 } else if (arg.startsWith(s: QLatin1String("-I"))) {
889 if (arg.size() == 2) {
890 ++i;
891 if (i == argc) {
892 printErr(out: u"The -I option should be followed by a path.\n"_s);
893 return 1;
894 }
895 includePath += args[i];
896 } else {
897 includePath += args[i].mid(position: 2);
898 }
899 continue;
900 }
901#if QT_CONFIG(clangcpp)
902 else if (arg == QLatin1String("-clang-parser")) {
903 useClangToParseCpp = true;
904 // the option after -clang-parser is optional
905 if ((i + 1) != argc && !args[i + 1].startsWith(s: QLatin1String("-"))) {
906 i++;
907 commandLineCompilationDatabaseDir = args[i];
908 }
909 continue;
910 }
911 else if (arg == QLatin1String("-project-roots")) {
912 while ((i + 1) != argc && !args[i + 1].startsWith(s: QLatin1String("-"))) {
913 i++;
914 rootDirs << args[i];
915 }
916 rootDirs.removeDuplicates();
917 continue;
918 }
919#endif
920 else if (arg.startsWith(s: QLatin1String("-")) && arg != QLatin1String("-")) {
921 printErr(QStringLiteral("Unrecognized option '%1'.\n").arg(a: arg));
922 return 1;
923 }
924
925 QStringList files;
926 if (arg.startsWith(s: QLatin1String("@"))) {
927 QFile lstFile(arg.mid(position: 1));
928 if (!lstFile.open(flags: QIODevice::ReadOnly)) {
929 printErr(QStringLiteral("lupdate error: List file '%1' is not readable.\n")
930 .arg(a: lstFile.fileName()));
931 return 1;
932 }
933 while (!lstFile.atEnd()) {
934 QString lineContent = QString::fromLocal8Bit(ba: lstFile.readLine().trimmed());
935
936 if (lineContent.startsWith(s: QLatin1String("-I"))) {
937 if (lineContent.size() == 2) {
938 printErr(out: u"The -I option should be followed by a path.\n"_s);
939 return 1;
940 }
941 includePath += lineContent.mid(position: 2);
942 } else {
943 files << lineContent;
944 }
945 }
946 } else {
947 files << arg;
948 }
949 if (metTsFlag) {
950 for (const QString &file : std::as_const(t&: files)) {
951 bool found = false;
952 for (const Translator::FileFormat &fmt : std::as_const(t&: Translator::registeredFileFormats())) {
953 if (file.endsWith(s: QLatin1Char('.') + fmt.extension, cs: Qt::CaseInsensitive)) {
954 QFileInfo fi(file);
955 if (!fi.exists() || fi.isWritable()) {
956 tsFileNames.append(t: QFileInfo(file).absoluteFilePath());
957 } else {
958 printErr(QStringLiteral("lupdate warning: For some reason, '%1' is not writable.\n")
959 .arg(a: file));
960 }
961 found = true;
962 break;
963 }
964 }
965 if (!found) {
966 printErr(QStringLiteral("lupdate error: File '%1' has no recognized extension.\n")
967 .arg(a: file));
968 return 1;
969 }
970 }
971 numFiles++;
972 } else if (metXTsFlag) {
973 alienFiles += files;
974 } else {
975 for (const QString &file : std::as_const(t&: files)) {
976 QFileInfo fi(file);
977 if (!fi.exists()) {
978 printErr(QStringLiteral("lupdate error: File '%1' does not exist.\n").arg(a: file));
979 return 1;
980 }
981 if (isProOrPriFile(filePath: file)) {
982 QString cleanFile = QDir::cleanPath(path: fi.absoluteFilePath());
983 proFiles << cleanFile;
984 } else if (fi.isDir()) {
985 if (options & Verbose)
986 printOut(QStringLiteral("Scanning directory '%1'...\n").arg(a: file));
987 QDir dir = QDir(fi.filePath());
988 projectRoots.insert(value: dir.absolutePath() + QLatin1Char('/'));
989 if (extensionsNameFilters.isEmpty()) {
990 for (QString ext : extensions.split(sep: QLatin1Char(','))) {
991 ext = ext.trimmed();
992 if (ext.startsWith(c: QLatin1Char('.')))
993 ext.remove(i: 0, len: 1);
994 extensionsNameFilters.insert(value: ext);
995 }
996 }
997 QDir::Filters filters = QDir::Files | QDir::NoSymLinks;
998 if (recursiveScan)
999 filters |= QDir::AllDirs | QDir::NoDotAndDotDot;
1000 QFileInfoList fileinfolist;
1001 recursiveFileInfoList(dir, nameFilters: extensionsNameFilters, filter: filters, fileinfolist: &fileinfolist);
1002 int scanRootLen = dir.absolutePath().size();
1003 for (const QFileInfo &fi : std::as_const(t&: fileinfolist)) {
1004 QString fn = QDir::cleanPath(path: fi.absoluteFilePath());
1005 if (fn.endsWith(s: QLatin1String(".qrc"), cs: Qt::CaseInsensitive)) {
1006 resourceFiles << fn;
1007 } else {
1008 sourceFiles << fn;
1009
1010 if (!fn.endsWith(s: QLatin1String(".java"))
1011 && !fn.endsWith(s: QLatin1String(".jui"))
1012 && !fn.endsWith(s: QLatin1String(".ui"))
1013 && !fn.endsWith(s: QLatin1String(".js"))
1014 && !fn.endsWith(s: QLatin1String(".qs"))
1015 && !fn.endsWith(s: QLatin1String(".qml"))) {
1016 int offset = 0;
1017 int depth = 0;
1018 do {
1019 offset = fn.lastIndexOf(c: QLatin1Char('/'), from: offset - 1);
1020 QString ffn = fn.mid(position: offset + 1);
1021 allCSources.insert(key: ffn, value: fn);
1022 } while (++depth < 3 && offset > scanRootLen);
1023 }
1024 }
1025 }
1026 } else {
1027 QString fn = QDir::cleanPath(path: fi.absoluteFilePath());
1028 if (fn.endsWith(s: QLatin1String(".qrc"), cs: Qt::CaseInsensitive))
1029 resourceFiles << fn;
1030 else
1031 sourceFiles << fn;
1032 projectRoots.insert(value: fi.absolutePath() + QLatin1Char('/'));
1033 }
1034 }
1035 numFiles++;
1036 }
1037 } // for args
1038
1039 if (numFiles == 0) {
1040 printUsage();
1041 return 1;
1042 }
1043
1044 if (!targetLanguage.isEmpty() && tsFileNames.size() != 1)
1045 printErr(out: u"lupdate warning: -target-language usually only"
1046 " makes sense with exactly one TS file.\n"_s);
1047
1048 if (proFiles.isEmpty() && resourceFiles.isEmpty() && sourceFiles.size() == 1
1049 && QFileInfo(sourceFiles.first()).fileName() == u"CMakeLists.txt"_s) {
1050 printErr(out: u"lupdate error: Passing a CMakeLists.txt as project file is not supported.\n"_s
1051 u"Please use the 'qt_add_lupdate' CMake command and build the "_s
1052 u"'update_translations' target.\n"_s);
1053 return 1;
1054 }
1055
1056 QString errorString;
1057 if (!proFiles.isEmpty()) {
1058 runInternalQtTool(toolName: u"lupdate-pro"_s, arguments: app.arguments().mid(pos: 1));
1059 return 0;
1060 }
1061
1062 Projects projectDescription;
1063 if (!projectDescriptionFile.isEmpty()) {
1064 projectDescription = readProjectDescription(filePath: projectDescriptionFile, errorString: &errorString);
1065 if (!errorString.isEmpty()) {
1066 printErr(QStringLiteral("lupdate error: %1\n").arg(a: errorString));
1067 return 1;
1068 }
1069 if (projectDescription.empty()) {
1070 printErr(QStringLiteral("lupdate error:"
1071 " Could not find project descriptions in %1.\n")
1072 .arg(a: projectDescriptionFile));
1073 return 1;
1074 }
1075 for (Project &project : projectDescription)
1076 expandQrcFiles(project);
1077 }
1078
1079 bool fail = false;
1080 if (projectDescription.empty()) {
1081 if (tsFileNames.isEmpty())
1082 printErr(out: u"lupdate warning:"
1083 " no TS files specified. Only diagnostics will be produced.\n"_s);
1084
1085 Translator fetchedTor;
1086 ConversionData cd;
1087 cd.m_noUiLines = options & NoUiLines;
1088 cd.m_sourceIsUtf16 = options & SourceIsUtf16;
1089 cd.m_projectRoots = projectRoots;
1090 cd.m_includePath = includePath;
1091 cd.m_allCSources = allCSources;
1092 cd.m_compilationDatabaseDir = commandLineCompilationDatabaseDir;
1093 cd.m_rootDirs = rootDirs;
1094 for (const QString &resource : std::as_const(t&: resourceFiles))
1095 sourceFiles << getResources(resourceFile: resource);
1096 processSources(fetchedTor, sourceFiles, cd, fail: &fail);
1097 updateTsFiles(fetchedTor, tsFileNames, alienFiles,
1098 sourceLanguage, targetLanguage, options, fail: &fail);
1099 } else {
1100 if (!sourceFiles.isEmpty() || !resourceFiles.isEmpty() || !includePath.isEmpty()) {
1101 printErr(QStringLiteral("lupdate error:"
1102 " Both project and source files / include paths specified.\n"));
1103 return 1;
1104 }
1105 QString errorString;
1106 ProjectProcessor projectProcessor(sourceLanguage, targetLanguage);
1107 if (!tsFileNames.isEmpty()) {
1108 Translator fetchedTor;
1109 projectProcessor.processProjects(topLevel: true, options, projects: projectDescription, nestComplain: true, parentTor: &fetchedTor,
1110 fail: &fail);
1111 if (!fail) {
1112 updateTsFiles(fetchedTor, tsFileNames, alienFiles,
1113 sourceLanguage, targetLanguage, options, fail: &fail);
1114 }
1115 } else {
1116 projectProcessor.processProjects(topLevel: true, options, projects: projectDescription, nestComplain: false, parentTor: nullptr,
1117 fail: &fail);
1118 }
1119 }
1120 return fail ? 1 : 0;
1121}
1122

source code of qttools/src/linguist/lupdate/main.cpp