Revision control
++ // Copyright (C) 2025 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++
++ #include "projectprocessor.h"
++ #include "qrcreader.h"
++ #include <translator.h>
++ #include <trlib/trparser.h>
++
++ #include <QtCore/qdir.h>
++ #include <QtCore/qfile.h>
++ #include <QtCore/qfileinfo.h>
++ #include <QtCore/qregularexpression.h>
++ #include <QtCore/qset.h>
++
++ #include <iostream>
++
++ using namespace Qt::StringLiterals;
++
++ namespace {
++
++ static void printOut(const QString &out)
++ {
++ std::cout << qPrintable(out);
++ }
++
++ static void printErr(const QString &out)
++ {
++ std::cerr << qPrintable(out);
++ }
++
++ static void printWarning(UpdateOptions options,
++ const QString &msg,
++ const QString &warningMsg = {},
++ const QString &errorMsg = {})
++ {
++ QString text = msg;
++ if (options & Werror) {
++ text.prepend("lupdate error: "_L1);
++ if (!errorMsg.isEmpty())
++ text.append(" "_L1).append(errorMsg);
++ } else {
++ text.prepend("lupdate warning: "_L1);
++ if (!warningMsg.isEmpty())
++ text.append(" "_L1).append(warningMsg);
++ }
++
++ printErr(text);
++ }
++
++ static bool readFileContent(const QString &filePath, QByteArray *content, QString *errorString)
++ {
++ QFile file(filePath);
++ if (!file.open(QIODevice::ReadOnly)) {
++ *errorString = file.errorString();
++ return false;
++ }
++ *content = file.readAll();
++ return true;
++ }
++
++ static bool readFileContent(const QString &filePath, QString *content, QString *errorString)
++ {
++ QByteArray ba;
++ if (!readFileContent(filePath, &ba, errorString))
++ return false;
++ *content = QString::fromUtf8(ba);
++ return true;
++ }
++
++ static void removeExcludedSources(Projects &projects)
++ {
++ for (Project &project : projects) {
++ for (const QRegularExpression &rx : std::as_const(project.excluded)) {
++ for (auto it = project.sources.begin(); it != project.sources.end(); ) {
++ if (rx.match(*it).hasMatch())
++ it = project.sources.erase(it);
++ else
++ ++it;
++ }
++ }
++ removeExcludedSources(project.subProjects);
++ }
++ }
++
++
++ // Remove .qrc files from the project and return them as absolute paths.
++ static QStringList extractQrcFiles(Project &project)
++ {
++ auto it = project.sources.begin();
++ QStringList qrcFiles;
++ while (it != project.sources.end()) {
++ QFileInfo fi(*it);
++ QString fn = QDir::cleanPath(fi.absoluteFilePath());
++ if (fn.endsWith(".qrc"_L1, Qt::CaseInsensitive)) {
++ qrcFiles += fn;
++ it = project.sources.erase(it);
++ } else {
++ ++it;
++ }
++ }
++ return qrcFiles;
++ }
++
++ // Replace all .qrc files in the project with their content.
++ static void expandQrcFiles(Project &project)
++ {
++ for (const QString &qrcFile : extractQrcFiles(project))
++ project.sources << getSourceFilesFromQrc(qrcFile);
++ }
++
++ static QSet<QString> projectRoots(const QString &projectFile, const QStringList &sourceFiles)
++ {
++ const QString proPath = QFileInfo(projectFile).path();
++ QSet<QString> sourceDirs;
++ sourceDirs.insert(proPath + u'/');
++ for (const QString &sf : sourceFiles)
++ sourceDirs.insert(sf.left(sf.lastIndexOf(u'/') + 1));
++ QStringList rootList = sourceDirs.values();
++ rootList.sort();
++ for (int prev = 0, curr = 1; curr < rootList.size(); )
++ if (rootList.at(curr).startsWith(rootList.at(prev)))
++ rootList.removeAt(curr);
++ else
++ prev = curr++;
++ return QSet<QString>(rootList.cbegin(), rootList.cend());
++ }
++
++ static bool updateTsFiles(const Translator &fetchedTor, const QStringList &tsFileNames,
++ const QStringList &alienFiles,
++ const QString &sourceLanguage, const QString &targetLanguage,
++ UpdateOptions options)
++ {
++ bool fail = false;
++ for (int i = 0; i < fetchedTor.messageCount(); i++) {
++ const TranslatorMessage &msg = fetchedTor.constMessage(i);
++ if (!msg.id().isEmpty() && msg.sourceText().isEmpty()) {
++ printWarning(options,
++ "Message with id '%1' has no source.\n"_L1.arg(msg.id()));
++ if (options & Werror)
++ return !fail;
++ }
++ }
++ QList<Translator> aliens;
++ for (const QString &fileName : alienFiles) {
++ ConversionData cd;
++ Translator tor;
++ if (!tor.load(fileName, cd, "auto"_L1)) {
++ printErr(cd.error());
++ fail = true;
++ continue;
++ }
++ tor.resolveDuplicates();
++ aliens << tor;
++ }
++ QDir dir;
++ QString err;
++ for (const QString &fileName : tsFileNames) {
++ QString fn = dir.relativeFilePath(fileName);
++ ConversionData cd;
++ Translator tor;
++ cd.m_sortContexts = !(options & NoSort);
++ cd.m_sortMessages = options & SortMessages;
++ if (QFile(fileName).exists()) {
++ if (!tor.load(fileName, cd, "auto"_L1)) {
++ printErr(cd.error());
++ fail = true;
++ continue;
++ }
++ tor.resolveDuplicates();
++ cd.clearErrors();
++ if (!targetLanguage.isEmpty() && targetLanguage != tor.languageCode()) {
++ printWarning(options,
++ "Specified target language '%1' disagrees with"
++ " existing file's language '%2'.\n"_L1
++ .arg(targetLanguage, tor.languageCode()),
++ u"Ignoring.\n"_s);
++ if (options & Werror)
++ return !fail;
++ }
++ if (!sourceLanguage.isEmpty() && sourceLanguage != tor.sourceLanguageCode()) {
++ printWarning(options,
++ "Specified source language '%1' disagrees with"
++ " existing file's language '%2'.\n"_L1
++ .arg(sourceLanguage, tor.sourceLanguageCode()),
++ u"Ignoring.\n"_s);
++ if (options & Werror)
++ return !fail;
++ }
++ // If there is translation in the file, the language should be recognized
++ // (when the language is not recognized, plural translations are lost)
++ if (tor.translationsExist()) {
++
++ if (tor.languageCode().isEmpty()) {
++ printErr("File %1 won't be updated: it does not specify any "
++ "target languages. To set a target language, open "
++ "the file in Qt Linguist.\n"_L1.arg(fileName));
++ continue;
++ }
++ QLocale::Language l;
++ QLocale::Territory c;
++ tor.languageAndTerritory(tor.languageCode(), &l, &c);
++ QStringList forms;
++ if (!getNumerusInfo(l, c, 0, &forms, 0)) {
++ printErr(QStringLiteral("File %1 won't be updated: it contains translation but the"
++ " target language is not recognized\n").arg(fileName));
++ continue;
++ }
++ }
++ } else {
++ if (!targetLanguage.isEmpty())
++ tor.setLanguageCode(targetLanguage);
++ else
++ tor.setLanguageCode(Translator::guessLanguageCodeFromFileName(fileName));
++ if (!sourceLanguage.isEmpty())
++ tor.setSourceLanguageCode(sourceLanguage);
++ }
++ tor.makeFileNamesAbsolute(QFileInfo(fileName).absoluteDir());
++ if (options & NoLocations)
++ tor.setLocationsType(Translator::NoLocations);
++ else if (options & RelativeLocations)
++ tor.setLocationsType(Translator::RelativeLocations);
++ else if (options & AbsoluteLocations)
++ tor.setLocationsType(Translator::AbsoluteLocations);
++ if (options & Verbose)
++ printOut(QStringLiteral("Updating '%1'...\n").arg(fn));
++
++ UpdateOptions theseOptions = options;
++ if (tor.locationsType() == Translator::NoLocations) // Could be set from file
++ theseOptions |= NoLocations;
++ Translator out = merge(tor, fetchedTor, aliens, theseOptions, err);
++
++ if ((options & Verbose) && !err.isEmpty()) {
++ printOut(err);
++ err.clear();
++ }
++ if (options & PluralOnly) {
++ if (options & Verbose)
++ printOut(QStringLiteral("Stripping non plural forms in '%1'...\n").arg(fn));
++ out.stripNonPluralForms();
++ }
++ if (options & NoObsolete)
++ out.stripObsoleteMessages();
++ out.stripEmptyContexts();
++
++ out.normalizeTranslations(cd);
++ if (!cd.errors().isEmpty()) {
++ printErr(cd.error());
++ cd.clearErrors();
++ }
++ if (!out.save(fileName, cd, "auto"_L1)) {
++ printErr(cd.error());
++ fail = true;
++ }
++ }
++ return !fail;
++ }
++
++ class ProjectProcessor
++ {
++ public:
++ ProjectProcessor(const QString &sourceLanguage,
++ const QString &targetLanguage)
++ : m_sourceLanguage(sourceLanguage),
++ m_targetLanguage(targetLanguage)
++ {
++ }
++
++ bool processProjects(bool topLevel, UpdateOptions options, const Projects &projects,
++ bool nestComplain, Translator *parentTor) const
++ {
++ bool ok = true;
++ for (const Project &prj : projects)
++ ok &= processProject(options, prj, topLevel, nestComplain, parentTor);
++ return ok;
++ }
++
++ private:
++
++ bool processProject(UpdateOptions options, const Project &prj, bool topLevel,
++ bool nestComplain, Translator *parentTor) const
++ {
++ bool ok = true;
++ QString codecForSource = prj.codec.toLower();
++ if (!codecForSource.isEmpty()) {
++ if (codecForSource == "utf-16"_L1 || codecForSource == "utf16"_L1) {
++ options |= SourceIsUtf16;
++ } else if (codecForSource == "utf-8"_L1 || codecForSource == "utf8"_L1) {
++ options &= ~SourceIsUtf16;
++ } else {
++ printWarning(
++ options,
++ "Codec for source '%1' is invalid.\n"_L1.arg(codecForSource),
++ u"Falling back to UTF-8.\n"_s);
++ if (options & Werror)
++ return ok;
++ options &= ~SourceIsUtf16;
++ }
++ }
++
++ const QString projectFile = prj.filePath;
++ const QStringList sources = prj.sources;
++ ConversionData cd;
++ cd.m_noUiLines = options & NoUiLines;
++ cd.m_projectRoots = projectRoots(projectFile, sources);
++ QStringList projectRootDirs;
++ for (const auto& dir : std::as_const(cd.m_projectRoots))
++ projectRootDirs.append(dir);
++ cd.m_includePath = prj.includePaths;
++ cd.m_excludes = prj.excluded;
++ cd.m_sourceIsUtf16 = options & SourceIsUtf16;
++
++ QStringList tsFiles;
++ if (prj.translations) {
++ tsFiles = *prj.translations;
++ if (parentTor) {
++ if (topLevel) {
++ printWarning(options, u"Existing top level."_s,
++ "TS files from command line will "
++ "override TRANSLATIONS in %1.\n"_L1.arg(projectFile),
++ u"Terminating the operation.\n"_s);
++ if (options & Werror)
++ return ok;
++ goto noTrans;
++ } else if (nestComplain) {
++ printWarning(options,
++ "TS files from command line "
++ "prevent recursing into %1.\n"_L1.arg(projectFile));
++ return ok;
++ }
++ }
++ if (tsFiles.isEmpty()) {
++ // This might mean either a buggy PRO file or an intentional detach -
++ // we can't know without seeing the actual RHS of the assignment ...
++ // Just assume correctness and be silent.
++ return ok;
++ }
++ Translator tor;
++ ok = processProjects(false, options, prj.subProjects, false, &tor);
++ processSources(tor, sources, cd);
++ ok &= updateTsFiles(tor, tsFiles, QStringList(), m_sourceLanguage, m_targetLanguage,
++ options);
++ return ok;
++ }
++
++
++ noTrans:
++ if (!parentTor) {
++ if (topLevel) {
++ printWarning(options, u"no TS files specified."_s,
++ "Only diagnostics will be produced for %1.\n"_L1.arg(projectFile),
++ u"Terminating the operation.\n"_s);
++ if (options & Werror)
++ return ok;
++ }
++ Translator tor;
++ ok = processProjects(false, options, prj.subProjects, nestComplain, &tor);
++ processSources(tor, sources, cd);
++ } else {
++ ok = processProjects(false, options, prj.subProjects, nestComplain, parentTor);
++ processSources(*parentTor, sources, cd);
++ }
++ return ok;
++ }
++
++ QString m_sourceLanguage;
++ QString m_targetLanguage;
++ };
++
++ }
++
++ QT_BEGIN_NAMESPACE
++
++ QStringList getSourceFilesFromQrc(const QString &resourceFile)
++ {
++ if (!QFile::exists(resourceFile))
++ return QStringList();
++ QString content;
++ QString errStr;
++ if (!readFileContent(resourceFile, &content, &errStr)) {
++ printErr(QStringLiteral("lupdate error: Can not read %1: %2\n").arg(resourceFile, errStr));
++ return QStringList();
++ }
++ ReadQrcResult rqr = readQrcFile(resourceFile, content);
++ if (rqr.hasError()) {
++ printErr(QStringLiteral("lupdate error: %1:%2: %3\n")
++ .arg(resourceFile, QString::number(rqr.line), rqr.errorString));
++ }
++ return rqr.files;
++ }
++
++ bool processProjectDescription(
++ Projects &projectDescription,
++ const QStringList &tsFileNames,
++ const QStringList &alienFiles,
++ const QString &sourceLanguage,
++ const QString &targetLanguage,
++ UpdateOptions options)
++ {
++ bool ok = true;
++
++ removeExcludedSources(projectDescription);
++ for (Project &project : projectDescription)
++ expandQrcFiles(project);
++
++ ProjectProcessor projectProcessor(sourceLanguage, targetLanguage);
++ if (!tsFileNames.isEmpty()) {
++ Translator fetchedTor;
++ ok &= projectProcessor.processProjects(true, options, projectDescription, true, &fetchedTor);
++ if (ok) {
++ ok &= updateTsFiles(fetchedTor, tsFileNames, alienFiles,
++ sourceLanguage, targetLanguage, options);
++ }
++ } else {
++ ok &= projectProcessor.processProjects(true, options, projectDescription, false, nullptr);
++ }
++ return ok;
++ }
++
++ 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)
++ {
++ Translator fetchedTor;
++ ConversionData cd;
++ cd.m_noUiLines = options & NoUiLines;
++ cd.m_sourceIsUtf16 = options & SourceIsUtf16;
++ cd.m_projectRoots = projectRoots;
++ cd.m_includePath = includePath;
++ cd.m_allCSources = allCSources;
++ processSources(fetchedTor, sourceFiles, cd);
++ return updateTsFiles(fetchedTor, tsFileNames, alienFiles,
++ sourceLanguage, targetLanguage, options);
++ }
++
++ QT_END_NAMESPACE