From 9377471425e4d4281b448995acce9ec21b78a0c2 Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 22 Dec 2016 14:19:47 -0300 Subject: [PATCH] Add command to check translations between widgets <-> strings file --- src/app/CMakeLists.txt | 10 +++ src/gen/CMakeLists.txt | 2 + src/gen/check_strings.cpp | 158 ++++++++++++++++++++++++++++++++++++++ src/gen/check_strings.h | 16 ++++ src/gen/gen.cpp | 11 ++- 5 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 src/gen/check_strings.cpp create mode 100644 src/gen/check_strings.h diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 960030232..5cea39af9 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -53,6 +53,16 @@ add_custom_command( DEPENDS gen) list(APPEND generated_files ${output_fn}) +# Check translations +file(GLOB string_files ${CMAKE_SOURCE_DIR}/data/strings/*.ini) +set(output_fn ${CMAKE_CURRENT_BINARY_DIR}/check-translations.txt) +add_custom_command( + OUTPUT ${output_fn} + COMMAND ${CMAKE_BINARY_DIR}/bin/gen --widgets-dir "${CMAKE_SOURCE_DIR}/data/widgets/" --strings-dir "${CMAKE_SOURCE_DIR}/data/strings/" >${output_fn} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS gen ${widget_files} ${string_files}) +list(APPEND generated_files ${output_fn}) + # Directory where generated files by "gen" utility will stay. include_directories(${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/gen/CMakeLists.txt b/src/gen/CMakeLists.txt index e2fb81c27..a031743ca 100644 --- a/src/gen/CMakeLists.txt +++ b/src/gen/CMakeLists.txt @@ -2,6 +2,7 @@ # Copyright (C) 2014-2016 David Capello add_executable(gen + check_strings.cpp gen.cpp pref_types.cpp skin_class.cpp @@ -15,4 +16,5 @@ endif() target_link_libraries(gen laf-base + cfg-lib ${TINYXML_LIBRARY}) diff --git a/src/gen/check_strings.cpp b/src/gen/check_strings.cpp new file mode 100644 index 000000000..dd27620ee --- /dev/null +++ b/src/gen/check_strings.cpp @@ -0,0 +1,158 @@ +// Aseprite Code Generator +// Copyright (c) 2016 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "gen/check_strings.h" + +#include "base/exception.h" +#include "base/file_handle.h" +#include "base/fs.h" +#include "base/split_string.h" +#include "base/string.h" +#include "base/unique_ptr.h" +#include "cfg/cfg.h" +#include "gen/common.h" + +#include "tinyxml.h" + +#include +#include +#include + +typedef std::vector XmlElements; + +static std::string find_first_id(TiXmlElement* elem) +{ + TiXmlElement* child = elem->FirstChildElement(); + while (child) { + const char* id = child->Attribute("id"); + if (id) + return id; + + std::string idStr = find_first_id(child); + if (!idStr.empty()) + return idStr; + + child = child->NextSiblingElement(); + } + return ""; +} + +static void collect_widgets_with_strings(TiXmlElement* elem, XmlElements& widgets) +{ + TiXmlElement* child = elem->FirstChildElement(); + while (child) { + const char* text = child->Attribute("text"); + const char* tooltip = child->Attribute("tooltip"); + if (text || tooltip) + widgets.push_back(child); + collect_widgets_with_strings(child, widgets); + child = child->NextSiblingElement(); + } +} + +static bool has_alpha_char(const char* p) +{ + while (*p) { + if (std::isalpha(*p)) + return true; + else + ++p; + } + return false; +} + +class CheckStrings { +public: + + void loadStrings(const std::string& dir) { + for (const auto& fn : base::list_files(dir)) { + cfg::CfgFile* f = new cfg::CfgFile; + f->load(base::join_path(dir, fn)); + m_stringFiles.push_back(f); + } + } + + void checkStringsOnWidgets(const std::string& dir) { + for (const auto& fn : base::list_files(dir)) { + std::string fullFn = base::join_path(dir, fn); + base::FileHandle inputFile(base::open_file(fullFn, "rb")); + base::UniquePtr doc(new TiXmlDocument()); + doc->SetValue(fullFn.c_str()); + if (!doc->LoadFile(inputFile.get())) { + std::cerr << doc->Value() << ":" + << doc->ErrorRow() << ":" + << doc->ErrorCol() << ": " + << "error " << doc->ErrorId() << ": " + << doc->ErrorDesc() << "\n"; + + throw std::runtime_error("invalid input file"); + } + + TiXmlHandle handle(doc); + XmlElements widgets; + + m_prefixId = find_first_id(doc->RootElement()); + + collect_widgets_with_strings(doc->RootElement(), widgets); + for (TiXmlElement* elem : widgets) { + checkString(elem, elem->Attribute("text")); + checkString(elem, elem->Attribute("tooltip")); + } + } + } + + void checkString(TiXmlElement* elem, const char* text) { + if (!text) + return; // Do nothing + else if (text[0] == '@') { + for (auto cfg : m_stringFiles) { + std::string lang = base::get_file_title(cfg->filename()); + std::string section, var; + + if (text[1] == '.') { + section = m_prefixId.c_str(); + var = text+2; + } + else { + std::vector parts; + base::split_string(text, parts, "."); + if (parts.size() >= 1) section = parts[0].c_str()+1; + if (parts.size() >= 2) var = parts[1]; + } + + const char* translated = + cfg->getValue(section.c_str(), var.c_str(), nullptr); + if (!translated || translated[0] == 0) { + std::cerr << elem->GetDocument()->Value() << ":" + << elem->Row() << ":" + << elem->Column() << ": " + << "warning: <" << lang + << "> translation for a string ID wasn't found '" + << text << "' (" << section << "." << var << ")\n"; + } + } + } + else if (has_alpha_char(text)) { + std::cerr << elem->GetDocument()->Value() << ":" + << elem->Row() << ":" + << elem->Column() << ": " + << "warning: raw string found '" + << text << "'\n"; + } + } + +private: + std::vector m_stringFiles; + std::string m_prefixId; +}; + +void check_strings(const std::string& widgetsDir, + const std::string& stringsDir) +{ + CheckStrings cs; + cs.loadStrings(stringsDir); + cs.checkStringsOnWidgets(widgetsDir); +} diff --git a/src/gen/check_strings.h b/src/gen/check_strings.h new file mode 100644 index 000000000..e143d8a8d --- /dev/null +++ b/src/gen/check_strings.h @@ -0,0 +1,16 @@ +// Aseprite Code Generator +// Copyright (c) 2016 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef GEN_CHECK_STRINGS_H_INCLUDED +#define GEN_CHECK_STRINGS_H_INCLUDED +#pragma once + +#include + +void check_strings(const std::string& widgetsDir, + const std::string& stringsDir); + +#endif diff --git a/src/gen/gen.cpp b/src/gen/gen.cpp index 9377f4c3d..c3fe88897 100644 --- a/src/gen/gen.cpp +++ b/src/gen/gen.cpp @@ -8,9 +8,11 @@ #include "base/fs.h" #include "base/program_options.h" #include "base/string.h" +#include "gen/check_strings.h" #include "gen/pref_types.h" #include "gen/skin_class.h" #include "gen/ui_class.h" +#include "gen/check_strings.h" #include "tinyxml.h" #include @@ -25,10 +27,12 @@ static void run(int argc, const char* argv[]) PO::Option& prefH = po.add("pref-h"); PO::Option& prefCpp = po.add("pref-cpp"); PO::Option& skin = po.add("skin"); + PO::Option& widgetsDir = po.add("widgets-dir").requiresValue(""); + PO::Option& stringsDir = po.add("strings-dir").requiresValue(""); po.parse(argc, argv); // Try to load the XML file - TiXmlDocument* doc = NULL; + TiXmlDocument* doc = nullptr; std::string inputFilename = po.value_of(inputOpt); if (!inputFilename.empty()) { @@ -56,6 +60,11 @@ static void run(int argc, const char* argv[]) else if (po.enabled(skin)) gen_skin_class(doc, inputFilename); } + else if (po.enabled(widgetsDir) && + po.enabled(stringsDir)) { + check_strings(po.value_of(widgetsDir), + po.value_of(stringsDir)); + } } int main(int argc, const char* argv[])