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 | |
26 | typedef std::vector<TiXmlElement*> XmlElements; |
27 | |
28 | static 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 | |
45 | static 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 | |
58 | static 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 | |
69 | static 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 | |
89 | class CheckStrings { |
90 | public: |
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* = handle |
151 | .FirstChild("gui" ) |
152 | .FirstChild("menus" ) |
153 | .FirstChild("menu" ).ToElement(); |
154 | while (xmlMenu) { |
155 | const char* = xmlMenu->Attribute("id" ); |
156 | if (menuId) { |
157 | m_prefixId = menuId; |
158 | XmlElements ; |
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 | |
225 | private: |
226 | std::vector<std::unique_ptr<cfg::CfgFile>> m_stringFiles; |
227 | std::string m_prefixId; |
228 | }; |
229 | |
230 | void 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 | |