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