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
filetransformer.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 "fileverifier.h"
6#include "utils.h"
8#include <translator.h>
9#include <trlib/metastrings.h>
10#include <trlib/trparser.h>
11
12#include <QFile>
13#include <QFileInfo>
14#include <QTextStream>
15#include <QSet>
16#include <QRegularExpression>
17
18using namespace Qt::StringLiterals;
19using namespace Utils;
20
21namespace {
22
23// UI elements for XML processing
24constexpr QLatin1String uiElement = "<ui "_L1;
25constexpr QLatin1String stringElement = "<string"_L1;
26constexpr QLatin1String stringListElement = "<stringlist"_L1;
27
28QString escapeForCppStringLiteral(const QString &str)
29{
30 QString result = str;
31 result.replace("\\", "\\\\");
32 result.replace("\"", "\\\"");
33 return result;
34}
35
36QString textMetaString(const QString &indentation, const QString &sourceText)
37{
38 const QStringList lines = sourceText.split('\n');
39 QString metaString;
40 for (int i = 0; i < lines.size(); i++) {
41 metaString += indentation + "//" + MetaStrings::sourceTextAnotation + " \""
42 + escapeForCppStringLiteral(lines[i]);
43 if (i < lines.size() - 1)
44 metaString += "\\n\"\n";
45 else
46 metaString += "\"\n";
47 }
48 return metaString;
49}
50
51QString labelMetaString(const QString &indentation, const QString &label)
52{
53 return indentation + "//" + MetaStrings::labelAnotation + ' ' + label + '\n';
54}
55
56QString idMetaString(const QString &indentation, const QString &id)
57{
58 return indentation + "//" + MetaStrings::extraAnotation + " meta-id " + id + '\n';
59}
60
61QRegularExpression idMetaStringRegex(const QString &id)
62{
63 const QString e = QRegularExpression::escape(id);
64 const QString pat = QStringLiteral(R"(^[ \t]*//~ meta-id\s*%1[ \t]*(?:\r?\n|$))").arg(e);
65 return QRegularExpression(pat, QRegularExpression::MultilineOption);
66}
67
68QString idBasedFunc(int trFunc, const QString &id, const QString &pluralArg)
69{
70 switch (trFunc) {
71 case TrFunctionAliasManager::Function_trUtf8:
72 case TrFunctionAliasManager::Function_tr:
73 case TrFunctionAliasManager::Function_translate:
74 return "qtTrId(\"" + id + "\"" + pluralArg + ")";
75 case TrFunctionAliasManager::Function_qsTr:
76 case TrFunctionAliasManager::Function_qsTranslate:
77 return "qsTrId(\"" + id + "\"" + pluralArg + ")";
78 case TrFunctionAliasManager::Function_QT_TR_NOOP:
79 case TrFunctionAliasManager::Function_QT_TR_NOOP_UTF8:
80 case TrFunctionAliasManager::Function_QT_TRANSLATE_NOOP:
81 case TrFunctionAliasManager::Function_QT_TRANSLATE_NOOP_UTF8:
82 return "QT_TRID_NOOP(\"" + id + "\")";
83 case TrFunctionAliasManager::Function_QT_TR_N_NOOP:
84 case TrFunctionAliasManager::Function_QT_TRANSLATE_N_NOOP:
85 return "QT_TRID_N_NOOP(\"" + id + "\")";
86 default:
87 return {};
88 }
89}
90
91int getTrFunction(const QString &expr)
92{
93 int i = 0;
94 int depth = 0;
95 QString trFunc;
96 while (i < expr.size()) {
97 if (QChar c = expr[i++]; (c.isLetterOrNumber() || c == '_'_L1) && depth == 0)
98 trFunc += c;
99 else {
100 while (c.isSpace() && i < expr.size())
101 c = expr[i++];
102
103 if (c == '(') {
104 if (depth == 0)
105 if (int trFuncId = trFunctionAliasManager.trFunctionByName(trFunc);
106 trFuncId >= 0)
107 return trFuncId;
108 depth++;
109 } else if (c == ')'_L1)
110 depth--;
111 else if (c == "\""_L1 || c == "'"_L1) {
112 const QChar quotation = c;
113 while (i < expr.size()) {
114 c = expr[i++];
115 if (c == '\\'_L1)
116 i++;
117 else if (c == quotation)
118 break;
119 }
120 }
121 trFunc.clear();
122 }
123 }
124 return -1;
125}
126
127QString getPluralArg(const QString &fn, bool plural)
128{
129 if (!plural)
130 return {};
131 int paren = 1;
132 int pos = fn.size() - 2;
133 while (paren > 0 && pos >= 0) {
134 if (fn[pos] == '('_L1)
135 paren--;
136 else if (fn[pos] == ')'_L1)
137 paren++;
138 if (paren == 1 && fn[pos] == ','_L1)
139 return ' ' + fn.sliced(pos).removeLast().simplified();
140 pos--;
141 }
142 return {};
143}
144
145void transformMessageNoLocation(TranslatorMessage &msg, const RecordDirectory &records,
146 QSet<QString> &ids, Translator &transformedTor, bool labels)
147{
148 if (msg.id().isEmpty()) {
149 const QString id = records.calculateId(msg);
150 if (labels)
151 msg.setLabel(msg.context());
152 msg.setId(id);
153 msg.setContext({});
154 msg.setComment({});
155 ids.insert(id);
156 }
157 transformedTor.append(msg);
158}
159
160void transformMessageWithLocation(TranslatorMessage &msg, const RecordDirectory &records,
161 QSet<QString> &ids, Translator &transformedTor, bool labels)
162{
163 TranslatorMessage::References normalRefs;
164 TranslatorMessage::References nonsupportedRefs;
165 for (const TranslatorMessage::Reference &r : msg.allReferences()) {
166 TranslatorMessage::Reference ur{ r.fileName(),
167 r.lineNumber()
168 + records.addedLines(r.fileName(), r.lineNumber()),
169 r.startOffset(), r.endOffset() };
170 if (records.isNonSupported(ur.fileName(), ur.lineNumber()))
171 nonsupportedRefs.append(ur);
172 else
173 normalRefs.append(ur);
174 }
176 if (!nonsupportedRefs.isEmpty()) {
177 TranslatorMessage newMsg = msg;
178 newMsg.setReferences(nonsupportedRefs);
179 transformedTor.append(newMsg);
180 }
181 if (QString ctx = msg.context(); !ctx.isEmpty() && !normalRefs.empty()) {
182 msg.setReferences(normalRefs);
183 const QString id = records.id(msg);
184 msg.setId(id);
185 msg.setContext({});
186 ids.insert(id);
187 msg.setComment({});
188 if (labels)
189 msg.setLabel(ctx);
190 transformedTor.append(msg);
191 } else if (!normalRefs.empty()) {
192 msg.setReferences(normalRefs);
193 transformedTor.append(msg);
194 }
195}
196
197bool makeFormIdBased(QStringList &lines, const QString &filename, const QString &label)
198{
199 auto itr = lines.begin();
200 qsizetype pos;
201 do // skip comment lines
202 pos = itr++->indexOf(uiElement);
203 while (pos < 0 && itr != lines.end());
204
205 if (pos < 0) {
206 printErr("ltext2id: no root element in the "
207 "ui file %1. Ignoring."_L1.arg(filename));
208 return false;
209 }
210 itr--;
211
212 if (!label.isEmpty() && itr->indexOf("label=") < 0)
213 itr->insert(pos + uiElement.size(), "label=\"" + label + "\" ");
214
215 if (itr->indexOf("idbasedtr") < 0)
216 itr->insert(pos + uiElement.size(), "idbasedtr=\"true\" ");
217 else
218 itr->replace("idbasedtr=\"false\"", "idbasedtr=\"true\"");
219
220 return true;
221}
222} // namespace
223
224QT_BEGIN_NAMESPACE
225
226// Static member definitions
227const QSet<QString> FileTransformer::cppExtensions{ "c"_L1, "c++"_L1, "cc"_L1, "cpp"_L1,
228 "cxx"_L1, "ch"_L1, "h"_L1, "h++"_L1,
229 "hh"_L1, "hpp"_L1, "hxx"_L1 };
230
232 "jui"_L1, "ui"_L1, "js"_L1, "mjs"_L1, "qml"_L1, // python doesn't have id based
233};
234
235FileTransformer::FileTransformer(RecordDirectory &records, bool labels, bool quiet)
236 : m_records(records), m_labels(labels), m_quiet(quiet)
237{
238}
239
241{
242 for (const auto &[filename, messages] : m_records.messageLocations().asKeyValueRange()) {
243 if (filename.endsWith(".ui", Qt::CaseInsensitive)) {
244 QStringList lines = readLines(filename);
245 if (lines.empty())
246 continue;
247
248 if (!m_quiet)
249 printOut("ltext2id: processing source file %1"_L1.arg(filename));
250 if (m_labels)
251 makeFormIdBased(lines, filename, (*messages.begin())->context);
252
253 for (const std::shared_ptr<MessageItem> &msg : messages) {
254 QString &line = lines[msg->lineNo - 1];
255 qsizetype pos = line.indexOf(stringElement);
256 qsizetype size = stringElement.size();
257 if (pos < 0 || line.size() < pos + size
258 || (line.at(pos + size) != '>' && !line.at(pos + size).isSpace())) {
259 pos = line.indexOf(stringListElement);
260 size = stringListElement.size();
261 }
262 if (pos < 0 || line.size() < pos + size
263 || (line.at(pos + size) != '>' && !line.at(pos + size).isSpace())) {
264 printErr("ltext2id error: could not find the "
265 "expected translatable string in %1:%2.\n"_L1.arg(filename)
266 .arg(msg->lineNo));
267 m_records.recordError(filename, msg->lineNo,
268 "please use id %1"_L1.arg(msg->id));
269 continue;
270 }
271 pos += size;
272 line.insert(pos, " id=\"" + msg->id + "\"");
273 }
274 writeLines(filename, lines);
275 }
276 }
277}
278
280{
281 for (const auto &[filename, messages] : m_records.messageLocations().asKeyValueRange()) {
282 if (!filename.endsWith(".ui", Qt::CaseInsensitive)) {
283 if (!m_quiet)
284 printOut("ltext2id: processing source file %1"_L1.arg(filename));
285
286 QFile file(filename);
287 if (!file.open(QIODevice::ReadOnly)) {
288 printErr("ltext2id error: failed to open file %1 for reading.\n"_L1.arg(filename));
289 continue;
290 }
291 QTextStream in(&file);
292 const QString code = in.readAll();
293 file.close();
294
295 QString newCode;
296 newCode.reserve(code.size());
297 qsizetype lastPos = 0;
298 int addedLines = 0;
299 for (const std::shared_ptr<MessageItem> &msg : messages) {
300 if (msg->startOffset >= 0 && msg->endOffset > msg->startOffset
301 && msg->endOffset <= code.size() && msg->startOffset >= lastPos) {
302 int lastLinePos = code.lastIndexOf('\n', msg->startOffset) + 1;
303 if (lastLinePos < lastPos)
304 lastLinePos = msg->startOffset;
305 const QString lastLine =
306 code.sliced(lastLinePos, msg->startOffset - lastLinePos);
307
308 const QString indentation = getIndentation(lastLine);
309 QString fnId = textMetaString(indentation, msg->sourceText);
310 if (m_labels)
311 fnId += labelMetaString(indentation, msg->context);
312 QString fn = code.sliced(msg->startOffset, msg->endOffset - msg->startOffset);
313 int trFunc = getTrFunction(fn);
314 QString idBasedFn = idBasedFunc(trFunc, msg->id, getPluralArg(fn, msg->plural));
315 if (idBasedFn.isEmpty()) {
316 msg->lineNo += addedLines;
317 if (trFunc >= 0)
318 m_records.recordNonSupported(filename, msg->lineNo);
319 else
320 m_records.recordError(filename, msg->lineNo,
321 "Could not detect any translation calls here"_L1);
322 continue;
323 }
324 fnId += lastLine + std::move(idBasedFn);
325
326 QString codePiece = code.sliced(lastPos, lastLinePos - lastPos);
327 if (msg->hasMetaId) {
328 codePiece.remove(idMetaStringRegex(msg->id));
329 addedLines--;
330 }
331 newCode += codePiece;
332 newCode += fnId;
333
334 addedLines += fnId.count('\n');
335 m_records.recordAddedLines(filename, msg->lineNo, addedLines);
336 msg->lineNo += addedLines;
337 if (const int origMsgLines = fn.count('\n'); origMsgLines) {
338 addedLines -= origMsgLines;
339 m_records.recordAddedLines(filename, msg->lineNo - addedLines + 1,
340 addedLines);
341 }
342 lastPos = msg->endOffset;
343 } else {
344 m_records.recordError(
345 filename, msg->lineNo,
346 QString("Invalid location offsets for the translation call: %1-%2")
347 .arg(msg->startOffset)
348 .arg(msg->endOffset));
349 }
350 }
351 newCode += code.sliced(lastPos);
352
353 QFile outFile(filename);
354 if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
355 printErr("ltext2id error: failed to open file %1 for writing.\n"_L1.arg(filename));
356 continue;
357 }
358 QTextStream out(&outFile);
359 out << newCode;
360 outFile.close();
361 }
362 }
363}
364
365bool FileTransformer::transformTsFiles(const QStringList &translations, bool sortMessages)
366{
368 cd.m_sortMessages = sortMessages;
369 bool ok = true;
370 for (const QString &tsFile : std::as_const(translations)) {
371 if (!m_quiet)
372 printOut("ltext2id: processing TS file %1"_L1.arg(tsFile));
373 Translator tor;
374 tor.load(tsFile, cd, "ts");
375 if (!cd.errors().empty()) { // timestamp files
376 cd.clearErrors();
377 continue;
378 }
379 tor.makeFileNamesAbsolute(QFileInfo(tsFile).absoluteDir());
380
381 Translator transformedTor;
382 QSet<QString> ids;
383 for (qsizetype i = 0; i < tor.messageCount(); i++) {
384 TranslatorMessage &msg = tor.message(i);
385 if (const QString filename = msg.fileName();
386 filename.isEmpty() || !m_records.containsFile(filename))
387 transformMessageNoLocation(msg, m_records, ids, transformedTor, m_labels);
388 else
389 transformMessageWithLocation(msg, m_records, ids, transformedTor, m_labels);
390 }
391 transformedTor.save(tsFile, cd, "ts");
392 if (!cd.errors().empty()) {
393 printErr(
394 "ltext2id error: error in processing translation files\n%1"_L1.arg(cd.error()));
395 ok = false;
396 }
397
398 if (!m_quiet)
399 printOut("ltext2id: verifying TS file %1"_L1.arg(tsFile));
400
401 FileVerifier verifier(m_records, m_quiet);
402 if (!verifier.verifyTs(tsFile, ids)) {
403 printErr("ltext2id: verifying TS file %1 failed."_L1.arg(tsFile));
404 ok = false;
405 }
406 }
407 return ok;
408}
409
411{
412 for (const auto &[filename, messages] : m_records.messageLocations().asKeyValueRange()) {
413 if (!filename.endsWith(".ui", Qt::CaseInsensitive)) {
414 if (!m_quiet)
415 printOut("ltext2id: processing source file %1"_L1.arg(filename));
416
417 QStringList lines = readLines(filename);
418 int addedLines = 0;
419 for (auto itr = messages.cbegin(); itr != messages.cend(); itr++) {
420 const MessageItem &m = **itr;
421 QString &line = lines[m.lineNo - 1];
422 const QString indentation = getIndentation(lines[m.lineNo - 1]);
423 line = idMetaString(indentation, m.id) + line;
424 m_records.recordAddedLines(filename, m.lineNo, ++addedLines);
425 }
426 writeLines(filename, lines);
427 }
428 }
429}
430
431bool FileTransformer::updateTsFiles(const QStringList &translations)
432{
434 bool ok = true;
435 for (const QString &tsFile : std::as_const(translations)) {
436 if (!m_quiet)
437 printOut("ltext2id: processing TS file %1"_L1.arg(tsFile));
438 Translator tor;
439 tor.load(tsFile, cd, "ts");
440 if (!cd.errors().empty()) { // timestamp files
441 cd.clearErrors();
442 continue;
443 }
444 tor.makeFileNamesAbsolute(QFileInfo(tsFile).absoluteDir());
445 Translator transformedTor;
446
447 for (qsizetype i = 0; i < tor.messageCount(); i++) {
448 TranslatorMessage &msg = tor.message(i);
449 TranslatorMessage::References refs;
450 for (const TranslatorMessage::Reference &r : msg.allReferences()) {
451 if (const QString filename = r.fileName();
452 !filename.isEmpty() && m_records.containsFile(filename)) {
453 TranslatorMessage::Reference ur{
454 filename, r.lineNumber() + m_records.addedLines(filename, r.lineNumber()),
455 r.startOffset(), r.endOffset()
456 };
457 refs.append(ur);
458 }
459 }
460 msg.clearReferences();
461 msg.setReferences(refs);
462 msg.setExtra(meta_id_key, m_records.id(msg));
463 transformedTor.append(msg);
464 }
465
466 transformedTor.save(tsFile, cd, "ts");
467 if (!cd.errors().empty()) {
468 printErr(
469 "ltext2id error: error in processing translation files\n%1"_L1.arg(cd.error()));
470 ok = false;
471 }
472
473 if (!m_quiet)
474 printOut("ltext2id: verifying TS file %1"_L1.arg(tsFile));
475 }
476 return ok;
477}
478
479QT_END_NAMESPACE
bool m_sortMessages
Definition translator.h:59
bool transformTsFiles(const QStringList &translations, bool sortMessages)
FileTransformer(RecordDirectory &records, bool labels, bool quiet)
bool updateTsFiles(const QStringList &translations)
static const QSet< QString > otherExtensions
void setReferences(const References &refs)
QList< Reference > References
void append(const TranslatorMessage &msg)
TrFunctionAliasManager trFunctionAliasManager
Definition trparser.cpp:153