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

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