diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5e32a26f..3bbe6733 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -569,6 +569,7 @@ if(NOT PIP_FREERTOS)
add_subdirectory("utils/code_model_generator")
add_subdirectory("utils/resources_compiler")
add_subdirectory("utils/deploy_tool")
+ add_subdirectory("utils/value_tree_translator")
if(PIP_UTILS AND (NOT CROSSTOOLS))
add_subdirectory("utils/system_test")
add_subdirectory("utils/udp_file_transfer")
diff --git a/utils/value_tree_translator/CMakeLists.txt b/utils/value_tree_translator/CMakeLists.txt
new file mode 100644
index 00000000..22139e2f
--- /dev/null
+++ b/utils/value_tree_translator/CMakeLists.txt
@@ -0,0 +1,16 @@
+list(APPEND PIP_UTILS_LIST "pip_vtt")
+set(PIP_UTILS_LIST ${PIP_UTILS_LIST} PARENT_SCOPE)
+import_version(pip_vtt PIP)
+set_deploy_property(pip_vtt
+ LABEL "PIP ValueTree translator"
+ FULLNAME "${PIP_DOMAIN}.pip_vtt"
+ COMPANY "${PIP_COMPANY}"
+ INFO "Platform-Independent Primitives")
+make_rc(pip_vtt _RC)
+add_executable(pip_vtt "main.cpp" ${_RC})
+target_link_libraries(pip_vtt pip)
+if (DEFINED LIB)
+ install(TARGETS pip_vtt DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
+else()
+ install(TARGETS pip_vtt DESTINATION bin)
+endif()
diff --git a/utils/value_tree_translator/main.cpp b/utils/value_tree_translator/main.cpp
new file mode 100644
index 00000000..5ee75a1d
--- /dev/null
+++ b/utils/value_tree_translator/main.cpp
@@ -0,0 +1,241 @@
+/*
+ PIP - Platform Independent Primitives
+ ValueTree translator
+ Ivan Pelipenko peri4ko@yandex.ru
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program. If not, see .
+*/
+
+#include "picli.h"
+#include "pidir.h"
+#include "pifile.h"
+#include "piiostream.h"
+#include "pijson.h"
+#include "pivaluetree.h"
+#include "pivaluetree_conversions.h"
+
+#include
+
+using namespace PICoutManipulators;
+using Attribute = PIValueTree::Attribute;
+
+const char help_string[] = "Read input files or entire directories as PIValueTree\n"
+ "according to extension, and create QtLinguist translation\n"
+ "file (*.ts). Extensions:\n"
+ " * conf - PIValueTreeConversions::fromText\n"
+ " * json - PIValueTreeConversions::fromJSON\n"
+ " * bin - binary deserialization\n"
+ "Output file can be translated by QtLinguist and used\n"
+ "by QAD PIValueTreeEdit widget to translate strings.\n"
+ "";
+
+void header() {
+ piCout << Bold << "PIP ValueTree translator";
+ piCout << Cyan << "Version" << Bold << PIPVersion() << NewLine;
+ piCout << Green << Bold << "Usage:" << Default
+ << "\"pip_vtt [-hHq] -l -o [] [] [...]\"" << NewLine;
+}
+
+void usage() {
+ header();
+ piCout << Green << Bold << "Details:";
+ piCout << Bold << "Debug control";
+ piCout << "-h " << Green << "- display this message and exit";
+ piCout << "-H " << Green << "- display details help";
+ piCout << "-q " << Green << "- quiet, no debug output to console";
+ piCout << "";
+ piCout << Bold << "Output control";
+ piCout << "-l " << Green << "- translation language (e.g. en_US, ru_RU)";
+ piCout << "-o " << Green << "- output file for translation (QtLinguist *.ts file)";
+ piCout << "";
+ piCout << Bold << "Input control";
+ piCout << " " << Green << "- add file or dir translation";
+}
+
+void help() {
+ header();
+ piCout << help_string;
+}
+
+void printError(const PIString & msg) {
+ std::cerr << msg.data() << std::endl;
+}
+
+
+PISet strings;
+
+void gatherStrings(const PIValueTree & vt) {
+ const static PIStringList attrs({Attribute::prefix, Attribute::suffix});
+ for (const auto & c: vt.children()) {
+ if (c.isArray()) return;
+ if (c.name().isNotEmpty()) strings << c.name();
+ if (c.comment().isNotEmpty()) strings << c.comment();
+ for (const auto & a: attrs) {
+ if (c.attributes().contains(a)) {
+ PIString sa = c.attributes().value(a).toString();
+ if (sa.isNotEmpty()) strings << sa;
+ }
+ }
+ gatherStrings(c);
+ }
+}
+
+const PIString context = "QAD::PIValueTreeEdit";
+
+struct TSMessage {
+ PIString source;
+ PIString translation;
+ PIString type;
+};
+
+PIMap readTS(const PIString & path) {
+ PIMap ret;
+ PIFile f(path, PIIODevice::ReadOnly);
+ if (!f.isOpened()) return ret;
+ PIIOTextStream ts(&f);
+ TSMessage msg;
+ int phase = 0;
+ while (!ts.isEnd()) {
+ auto line = ts.readLine().trim();
+ switch (phase) {
+ case 0:
+ if (line == "") phase = 1;
+ break;
+ case 1:
+ if (line.startsWith("")) {
+ line.cutLeft(6).cutRight(7);
+ if (line == context) phase = 2;
+ }
+ break;
+ case 2:
+ if (line == "") {
+ msg = {};
+ phase = 3;
+ }
+ break;
+ case 3:
+ if (line == "") {
+ ret[msg.source] = msg;
+ phase = 2;
+ } else if (line.startsWith("")) {
+ line.cutLeft(8).cutRight(9);
+ msg.source = line;
+ } else if (line.startsWith("');
+ while (trs.isNotEmpty()) {
+ PIString t = trs.takeCWord();
+ trs.cutLeft(1);
+ PIString v = trs.takeRange('\"', '\"');
+ if (t == "type") msg.type = v;
+ }
+ line.cutRight(14);
+ msg.translation = line;
+ }
+ break;
+ }
+ if (line == "") phase = 0;
+ }
+ return ret;
+}
+
+
+int main(int argc, char * argv[]) {
+ PICLI cli(argc, argv);
+ // piCout << cli.rawArguments();
+ cli.setOptionalArgumentsCount(-1);
+ cli.addArgument("output", true);
+ cli.addArgument("language", true);
+ cli.addArgument("help");
+ cli.addArgument("Help");
+ cli.addArgument("quiet");
+ if (cli.hasArgument("Help")) {
+ help();
+ return 0;
+ }
+ if (cli.hasArgument("help") || cli.argumentValue("output").isEmpty() || cli.argumentValue("language").isEmpty() ||
+ cli.optionalArguments().isEmpty()) {
+ usage();
+ return 0;
+ }
+ piDebug = !cli.hasArgument("quiet");
+ PIString out_path = cli.argumentValue("output");
+
+ PIStringList files;
+ const static PIStringList ext({"conf", "json", "bin"});
+ for (const PIString & a: cli.optionalArguments()) {
+ if (PIDir::isExists(a)) {
+ auto dl = PIDir(a).allEntries();
+ for (const auto & i: dl) {
+ if (!i.isFile()) continue;
+ if (ext.contains(i.extension().toLowerCase().trim())) files << i.path;
+ }
+ } else {
+ if (PIFile::isExists(a)) files << a;
+ }
+ }
+
+
+ piCout << Cyan << Bold << "Read" << files.size_s() << "files ...";
+ for (const auto & p: files) {
+ PIString ext = PIFile::FileInfo(p).extension().toLowerCase().trim();
+ PIFile f(p, PIIODevice::ReadOnly);
+ if (ext == "conf") gatherStrings(PIValueTreeConversions::fromText(PIString::fromUTF8(f.readAll())));
+ if (ext == "json") gatherStrings(PIValueTreeConversions::fromJSON(PIJSON::fromJSON(PIString::fromUTF8(f.readAll()))));
+ if (ext == "bin") gatherStrings(piDeserialize(f.readAll()));
+ }
+ piCout << Cyan << Bold << "Reading done";
+
+
+ piCout << Cyan << Bold << "Reading ts file ...";
+
+ auto old = readTS(out_path);
+
+ piCout << Cyan << Bold << "Reading done";
+
+
+ piCout << Cyan << Bold << "Writing ts file ...";
+
+ PIFile outf(out_path, PIIODevice::ReadWrite);
+ if (!outf.isOpened()) {
+ printError("Can`t open \"" + outf.path() + "\"!");
+ return 1;
+ }
+ outf.clear();
+ PIIOTextStream ts(&outf);
+ ts << "\n";
+ ts << "\n";
+ ts << "\n";
+ ts << "\n";
+ ts << " " << context << "\n";
+ for (const auto & s: strings) {
+ ts << " \n";
+ ts << " " << s.first << "\n";
+ ts << " " << m.translation << "\n";
+ ts << " \n";
+ }
+ ts << "\n";
+ ts << "\n";
+
+ piCout << Cyan << Bold << "Writing done";
+
+
+ return 0;
+}