1// Aseprite Code Generator
2// Copyright (c) 2021 Igara Studio S.A.
3// Copyright (c) 2016-2018 David Capello
4//
5// This file is released under the terms of the MIT license.
6// Read LICENSE.txt for more information.
7
8#include "gen/check_strings.h"
9
10#include "base/exception.h"
11#include "base/file_handle.h"
12#include "base/fs.h"
13#include "base/split_string.h"
14#include "base/string.h"
15#include "cfg/cfg.h"
16#include "gen/common.h"
17
18#include "tinyxml.h"
19
20#include <cctype>
21#include <iostream>
22#include <memory>
23#include <stdexcept>
24#include <vector>
25
26typedef std::vector<TiXmlElement*> XmlElements;
27
28static std::string find_first_id(TiXmlElement* elem)
29{
30 TiXmlElement* child = elem->FirstChildElement();
31 while (child) {
32 const char* id = child->Attribute("id");
33 if (id)
34 return id;
35
36 std::string idStr = find_first_id(child);
37 if (!idStr.empty())
38 return idStr;
39
40 child = child->NextSiblingElement();
41 }
42 return "";
43}
44
45static void collect_elements_with_strings(TiXmlElement* elem, XmlElements& elems)
46{
47 TiXmlElement* child = elem->FirstChildElement();
48 while (child) {
49 const char* text = child->Attribute("text");
50 const char* tooltip = child->Attribute("tooltip");
51 if (text || tooltip)
52 elems.push_back(child);
53 collect_elements_with_strings(child, elems);
54 child = child->NextSiblingElement();
55 }
56}
57
58static bool has_alpha_char(const char* p)
59{
60 while (*p) {
61 if (std::isalpha(*p))
62 return true;
63 else
64 ++p;
65 }
66 return false;
67}
68
69static bool is_email(const char* p)
70{
71 if (!*p || !std::isalpha(*p))
72 return false;
73 ++p;
74
75 while (*p && (std::isalpha(*p) || *p == '.'))
76 ++p;
77
78 if (*p != '@')
79 return false;
80 ++p;
81
82 while (*p && (std::isalpha(*p) || *p == '.'))
83 ++p;
84
85 // Return true if we are in the end of string
86 return (*p == 0);
87}
88
89class CheckStrings {
90public:
91
92 void loadStrings(const std::string& dir) {
93 for (const auto& fn : base::list_files(dir)) {
94 std::unique_ptr<cfg::CfgFile> f(new cfg::CfgFile);
95 f->load(base::join_path(dir, fn));
96 m_stringFiles.push_back(std::move(f));
97 }
98 }
99
100 void checkStringsOnWidgets(const std::string& dir) {
101 for (const auto& fn : base::list_files(dir)) {
102 std::string fullFn = base::join_path(dir, fn);
103 base::FileHandle inputFile(base::open_file(fullFn, "rb"));
104 std::unique_ptr<TiXmlDocument> doc(new TiXmlDocument());
105 doc->SetValue(fullFn.c_str());
106 if (!doc->LoadFile(inputFile.get())) {
107 std::cerr << doc->Value() << ":"
108 << doc->ErrorRow() << ":"
109 << doc->ErrorCol() << ": "
110 << "error " << doc->ErrorId() << ": "
111 << doc->ErrorDesc() << "\n";
112
113 throw std::runtime_error("invalid input file");
114 }
115
116 TiXmlHandle handle(doc.get());
117 XmlElements widgets;
118
119 const char* warnings = doc->RootElement()->Attribute("i18nwarnings");
120 if (warnings && strcmp(warnings, "false") == 0)
121 continue;
122
123 m_prefixId = find_first_id(doc->RootElement());
124
125 collect_elements_with_strings(doc->RootElement(), widgets);
126 for (TiXmlElement* elem : widgets) {
127 checkString(elem, elem->Attribute("text"));
128 checkString(elem, elem->Attribute("tooltip"));
129 }
130 }
131 }
132
133 void checkStringsOnGuiFile(const std::string& fullFn) {
134 base::FileHandle inputFile(base::open_file(fullFn, "rb"));
135 std::unique_ptr<TiXmlDocument> doc(new TiXmlDocument());
136 doc->SetValue(fullFn.c_str());
137 if (!doc->LoadFile(inputFile.get())) {
138 std::cerr << doc->Value() << ":"
139 << doc->ErrorRow() << ":"
140 << doc->ErrorCol() << ": "
141 << "error " << doc->ErrorId() << ": "
142 << doc->ErrorDesc() << "\n";
143
144 throw std::runtime_error("invalid input file");
145 }
146
147 TiXmlHandle handle(doc.get());
148
149 // For each menu
150 TiXmlElement* xmlMenu = handle
151 .FirstChild("gui")
152 .FirstChild("menus")
153 .FirstChild("menu").ToElement();
154 while (xmlMenu) {
155 const char* menuId = xmlMenu->Attribute("id");
156 if (menuId) {
157 m_prefixId = menuId;
158 XmlElements menus;
159 collect_elements_with_strings(xmlMenu, menus);
160 for (TiXmlElement* elem : menus)
161 checkString(elem, elem->Attribute("text"));
162 }
163 xmlMenu = xmlMenu->NextSiblingElement();
164 }
165
166 // For each tool
167 m_prefixId = "tools";
168 TiXmlElement* xmlGroup = handle
169 .FirstChild("gui")
170 .FirstChild("tools")
171 .FirstChild("group").ToElement();
172 while (xmlGroup) {
173 XmlElements tools;
174 collect_elements_with_strings(xmlGroup, tools);
175 for (TiXmlElement* elem : tools) {
176 checkString(elem, elem->Attribute("text"));
177 checkString(elem, elem->Attribute("tooltip"));
178 }
179 xmlGroup = xmlGroup->NextSiblingElement();
180 }
181 }
182
183 void checkString(TiXmlElement* elem, const char* text) {
184 if (!text)
185 return; // Do nothing
186 else if (text[0] == '@') {
187 for (auto& cfg : m_stringFiles) {
188 std::string lang = base::get_file_title(cfg->filename());
189 std::string section, var;
190
191 if (text[1] == '.') {
192 section = m_prefixId.c_str();
193 var = text+2;
194 }
195 else {
196 std::vector<std::string> parts;
197 base::split_string(text, parts, ".");
198 if (parts.size() >= 1) section = parts[0].c_str()+1;
199 if (parts.size() >= 2) var = parts[1];
200 }
201
202 const char* translated =
203 cfg->getValue(section.c_str(), var.c_str(), nullptr);
204 if (!translated || translated[0] == 0) {
205 std::cerr << elem->GetDocument()->Value() << ":"
206 << elem->Row() << ":"
207 << elem->Column() << ": "
208 << "warning: <" << lang
209 << "> translation for a string ID wasn't found '"
210 << text << "' (" << section << "." << var << ")\n";
211 }
212 }
213 }
214 else if (text[0] != '!' &&
215 has_alpha_char(text) &&
216 !is_email(text)) {
217 std::cerr << elem->GetDocument()->Value() << ":"
218 << elem->Row() << ":"
219 << elem->Column() << ": "
220 << "warning: raw string found '"
221 << text << "'\n";
222 }
223 }
224
225private:
226 std::vector<std::unique_ptr<cfg::CfgFile>> m_stringFiles;
227 std::string m_prefixId;
228};
229
230void check_strings(const std::string& widgetsDir,
231 const std::string& stringsDir,
232 const std::string& guiFile)
233{
234 CheckStrings cs;
235 cs.loadStrings(stringsDir);
236 cs.checkStringsOnWidgets(widgetsDir);
237 cs.checkStringsOnGuiFile(guiFile);
238}
239