Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
projectprocessor.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
5#include "qrcreader.h"
6#include <translator.h>
7#include <trlib/trparser.h>
8
9#include <QtCore/qdir.h>
10#include <QtCore/qfile.h>
11#include <QtCore/qfileinfo.h>
12#include <QtCore/qregularexpression.h>
13#include <QtCore/qset.h>
14
15#include <iostream>
16
17using namespace Qt::StringLiterals;
18
19namespace {
20
21static void printOut(const QString &out)
22{
23 std::cout << qPrintable(out);
24}
25
26static void printErr(const QString &out)
27{
28 std::cerr << qPrintable(out);
29}
30
31static void printWarning(UpdateOptions options,
32 const QString &msg,
33 const QString &warningMsg = {},
34 const QString &errorMsg = {})
35{
36 QString text = msg;
37 if (options & Werror) {
38 text.prepend("lupdate error: "_L1);
39 if (!errorMsg.isEmpty())
40 text.append(" "_L1).append(errorMsg);
41 } else {
42 text.prepend("lupdate warning: "_L1);
43 if (!warningMsg.isEmpty())
44 text.append(" "_L1).append(warningMsg);
45 }
46
47 printErr(text);
48}
49
50static bool readFileContent(const QString &filePath, QByteArray *content, QString *errorString)
51{
52 QFile file(filePath);
53 if (!file.open(QIODevice::ReadOnly)) {
54 *errorString = file.errorString();
55 return false;
56 }
57 *content = file.readAll();
58 return true;
59}
60
61static bool readFileContent(const QString &filePath, QString *content, QString *errorString)
62{
63 QByteArray ba;
64 if (!readFileContent(filePath, &ba, errorString))
65 return false;
66 *content = QString::fromLocal8Bit(ba);
67 return true;
68}
69
70static void removeExcludedSources(Projects &projects)
71{
72 for (Project &project : projects) {
73 for (const QRegularExpression &rx : std::as_const(project.excluded)) {
74 for (auto it = project.sources.begin(); it != project.sources.end(); ) {
75 if (rx.match(*it).hasMatch())
76 it = project.sources.erase(it);
77 else
78 ++it;
79 }
80 }
81 removeExcludedSources(project.subProjects);
82 }
83}
84
85
86// Remove .qrc files from the project and return them as absolute paths.
87static QStringList extractQrcFiles(Project &project)
88{
89 auto it = project.sources.begin();
90 QStringList qrcFiles;
91 while (it != project.sources.end()) {
92 QFileInfo fi(*it);
93 QString fn = QDir::cleanPath(fi.absoluteFilePath());
94 if (fn.endsWith(".qrc"_L1, Qt::CaseInsensitive)) {
95 qrcFiles += fn;
96 it = project.sources.erase(it);
97 } else {
98 ++it;
99 }
100 }
101 return qrcFiles;
102}
103
104// Replace all .qrc files in the project with their content.
105static void expandQrcFiles(Project &project)
106{
107 for (const QString &qrcFile : extractQrcFiles(project))
108 project.sources << getSourceFilesFromQrc(qrcFile);
109}
110
111static QSet<QString> projectRoots(const QString &projectFile, const QStringList &sourceFiles)
112{
113 const QString proPath = QFileInfo(projectFile).path();
114 QSet<QString> sourceDirs;
115 sourceDirs.insert(proPath + u'/');
116 for (const QString &sf : sourceFiles)
117 sourceDirs.insert(sf.left(sf.lastIndexOf(u'/') + 1));
118 QStringList rootList = sourceDirs.values();
119 rootList.sort();
120 for (int prev = 0, curr = 1; curr < rootList.size(); )
121 if (rootList.at(curr).startsWith(rootList.at(prev)))
122 rootList.removeAt(curr);
123 else
124 prev = curr++;
125 return QSet<QString>(rootList.cbegin(), rootList.cend());
126}
127
128static bool updateTsFiles(const Translator &fetchedTor, const QStringList &tsFileNames,
129 const QStringList &alienFiles,
130 const QString &sourceLanguage, const QString &targetLanguage,
131 UpdateOptions options)
132{
133 bool fail = false;
134 for (int i = 0; i < fetchedTor.messageCount(); i++) {
135 const TranslatorMessage &msg = fetchedTor.constMessage(i);
136 if (!msg.id().isEmpty() && msg.sourceText().isEmpty()) {
137 printWarning(options,
138 "Message with id '%1' has no source.\n"_L1.arg(msg.id()));
139 if (options & Werror)
140 return !fail;
141 }
142 }
143 QList<Translator> aliens;
144 for (const QString &fileName : alienFiles) {
145 ConversionData cd;
146 Translator tor;
147 if (!tor.load(fileName, cd, "auto"_L1)) {
148 printErr(cd.error());
149 fail = true;
150 continue;
151 }
152 tor.resolveDuplicates();
153 aliens << tor;
154 }
155 QDir dir;
156 QString err;
157 for (const QString &fileName : tsFileNames) {
158 QString fn = dir.relativeFilePath(fileName);
159 ConversionData cd;
160 Translator tor;
161 cd.m_sortContexts = !(options & NoSort);
162 cd.m_sortMessages = options & SortMessages;
163 if (QFile(fileName).exists()) {
164 if (!tor.load(fileName, cd, "auto"_L1)) {
165 printErr(cd.error());
166 fail = true;
167 continue;
168 }
169 tor.resolveDuplicates();
170 cd.clearErrors();
171 if (!targetLanguage.isEmpty() && targetLanguage != tor.languageCode()) {
172 printWarning(options,
173 "Specified target language '%1' disagrees with"
174 " existing file's language '%2'.\n"_L1
175 .arg(targetLanguage, tor.languageCode()),
176 u"Ignoring.\n"_s);
177 if (options & Werror)
178 return !fail;
179 }
180 if (!sourceLanguage.isEmpty() && sourceLanguage != tor.sourceLanguageCode()) {
181 printWarning(options,
182 "Specified source language '%1' disagrees with"
183 " existing file's language '%2'.\n"_L1
184 .arg(sourceLanguage, tor.sourceLanguageCode()),
185 u"Ignoring.\n"_s);
186 if (options & Werror)
187 return !fail;
188 }
189 // If there is translation in the file, the language should be recognized
190 // (when the language is not recognized, plural translations are lost)
191 if (tor.translationsExist()) {
192
193 if (tor.languageCode().isEmpty()) {
194 printErr("File %1 won't be updated: it does not specify any "
195 "target languages. To set a target language, open "
196 "the file in Qt Linguist.\n"_L1.arg(fileName));
197 continue;
198 }
199 QLocale::Language l;
200 QLocale::Territory c;
201 tor.languageAndTerritory(tor.languageCode(), &l, &c);
202 QStringList forms;
203 if (!getNumerusInfo(l, c, 0, &forms, 0)) {
204 printErr(QStringLiteral("File %1 won't be updated: it contains translation but the"
205 " target language is not recognized\n").arg(fileName));
206 continue;
207 }
208 }
209 } else {
210 if (!targetLanguage.isEmpty())
211 tor.setLanguageCode(targetLanguage);
212 else
213 tor.setLanguageCode(Translator::guessLanguageCodeFromFileName(fileName));
214 if (!sourceLanguage.isEmpty())
215 tor.setSourceLanguageCode(sourceLanguage);
216 }
217 tor.makeFileNamesAbsolute(QFileInfo(fileName).absoluteDir());
218 if (options & NoLocations)
219 tor.setLocationsType(Translator::NoLocations);
220 else if (options & RelativeLocations)
221 tor.setLocationsType(Translator::RelativeLocations);
222 else if (options & AbsoluteLocations)
223 tor.setLocationsType(Translator::AbsoluteLocations);
224 if (options & Verbose)
225 printOut(QStringLiteral("Updating '%1'...\n").arg(fn));
226
227 UpdateOptions theseOptions = options;
228 if (tor.locationsType() == Translator::NoLocations) // Could be set from file
229 theseOptions |= NoLocations;
230 Translator out = merge(tor, fetchedTor, aliens, theseOptions, err);
231
232 if ((options & Verbose) && !err.isEmpty()) {
233 printOut(err);
234 err.clear();
235 }
236 if (options & PluralOnly) {
237 if (options & Verbose)
238 printOut(QStringLiteral("Stripping non plural forms in '%1'...\n").arg(fn));
239 out.stripNonPluralForms();
240 }
241 if (options & NoObsolete)
242 out.stripObsoleteMessages();
243 out.stripEmptyContexts();
244
245 out.normalizeTranslations(cd);
246 if (!cd.errors().isEmpty()) {
247 printErr(cd.error());
248 cd.clearErrors();
249 }
250 if (!out.save(fileName, cd, "auto"_L1)) {
251 printErr(cd.error());
252 fail = true;
253 }
254 }
255 return !fail;
256}
257
258class ProjectProcessor
259{
260public:
261 ProjectProcessor(const QString &sourceLanguage,
262 const QString &targetLanguage)
263 : m_sourceLanguage(sourceLanguage),
264 m_targetLanguage(targetLanguage)
265 {
266 }
267
268 bool processProjects(bool topLevel, UpdateOptions options, const Projects &projects,
269 bool nestComplain, Translator *parentTor) const
270 {
271 bool ok = true;
272 for (const Project &prj : projects)
273 ok &= processProject(options, prj, topLevel, nestComplain, parentTor);
274 return ok;
275 }
276
277private:
278
279 bool processProject(UpdateOptions options, const Project &prj, bool topLevel,
280 bool nestComplain, Translator *parentTor) const
281 {
282 bool ok = true;
283 QString codecForSource = prj.codec.toLower();
284 if (!codecForSource.isEmpty()) {
285 if (codecForSource == "utf-16"_L1 || codecForSource == "utf16"_L1) {
286 options |= SourceIsUtf16;
287 } else if (codecForSource == "utf-8"_L1 || codecForSource == "utf8"_L1) {
288 options &= ~SourceIsUtf16;
289 } else {
290 printWarning(
291 options,
292 "Codec for source '%1' is invalid.\n"_L1.arg(codecForSource),
293 u"Falling back to UTF-8.\n"_s);
294 if (options & Werror)
295 return ok;
296 options &= ~SourceIsUtf16;
297 }
298 }
299
300 const QString projectFile = prj.filePath;
301 const QStringList sources = prj.sources;
303 cd.m_noUiLines = options & NoUiLines;
304 cd.m_projectRoots = projectRoots(projectFile, sources);
305 QStringList projectRootDirs;
306 for (const auto& dir : std::as_const(cd.m_projectRoots))
307 projectRootDirs.append(dir);
308 cd.m_includePath = prj.includePaths;
309 cd.m_excludes = prj.excluded;
310 cd.m_sourceIsUtf16 = options & SourceIsUtf16;
311
312 QStringList tsFiles;
313 if (prj.translations) {
314 tsFiles = *prj.translations;
315 if (parentTor) {
316 if (topLevel) {
317 printWarning(options, u"Existing top level."_s,
318 "TS files from command line will "
319 "override TRANSLATIONS in %1.\n"_L1.arg(projectFile),
320 u"Terminating the operation.\n"_s);
321 if (options & Werror)
322 return ok;
323 goto noTrans;
324 } else if (nestComplain) {
325 printWarning(options,
326 "TS files from command line "
327 "prevent recursing into %1.\n"_L1.arg(projectFile));
328 return ok;
329 }
330 }
331 if (tsFiles.isEmpty()) {
332 // This might mean either a buggy PRO file or an intentional detach -
333 // we can't know without seeing the actual RHS of the assignment ...
334 // Just assume correctness and be silent.
335 return ok;
336 }
337 Translator tor;
338 ok = processProjects(false, options, prj.subProjects, false, &tor);
339 processSources(tor, sources, cd);
340 ok &= updateTsFiles(tor, tsFiles, QStringList(), m_sourceLanguage, m_targetLanguage,
341 options);
342 return ok;
343 }
344
345
346 noTrans:
347 if (!parentTor) {
348 if (topLevel) {
349 printWarning(options, u"no TS files specified."_s,
350 "Only diagnostics will be produced for %1.\n"_L1.arg(projectFile),
351 u"Terminating the operation.\n"_s);
352 if (options & Werror)
353 return ok;
354 }
355 Translator tor;
356 ok = processProjects(false, options, prj.subProjects, nestComplain, &tor);
357 processSources(tor, sources, cd);
358 } else {
359 ok = processProjects(false, options, prj.subProjects, nestComplain, parentTor);
360 processSources(*parentTor, sources, cd);
361 }
362 return ok;
363 }
364
365 QString m_sourceLanguage;
366 QString m_targetLanguage;
367};
368
369}
370
372
373QStringList getSourceFilesFromQrc(const QString &resourceFile)
374{
375 if (!QFile::exists(resourceFile))
376 return QStringList();
377 QString content;
378 QString errStr;
379 if (!readFileContent(resourceFile, &content, &errStr)) {
380 printErr(QStringLiteral("lupdate error: Can not read %1: %2\n").arg(resourceFile, errStr));
381 return QStringList();
382 }
383 ReadQrcResult rqr = readQrcFile(resourceFile, content);
384 if (rqr.hasError()) {
385 printErr(QStringLiteral("lupdate error: %1:%2: %3\n")
386 .arg(resourceFile, QString::number(rqr.line), rqr.errorString));
387 }
388 return rqr.files;
389}
390
392 Projects &projectDescription,
393 const QStringList &tsFileNames,
394 const QStringList &alienFiles,
395 const QString &sourceLanguage,
396 const QString &targetLanguage,
397 UpdateOptions options)
398{
399 bool ok = true;
400
401 removeExcludedSources(projectDescription);
402 for (Project &project : projectDescription)
403 expandQrcFiles(project);
404
405 ProjectProcessor projectProcessor(sourceLanguage, targetLanguage);
406 if (!tsFileNames.isEmpty()) {
407 Translator fetchedTor;
408 ok &= projectProcessor.processProjects(true, options, projectDescription, true, &fetchedTor);
409 if (ok) {
410 ok &= updateTsFiles(fetchedTor, tsFileNames, alienFiles,
411 sourceLanguage, targetLanguage, options);
412 }
413 } else {
414 ok &= projectProcessor.processProjects(true, options, projectDescription, false, nullptr);
415 }
416 return ok;
417}
418
420 const QStringList &sourceFiles,
421 const QStringList &tsFileNames,
422 const QStringList &alienFiles,
423 const QSet<QString> &projectRoots,
424 const QStringList &includePath,
425 const QMultiHash<QString, QString> &allCSources,
426 const QString &sourceLanguage,
427 const QString &targetLanguage,
428 UpdateOptions options)
429{
430 Translator fetchedTor;
432 cd.m_noUiLines = options & NoUiLines;
433 cd.m_sourceIsUtf16 = options & SourceIsUtf16;
434 cd.m_projectRoots = projectRoots;
435 cd.m_includePath = includePath;
436 cd.m_allCSources = allCSources;
437 processSources(fetchedTor, sourceFiles, cd);
438 return updateTsFiles(fetchedTor, tsFileNames, alienFiles,
439 sourceLanguage, targetLanguage, options);
440}
441
442QT_END_NAMESPACE
bool m_sourceIsUtf16
Definition translator.h:44
int messageCount() const
Definition translator.h:137
const TranslatorMessage & constMessage(int i) const
Definition translator.h:140
std::vector< Project > Projects
bool processSourceFiles(const QStringList &sourceFiles, const QStringList &tsFileNames, const QStringList &alienFiles, const QSet< QString > &projectRoots, const QStringList &includePath, const QMultiHash< QString, QString > &allCSources, const QString &sourceLanguage, const QString &targetLanguage, UpdateOptions options)
bool processProjectDescription(Projects &projectDescription, const QStringList &tsFileNames, const QStringList &alienFiles, const QString &sourceLanguage, const QString &targetLanguage, UpdateOptions options)
QList< QString > QStringList
Constructs a string list that contains the given string, str.
@ Werror
Definition trparser.h:26
@ SourceIsUtf16
Definition trparser.h:25
@ NoUiLines
Definition trparser.h:24