QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
ComponentInformationTranslation.cc
Go to the documentation of this file.
3#include "JsonParsing.h"
5#include "QGCCompression.h"
7
8#include <QtCore/QStandardPaths>
9#include <QtCore/QDir>
10#include <QtCore/QJsonArray>
11#include <QtCore/QJsonDocument>
12#include <QtCore/QXmlStreamReader>
13
14QGC_LOGGING_CATEGORY(ComponentInformationTranslationLog, "ComponentInformation.ComponentInformationTranslation")
15
17 QGCCachedFileDownload* cachedFileDownload)
18 : QObject(parent), _cachedFileDownload(cachedFileDownload)
19{
20}
21
23 const QString& toTranslateJsonFile, int maxCacheAgeSec, const QString& componentName)
24{
25 // Metadata is authored in English, no translation needed
26 const QString locale = QLocale::system().name();
27 if (locale.startsWith(QLatin1String("en"))) {
28 qCDebug(ComponentInformationTranslationLog) << "Skipping translation for English locale" << locale << "for" << componentName;
29 return false;
30 }
31
32 // Parse summary: find url for current locale
33 _toTranslateJsonFile = toTranslateJsonFile;
34 QString url = getUrlFromSummaryJson(summaryJsonFile, locale, componentName);
35 if (url.isEmpty()) {
36 return false;
37 }
38
39 // Download file
40 connect(_cachedFileDownload, &QGCCachedFileDownload::finished, this, &ComponentInformationTranslation::onDownloadCompleted);
41 if (!_cachedFileDownload->download(url, maxCacheAgeSec)) {
42 qCWarning(ComponentInformationTranslationLog) << "Metadata translation download failed";
43 disconnect(_cachedFileDownload, &QGCCachedFileDownload::finished, this, &ComponentInformationTranslation::onDownloadCompleted);
44 return false;
45 }
46 return true;
47}
48
49QString ComponentInformationTranslation::getUrlFromSummaryJson(const QString &summaryJsonFile, const QString &locale, const QString &componentName)
50{
51 QString errorString;
52 QJsonDocument jsonDoc;
53
54 if (!JsonParsing::isJsonFile(summaryJsonFile, jsonDoc, errorString)) {
55 qCWarning(ComponentInformationTranslationLog) << "Metadata translation summary json file open failed for" << componentName << ":" << errorString;
56 return "";
57 }
58 QJsonObject jsonObj = jsonDoc.object();
59
60 QJsonObject localeObj = jsonObj[locale].toObject();
61 if (localeObj.isEmpty()) {
62 qCWarning(ComponentInformationTranslationLog) << "Locale" << locale << "not found in translation json for" << componentName;
63 return "";
64 }
65
66 QString url = localeObj["url"].toString();
67 if (url.isEmpty()) {
68 qCWarning(ComponentInformationTranslationLog) << "Locale" << locale << "has no url in translation json for" << componentName;
69 }
70 return url;
71}
72
73void ComponentInformationTranslation::onDownloadCompleted(bool success, const QString &localFile, QString errorMsg, [[maybe_unused]] bool fromCache)
74{
75 disconnect(_cachedFileDownload, &QGCCachedFileDownload::finished, this, &ComponentInformationTranslation::onDownloadCompleted);
76
77 QString tsFileName = localFile;
78 bool deleteFile = false;
79 if (success) {
80 const QString tempPath = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).absoluteFilePath("qgc_translation_file_decompressed.ts");
81 tsFileName = QGCCompression::decompressIfNeeded(localFile, tempPath, false);
82 if (tsFileName.isEmpty()) {
83 const QString remoteFile = _cachedFileDownload->url().toString();
84 errorMsg = "Decompression of translation file failed: " + remoteFile;
85 } else if (tsFileName != localFile) {
86 deleteFile = true; // Mark for cleanup since we decompressed
87 }
88 }
89
90 // Translate json file to new temp file
91 QString translatedJsonFilename;
92 if (errorMsg.isEmpty()) {
93 translatedJsonFilename = translateJsonUsingTS(_toTranslateJsonFile, tsFileName);
94 if (translatedJsonFilename.isEmpty()) {
95 errorMsg = "Failed to translate json file";
96 }
97 }
98
99 if (deleteFile) {
100 QFile(localFile).remove();
101 }
102
103 emit downloadComplete(translatedJsonFilename, errorMsg);
104}
105
106QString ComponentInformationTranslation::translateJsonUsingTS(const QString &toTranslateJsonFile, const QString &tsFile)
107{
108 qCDebug(ComponentInformationTranslationLog) << "Translating" << toTranslateJsonFile << "using" << tsFile;
109
110 // Open JSON and get the 'translation' object
111 QString errorString;
112 QJsonDocument jsonDoc;
113
114 if (!JsonParsing::isJsonFile(toTranslateJsonFile, jsonDoc, errorString)) {
115 qCWarning(ComponentInformationTranslationLog) << "Metadata json file to translate open failed:" << errorString;
116 return "";
117 }
118
119 QString schemaError;
120 if (!JsonSchemaValidator::validate(jsonDoc, QStringLiteral(":/json/component_metadata/translation.schema.json"), schemaError)) {
121 qCWarning(ComponentInformationTranslationLog) << "Translation json schema validation failed:" << schemaError;
122 }
123
124 QJsonObject jsonObj = jsonDoc.object();
125
126 QJsonObject translationObj = jsonObj["translation"].toObject();
127 if (translationObj.isEmpty()) {
128 qCWarning(ComponentInformationTranslationLog) << "json file does not contain 'translation' object";
129 return "";
130 }
131
132
133 // Open and parse TS file into a hash table
134 QHash<QString, QString> translations;
135 QFile xmlFile(tsFile);
136 if (!xmlFile.open(QIODevice::ReadOnly)) {
137 qCWarning(ComponentInformationTranslationLog) << "Failed opening TS file";
138 return "";
139 }
140
141 QXmlStreamReader xml(xmlFile.readAll());
142 xmlFile.close();
143 if (xml.hasError()) {
144 qCWarning(ComponentInformationTranslationLog) << "Badly formed TS (XML)" << xml.errorString();
145 return "";
146 }
147
148 bool insideTS = false;
149
150 while (!xml.atEnd()) {
151 if (xml.isStartElement()) {
152 QString elementName = xml.name().toString();
153
154 if (elementName == "TS") {
155 insideTS = true;
156 } else if (insideTS && elementName == "context") {
157 // Read whole <context>
158 QString name;
159 QString translation;
160 bool insideMessage = false;
161 while (!xml.atEnd()) {
162
163 if (xml.isStartElement()) {
164 if (xml.name().toString() == "message") {
165 insideMessage = true;
166 } else if (xml.name().toString() == "name" && !insideMessage) {
167 name = xml.readElementText();
168 } else if (xml.name().toString() == "translation" && insideMessage) {
169 translation = xml.readElementText();
170 }
171 } else if (xml.isEndElement()) {
172 if (xml.name().toString() == "context") {
173 break;
174 } else if (xml.name().toString() == "message") {
175 insideMessage = false;
176 }
177 }
178
179 xml.readNext();
180 }
181
182 if (name != "" && translation != "") {
183 translations[name] = translation;
184 }
185 }
186
187 } else if (xml.isEndElement()) {
188 QString elementName = xml.name().toString();
189
190 if (elementName == "TS") {
191 insideTS = false;
192 }
193 }
194 xml.readNext();
195 }
196
197 if (translations.isEmpty()) {
198 qCWarning(ComponentInformationTranslationLog) << "No translations found in TS file";
199 return "";
200 }
201
202 // Translate the json document
203 jsonDoc.setObject(translate(translationObj, translations, jsonDoc.object()));
204
205 // Write to file
206 QString translatedFileName = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).absoluteFilePath("qgc_translated_metadata.json");
207
208 QFile translatedFile(translatedFileName);
209 if (!translatedFile.open(QFile::WriteOnly|QFile::Truncate)) {
210 errorString = tr("File open failed: file:error %1 %2").arg(translatedFile.fileName()).arg(translatedFile.errorString());
211 return "";
212 }
213 translatedFile.write(jsonDoc.toJson());
214 translatedFile.close();
215
216 qCDebug(ComponentInformationTranslationLog) << "JSON file" << toTranslateJsonFile << "successfully translated to" << translatedFileName;
217 return translatedFileName;
218}
219
220QJsonObject ComponentInformationTranslation::translate(const QJsonObject& translationObj,
221 const QHash<QString, QString>& translations, QJsonObject doc)
222{
223 QJsonObject defs = translationObj["$defs"].toObject();
224 if (translationObj.contains("items")) {
225 doc = translateItems("", defs, translationObj["items"].toObject(), translations, doc);
226 }
227 if (translationObj.contains("$ref")) {
228 doc = translateItems("", defs, defs[getRefName(translationObj["$ref"].toString())].toObject(), translations, doc);
229 }
230 return doc;
231}
232
233QJsonObject ComponentInformationTranslation::translateItems(const QString& prefix, const QJsonObject& defs,
234 const QJsonObject& translationObj,
235 const QHash<QString, QString>& translations,
236 QJsonObject jsonData)
237{
238 for (auto translationItemIter = translationObj.begin(); translationItemIter != translationObj.end(); ++translationItemIter) {
239 QStringList translationKeys;
240 if (translationItemIter.key() == "*") {
241 translationKeys = jsonData.keys();
242 } else {
243 translationKeys.append(translationItemIter.key());
244 }
245 for (const auto& jsonItem : translationKeys) {
246 QString nextPrefix = prefix + '/' + jsonItem;
247 QJsonObject nextTranslationObj = translationItemIter.value().toObject();
248 if (jsonData.contains(jsonItem)) {
249 jsonData[jsonItem] = translateTranslationItems(nextPrefix, defs, nextTranslationObj, translations, jsonData[jsonItem]);
250 }
251 }
252 }
253 return jsonData;
254}
255
256QString ComponentInformationTranslation::getRefName(const QString& ref)
257{
258 // expected format: '#/$defs/<name>'
259 return ref.mid(8);
260}
261
262QJsonValue ComponentInformationTranslation::translateTranslationItems(const QString& prefix, const QJsonObject& defs,
263 const QJsonObject& translationObj,
264 const QHash<QString, QString>& translations,
265 QJsonValue jsonData)
266{
267 if (translationObj.contains("list")) {
268 QJsonObject translationList = translationObj["list"].toObject();
269 QString key = translationList["key"].toString();
270 int idx = 0;
271 QJsonArray array = jsonData.toArray();
272 for (const auto& listEntry : array) {
273 QString value;
274 if (!key.isEmpty() && listEntry.toObject().contains(key)) {
275 value = listEntry.toObject()[key].toString();
276 } else {
277 value = QString::number(idx);
278 }
279 array[idx] = translateTranslationItems(prefix + '/' + value, defs, translationList, translations, listEntry);
280 ++idx;
281 }
282 jsonData = array;
283 }
284 if (translationObj.contains("translate")) {
285 for (const auto& translateName : translationObj["translate"].toArray()) {
286 QString translateNameStr = translateName.toString();
287 if (jsonData.toObject().contains(translateNameStr)) {
288 if (jsonData[translateNameStr].isString()) {
289 auto lookupIter = translations.find(prefix + '/' + translateNameStr);
290 if (lookupIter != translations.end()) {
291 // We need to copy as there's no way to modify nested elements! See https://bugreports.qt.io/browse/QTBUG-25723
292 QJsonObject obj = jsonData.toObject();
293 obj.insert(translateNameStr, lookupIter.value());
294 jsonData = obj;
295 }
296 } else if (jsonData[translateNameStr].isArray()) { // List of strings
297 QJsonArray jsonArray = jsonData[translateNameStr].toArray();
298 for (int i=0; i < jsonArray.count(); ++i) {
299 auto lookupIter = translations.find(prefix + '/' + translateNameStr + '/' + QString::number(i));
300 if (lookupIter != translations.end()) {
301 jsonArray.replace(i, lookupIter.value());
302 }
303 }
304 QJsonObject obj = jsonData.toObject();
305 obj[translateNameStr] = jsonArray;
306 jsonData = obj;
307 }
308 }
309 }
310 }
311
312 if (translationObj.contains("translate-global")) {
313 for (const auto& translateName : translationObj["translate-global"].toArray()) {
314 QString translateNameStr = translateName.toString();
315 if (jsonData.toObject().contains(translateNameStr)) {
316 if (jsonData[translateNameStr].isString()) {
317 auto lookupIter = translations.find("$globals/" + translateNameStr + "/" + jsonData[translateNameStr].toString());
318 if (lookupIter != translations.end()) {
319 QJsonObject obj = jsonData.toObject();
320 obj.insert(translateNameStr, lookupIter.value());
321 jsonData = obj;
322 }
323 } else if (jsonData[translateNameStr].isArray()) { // List of strings
324 QJsonArray jsonArray = jsonData[translateNameStr].toArray();
325 for (int i=0; i < jsonArray.count(); ++i) {
326 auto lookupIter = translations.find("$globals/" + translateNameStr + '/' + jsonArray[i].toString());
327 if (lookupIter != translations.end()) {
328 jsonArray.replace(i, lookupIter.value());
329 }
330 }
331 QJsonObject obj = jsonData.toObject();
332 obj[translateNameStr] = jsonArray;
333 jsonData = obj;
334 }
335 }
336 }
337 }
338 if (translationObj.contains("items")) {
339 jsonData = translateItems(prefix, defs, translationObj["items"].toObject(), translations, jsonData.toObject());
340 }
341 if (translationObj.contains("$ref")) {
342 jsonData = translateTranslationItems(prefix, defs, defs[getRefName(translationObj["$ref"].toString())].toObject(), translations, jsonData);
343 }
344 return jsonData;
345}
Cached file download with time-based expiration and fallback support.
QString errorString
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void downloadComplete(QString translatedJsonTempFile, QString errorMsg)
bool downloadAndTranslate(const QString &summaryJsonFile, const QString &toTranslateJsonFile, int maxCacheAgeSec, const QString &componentName)
QString translateJsonUsingTS(const QString &toTranslateJsonFile, const QString &tsFile)
Cached file download with time-based expiration.
bool download(const QString &url, int maxCacheAgeSec)
void finished(bool success, const QString &localPath, const QString &errorMessage, bool fromCache)
bool isJsonFile(const QByteArray &bytes, QJsonDocument &jsonDoc, QString &errorString)
Determines whether an in-memory byte buffer contains parseable JSON content.
bool validate(const QJsonDocument &doc, const QString &schemaResourcePath, QString &errorString)
QString decompressIfNeeded(const QString &filePath, const QString &outputPath, bool removeOriginal)