diff --git a/UnitTest.qrc b/UnitTest.qrc
index ed67ae4..0423748 100644
--- a/UnitTest.qrc
+++ b/UnitTest.qrc
@@ -15,6 +15,8 @@
src/MissionManager/UnitTest/PolygonBadXml.kml
src/MissionManager/UnitTest/PolygonBadCoordinatesNode.kml
src/comm/MockLinkOptionsDlg.qml
+ src/qgcunittest/TranslationTest.json
+ src/qgcunittest/TranslationTest_de_DE.ts
src/comm/MockLinkOptionsDlg.qml
diff --git a/qgroundcontrol.pro b/qgroundcontrol.pro
index 67f96b2..8ee00d9 100644
--- a/qgroundcontrol.pro
+++ b/qgroundcontrol.pro
@@ -499,6 +499,7 @@ DebugBuild { PX4FirmwarePlugin { PX4FirmwarePluginFactory { APMFirmwarePlugin {
src/MissionManager/TransectStyleComplexItemTestBase.h \
src/MissionManager/VisualMissionItemTest.h \
src/qgcunittest/ComponentInformationCacheTest.h \
+ src/qgcunittest/ComponentInformationTranslationTest.h \
src/qgcunittest/GeoTest.h \
src/qgcunittest/MavlinkLogTest.h \
src/qgcunittest/MultiSignalSpy.h \
@@ -547,6 +548,7 @@ DebugBuild { PX4FirmwarePlugin { PX4FirmwarePluginFactory { APMFirmwarePlugin {
src/MissionManager/TransectStyleComplexItemTestBase.cc \
src/MissionManager/VisualMissionItemTest.cc \
src/qgcunittest/ComponentInformationCacheTest.cc \
+ src/qgcunittest/ComponentInformationTranslationTest.cc \
src/qgcunittest/GeoTest.cc \
src/qgcunittest/MavlinkLogTest.cc \
src/qgcunittest/MultiSignalSpy.cc \
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index fc2baa9..b844dc7 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -33,6 +33,7 @@ if(BUILD_TESTING)
add_subdirectory(qgcunittest)
add_qgc_test(ComponentInformationCacheTest)
+ add_qgc_test(ComponentInformationTranslationTest)
add_qgc_test(CameraCalcTest)
add_qgc_test(CameraSectionTest)
add_qgc_test(CorridorScanComplexItemTest)
diff --git a/src/qgcunittest/CMakeLists.txt b/src/qgcunittest/CMakeLists.txt
index c1228f2..9de2956 100644
--- a/src/qgcunittest/CMakeLists.txt
+++ b/src/qgcunittest/CMakeLists.txt
@@ -6,6 +6,8 @@ add_library(qgcunittest
#FileManagerTest.h
ComponentInformationCacheTest.cc
ComponentInformationCacheTest.h
+ ComponentInformationTranslationTest.cc
+ ComponentInformationTranslationTest.h
GeoTest.cc
GeoTest.h
#MainWindowTest.cc
diff --git a/src/qgcunittest/ComponentInformationTranslationTest.cc b/src/qgcunittest/ComponentInformationTranslationTest.cc
new file mode 100644
index 0000000..3e1c477
--- /dev/null
+++ b/src/qgcunittest/ComponentInformationTranslationTest.cc
@@ -0,0 +1,46 @@
+/****************************************************************************
+ *
+ * (c) 2021 QGROUNDCONTROL PROJECT
+ *
+ * QGroundControl is licensed according to the terms in the file
+ * COPYING.md in the root of the source code directory.
+ *
+ ****************************************************************************/
+
+
+#include "ComponentInformationTranslationTest.h"
+
+void ComponentInformationTranslationTest::_basic_test()
+{
+ QString translationJson = ":/unittest/TranslationTest.json";
+ QString translationTs = ":/unittest/TranslationTest_de_DE.ts";
+ ComponentInformationTranslation* translation = new ComponentInformationTranslation(this, new QGCCachedFileDownload(this, ""));
+ QString tempFilename = translation->translateJsonUsingTS(translationJson, translationTs);
+
+ QVERIFY(!tempFilename.isEmpty());
+
+ // Compare json files
+ QFile translationJsonFile(translationJson);
+ QVERIFY(translationJsonFile.open(QFile::ReadOnly | QFile::Text));
+ QByteArray expectedOutput = translationJsonFile.readAll().replace("translate-me", "TRANSLATED");
+
+ QJsonDocument expectedJson;
+ readJson(expectedOutput, expectedJson);
+
+ QFile tempJson(tempFilename);
+ QVERIFY(tempJson.open(QFile::ReadOnly | QFile::Text));
+ QByteArray translatedOutput = tempJson.readAll();
+ QJsonDocument translatedJson;
+ readJson(translatedOutput, translatedJson);
+
+ QVERIFY(expectedJson == translatedJson);
+}
+
+void ComponentInformationTranslationTest::readJson(const QByteArray& bytes, QJsonDocument& jsonDoc)
+{
+ QJsonParseError parseError;
+ jsonDoc = QJsonDocument::fromJson(bytes, &parseError);
+ QTEST_ASSERT(parseError.error == QJsonParseError::NoError);
+ QVERIFY(!jsonDoc.isEmpty());
+}
+
diff --git a/src/qgcunittest/ComponentInformationTranslationTest.h b/src/qgcunittest/ComponentInformationTranslationTest.h
new file mode 100644
index 0000000..e1d9908
--- /dev/null
+++ b/src/qgcunittest/ComponentInformationTranslationTest.h
@@ -0,0 +1,34 @@
+/****************************************************************************
+ *
+ * (c) 2021 QGROUNDCONTROL PROJECT
+ *
+ * QGroundControl is licensed according to the terms in the file
+ * COPYING.md in the root of the source code directory.
+ *
+ ****************************************************************************/
+
+
+#pragma once
+
+#include "ComponentInformationTranslation.h"
+
+#include "UnitTest.h"
+
+#include
+#include
+#include
+
+class ComponentInformationTranslationTest : public UnitTest
+{
+ Q_OBJECT
+
+public:
+ ComponentInformationTranslationTest() = default;
+ virtual ~ComponentInformationTranslationTest() = default;
+
+private slots:
+ void _basic_test();
+private:
+ void readJson(const QByteArray& bytes, QJsonDocument& jsonDoc);
+};
+
diff --git a/src/qgcunittest/TranslationTest.json b/src/qgcunittest/TranslationTest.json
new file mode 100644
index 0000000..cb24643
--- /dev/null
+++ b/src/qgcunittest/TranslationTest.json
@@ -0,0 +1,151 @@
+{
+ "version": 1,
+ "translation": {
+ "items": {
+ "first_element": {
+ "items": {
+ "first_list_element": {
+ "list": {
+ "key": "name",
+ "translate": ["label", "text"]
+ }
+ },
+ "second_list_element": {
+ "list": {
+ "translate": ["label", "text"]
+ }
+ },
+ "object1": {
+ "translate": ["list_of_strings"]
+ },
+ "object2": {
+ "items": {
+ "*": {
+ "translate": ["name"]
+ }
+ }
+ }
+ }
+ },
+ "second_element": {
+ "items": {
+ "*": {
+ "translate-global": ["category"]
+ }
+ }
+ },
+ "third_element": {
+ "items": {
+ "subgroups": {
+ "$ref": "#/$defs/recursive-def"
+ }
+ }
+ }
+ },
+ "$defs": {
+ "recursive-def": {
+ "list": {
+ "translate": ["description"],
+ "items": {
+ "subgroups": {
+ "$ref": "#/$defs/recursive-def"
+ }
+ }
+ }
+ }
+ }
+ },
+
+ "first_element": {
+ "first_list_element": [
+ {
+ "name": "1. element",
+ "label": "translate-me-list1-1.0",
+ "text": "translate-me-list1-1.1"
+ },
+ {
+ "name": "2. element",
+ "label": "translate-me-list1-2.0",
+ "text": "translate-me-list1-2.1"
+ },
+ {
+ "name": "3. element",
+ "label": "translate-me-list1-3.0",
+ "text": "translate-me-list1-3.1"
+ }
+ ],
+ "second_list_element": [
+ {
+ "name": "1. element",
+ "label": "translate-me-list2-1.0",
+ "text": "translate-me-list2-1.1"
+ },
+ {
+ "name": "2. element",
+ "label": "translate-me-list2-2.0",
+ "text": "translate-me-list2-2.1"
+ },
+ {
+ "name": "3. element",
+ "label": "translate-me-list2-3.0",
+ "text": "translate-me-list2-3.1"
+ }
+ ],
+ "object1": {
+ "list_of_strings": ["translate-me-list-of-strings-1", "translate-me-list-of-strings-2", "translate-me-list-of-strings-3"]
+ },
+ "object2": {
+ "key1": {
+ "name": "translate-me-name1"
+ },
+ "key2": {
+ "name": "translate-me-name2"
+ },
+ "key3": {
+ "name": "translate-me-name3"
+ }
+ }
+ },
+ "second_element": {
+ "element1": {
+ "category": "translate-me-global-cat1"
+ },
+ "element2": {
+ "category": "translate-me-global-cat2"
+ },
+ "element3": {
+ "category": "translate-me-global-cat1"
+ },
+ "element4": {
+ "category": "translate-me-global-cat2"
+ },
+ "element5": {
+ "category": "translate-me-global-cat <> special symbol"
+ }
+ },
+ "third_element": {
+ "subgroups": [
+ {
+ "description": "translate-me-subgroup1",
+ "subgroups": [
+ {
+ "description": "translate-me-subgroup1-1",
+ "subgroups": [
+ {
+ "description": "translate-me-subgroup1-1-1"
+ }
+ ]
+ },
+ {
+ "description": "translate-me-subgroup1-2",
+ "subgroups": [
+ {
+ "description": "translate-me-subgroup1-2-1"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/src/qgcunittest/TranslationTest_de_DE.ts b/src/qgcunittest/TranslationTest_de_DE.ts
new file mode 100644
index 0000000..5666215
--- /dev/null
+++ b/src/qgcunittest/TranslationTest_de_DE.ts
@@ -0,0 +1,186 @@
+
+
+
+
+ /first_element/first_list_element/1. element/label
+
+ translate-me-list1-1.0
+ TRANSLATED-list1-1.0
+
+
+
+ /first_element/first_list_element/1. element/text
+
+ translate-me-list1-1.1
+ TRANSLATED-list1-1.1
+
+
+
+ /first_element/first_list_element/2. element/label
+
+ translate-me-list1-2.0
+ TRANSLATED-list1-2.0
+
+
+
+ /first_element/first_list_element/2. element/text
+
+ translate-me-list1-2.1
+ TRANSLATED-list1-2.1
+
+
+
+ /first_element/first_list_element/3. element/label
+
+ translate-me-list1-3.0
+ TRANSLATED-list1-3.0
+
+
+
+ /first_element/first_list_element/3. element/text
+
+ translate-me-list1-3.1
+ TRANSLATED-list1-3.1
+
+
+
+ /first_element/second_list_element/0/label
+
+ translate-me-list2-1.0
+ TRANSLATED-list2-1.0
+
+
+
+ /first_element/second_list_element/0/text
+
+ translate-me-list2-1.1
+ TRANSLATED-list2-1.1
+
+
+
+ /first_element/second_list_element/1/label
+
+ translate-me-list2-2.0
+ TRANSLATED-list2-2.0
+
+
+
+ /first_element/second_list_element/1/text
+
+ translate-me-list2-2.1
+ TRANSLATED-list2-2.1
+
+
+
+ /first_element/second_list_element/2/label
+
+ translate-me-list2-3.0
+ TRANSLATED-list2-3.0
+
+
+
+ /first_element/second_list_element/2/text
+
+ translate-me-list2-3.1
+ TRANSLATED-list2-3.1
+
+
+
+ /first_element/object1/list_of_strings/0
+
+ translate-me-list-of-strings-1
+ TRANSLATED-list-of-strings-1
+
+
+
+ /first_element/object1/list_of_strings/1
+
+ translate-me-list-of-strings-2
+ TRANSLATED-list-of-strings-2
+
+
+
+ /first_element/object1/list_of_strings/2
+
+ translate-me-list-of-strings-3
+ TRANSLATED-list-of-strings-3
+
+
+
+ /first_element/object2/key1/name
+
+ translate-me-name1
+ TRANSLATED-name1
+
+
+
+ /first_element/object2/key2/name
+
+ translate-me-name2
+ TRANSLATED-name2
+
+
+
+ /first_element/object2/key3/name
+
+ translate-me-name3
+ TRANSLATED-name3
+
+
+
+ /third_element/subgroups/0/description
+
+ translate-me-subgroup1
+ TRANSLATED-subgroup1
+
+
+
+ /third_element/subgroups/0/subgroups/0/description
+
+ translate-me-subgroup1-1
+ TRANSLATED-subgroup1-1
+
+
+
+ /third_element/subgroups/0/subgroups/0/subgroups/0/description
+
+ translate-me-subgroup1-1-1
+ TRANSLATED-subgroup1-1-1
+
+
+
+ /third_element/subgroups/0/subgroups/1/description
+
+ translate-me-subgroup1-2
+ TRANSLATED-subgroup1-2
+
+
+
+ /third_element/subgroups/0/subgroups/1/subgroups/0/description
+
+ translate-me-subgroup1-2-1
+ TRANSLATED-subgroup1-2-1
+
+
+
+ $globals/category/translate-me-global-cat <> special symbol
+
+ translate-me-global-cat <> special symbol
+ TRANSLATED-global-cat <> special symbol
+
+
+
+ $globals/category/translate-me-global-cat2
+
+ translate-me-global-cat2
+ TRANSLATED-global-cat2
+
+
+
+ $globals/category/translate-me-global-cat1
+
+ translate-me-global-cat1
+ TRANSLATED-global-cat1
+
+
+
diff --git a/src/qgcunittest/UnitTestList.cc b/src/qgcunittest/UnitTestList.cc
index 222c49f..ec912e8 100644
--- a/src/qgcunittest/UnitTestList.cc
+++ b/src/qgcunittest/UnitTestList.cc
@@ -12,6 +12,7 @@
// ones are enabled/disabled
#include "ComponentInformationCacheTest.h"
+#include "ComponentInformationTranslationTest.h"
#include "FactSystemTestGeneric.h"
#include "FactSystemTestPX4.h"
//#include "FileDialogTest.h"
@@ -52,6 +53,7 @@
#include "InitialConnectTest.h"
UT_REGISTER_TEST(ComponentInformationCacheTest)
+UT_REGISTER_TEST(ComponentInformationTranslationTest)
UT_REGISTER_TEST(FactSystemTestGeneric)
UT_REGISTER_TEST(FactSystemTestPX4)
//UT_REGISTER_TEST(FileDialogTest)