KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_translation_format_strings.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation; either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
28
29#include <boost/test/unit_test.hpp>
30
31#include <wx/dir.h>
32#include <wx/filename.h>
33#include <wx/textfile.h>
34
35#include <algorithm>
36#include <cctype>
37#include <cstdlib>
38#include <map>
39#include <set>
40#include <string>
41#include <vector>
42
43
44namespace
45{
46struct PO_ENTRY
47{
48 std::string m_msgId;
49 std::string m_msgIdPlural;
50 std::vector<std::string> m_msgStrs;
51 int m_line = 0;
52 bool m_fuzzy = false;
53};
54
55
56// Concatenate the double-quoted runs of a .po line, honoring backslash escapes.
57std::string unquote( const std::string& aLine )
58{
59 std::string out;
60 bool inQuote = false;
61
62 for( size_t i = 0; i < aLine.size(); ++i )
63 {
64 char ch = aLine[i];
65
66 if( !inQuote )
67 {
68 if( ch == '"' )
69 inQuote = true;
70
71 continue;
72 }
73
74 if( ch == '\\' && i + 1 < aLine.size() )
75 {
76 out += aLine[++i];
77 continue;
78 }
79
80 if( ch == '"' )
81 inQuote = false;
82 else
83 out += ch;
84 }
85
86 return out;
87}
88
89
90std::vector<PO_ENTRY> parsePo( const wxString& aPath )
91{
92 std::vector<PO_ENTRY> entries;
93 wxTextFile file;
94
95 if( !file.Open( aPath ) )
96 return entries;
97
98 PO_ENTRY cur;
99 bool haveCur = false;
100 bool pendingFuzzy = false;
101 enum { NONE, ID, IDP, STR } state = NONE;
102 size_t strIdx = 0;
103
104 auto starts = []( const std::string& aStr, const char* aPrefix )
105 {
106 return aStr.rfind( aPrefix, 0 ) == 0;
107 };
108
109 int lineNo = 0;
110
111 for( wxString wxLine = file.GetFirstLine(); !file.Eof(); wxLine = file.GetNextLine() )
112 {
113 ++lineNo;
114 std::string line = wxLine.Trim( false ).Trim( true ).utf8_string();
115
116 if( starts( line, "#," ) )
117 {
118 // A fuzzy flag may share its "#," line with others, so accumulate rather than overwrite.
119 if( line.find( "fuzzy" ) != std::string::npos )
120 pendingFuzzy = true;
121 }
122 else if( starts( line, "msgid_plural" ) )
123 {
124 if( haveCur )
125 cur.m_msgIdPlural = unquote( line );
126
127 state = IDP;
128 }
129 else if( starts( line, "msgid " ) )
130 {
131 if( haveCur )
132 entries.push_back( cur );
133
134 cur = PO_ENTRY();
135 cur.m_msgId = unquote( line );
136 cur.m_line = lineNo;
137 cur.m_fuzzy = pendingFuzzy;
138 pendingFuzzy = false;
139 haveCur = true;
140 state = ID;
141 }
142 else if( starts( line, "msgstr[" ) )
143 {
144 if( haveCur )
145 {
146 int idx = std::atoi( line.c_str() + sizeof( "msgstr[" ) - 1 );
147
148 // No language has dozens of plural forms; reject wild indices to bound the resize.
149 if( idx >= 0 && idx < 64 )
150 {
151 strIdx = static_cast<size_t>( idx );
152
153 if( cur.m_msgStrs.size() <= strIdx )
154 cur.m_msgStrs.resize( strIdx + 1 );
155
156 cur.m_msgStrs[strIdx] = unquote( line );
157 }
158 }
159
160 state = STR;
161 }
162 else if( starts( line, "msgstr " ) )
163 {
164 if( haveCur )
165 {
166 cur.m_msgStrs.assign( 1, unquote( line ) );
167 strIdx = 0;
168 }
169
170 state = STR;
171 }
172 else if( !line.empty() && line[0] == '"' )
173 {
174 if( haveCur && state == ID )
175 cur.m_msgId += unquote( line );
176 else if( haveCur && state == IDP )
177 cur.m_msgIdPlural += unquote( line );
178 else if( haveCur && state == STR && strIdx < cur.m_msgStrs.size() )
179 cur.m_msgStrs[strIdx] += unquote( line );
180 }
181 else if( line.empty() )
182 {
183 // Blank lines separate entries; the next msgid flushes.
184 }
185 else
186 {
187 state = NONE;
188 }
189 }
190
191 if( haveCur )
192 entries.push_back( cur );
193
194 return entries;
195}
196
197
203std::map<char, int> consumingSpecs( const std::string& aStr )
204{
205 static const std::string flags = "-+#0";
206 static const std::string convs = "diouxXeEfFgGaAcsSCp";
207
208 std::map<char, int> counts; // non-positional and '*' consumptions
209 std::map<char, std::set<int>> posArgs; // distinct positional indices per conversion
210
211 for( size_t i = 0; i < aStr.size(); ++i )
212 {
213 if( aStr[i] != '%' )
214 continue;
215
216 size_t j = i + 1;
217
218 if( j < aStr.size() && aStr[j] == '%' )
219 {
220 i = j;
221 continue;
222 }
223
224 // Optional positional argument "<digits>$".
225 int position = 0;
226 size_t k = j;
227
228 while( k < aStr.size() && std::isdigit( static_cast<unsigned char>( aStr[k] ) ) )
229 ++k;
230
231 if( k < aStr.size() && aStr[k] == '$' && k > j )
232 {
233 position = std::atoi( aStr.c_str() + j );
234 j = k + 1;
235 }
236
237 while( j < aStr.size() && flags.find( aStr[j] ) != std::string::npos )
238 ++j;
239
240 // A '*' width consumes an int argument; digits are a literal width.
241 if( j < aStr.size() && aStr[j] == '*' )
242 {
243 counts['*']++;
244 ++j;
245 }
246 else
247 {
248 while( j < aStr.size() && std::isdigit( static_cast<unsigned char>( aStr[j] ) ) )
249 ++j;
250 }
251
252 if( j < aStr.size() && aStr[j] == '.' )
253 {
254 ++j;
255
256 // A '*' precision likewise consumes an int argument.
257 if( j < aStr.size() && aStr[j] == '*' )
258 {
259 counts['*']++;
260 ++j;
261 }
262 else
263 {
264 while( j < aStr.size() && std::isdigit( static_cast<unsigned char>( aStr[j] ) ) )
265 ++j;
266 }
267 }
268
269 static const std::string lengths = "lhzjtL";
270
271 while( j < aStr.size() && lengths.find( aStr[j] ) != std::string::npos )
272 ++j;
273
274 if( j < aStr.size() && convs.find( aStr[j] ) != std::string::npos )
275 {
276 if( position > 0 )
277 posArgs[aStr[j]].insert( position );
278 else
279 counts[aStr[j]]++;
280
281 i = j;
282 }
283 }
284
285 for( const auto& [conv, positions] : posArgs )
286 counts[conv] += static_cast<int>( positions.size() );
287
288 return counts;
289}
290
291
292wxFileName poDir()
293{
294 wxFileName dir = wxFileName::DirName( wxString::FromUTF8( QA_SRC_ROOT ) );
295 dir.AppendDir( wxS( "translation" ) );
296 dir.AppendDir( wxS( "pofiles" ) );
297 return dir;
298}
299} // namespace
300
301
302BOOST_AUTO_TEST_SUITE( TranslationFormatStrings )
303
304
305
309BOOST_AUTO_TEST_CASE( NoExtraFormatSpecifiers )
310{
311 wxString poPath = poDir().GetFullPath();
312
313 BOOST_REQUIRE_MESSAGE( wxDir::Exists( poPath ),
314 "Translation pofiles directory not found: " << poPath.utf8_string() );
315
316 wxArrayString files;
317 wxDir::GetAllFiles( poPath, &files, wxS( "*.po" ), wxDIR_FILES );
318
319 BOOST_REQUIRE_MESSAGE( !files.empty(), "No .po files found in " << poPath.utf8_string() );
320
321 int violations = 0;
322
323 for( const wxString& po : files )
324 {
325 wxFileName poFile( po );
326
327 for( const PO_ENTRY& entry : parsePo( po ) )
328 {
329 if( entry.m_fuzzy )
330 continue;
331
332 // A plural form may reference either source string, so budget the per-conversion max.
333 std::map<char, int> src = consumingSpecs( entry.m_msgId );
334
335 for( const auto& [conv, count] : consumingSpecs( entry.m_msgIdPlural ) )
336 src[conv] = std::max( src[conv], count );
337
338 // Only msgids that are already format strings are in scope; literal-percent labels are
339 // skipped to avoid false positives.
340 if( src.empty() )
341 continue;
342
343 for( const std::string& msgStr : entry.m_msgStrs )
344 {
345 if( msgStr.empty() )
346 continue;
347
348 std::map<char, int> dst = consumingSpecs( msgStr );
349
350 for( const auto& [conv, count] : dst )
351 {
352 if( count > src[conv] )
353 {
354 ++violations;
355 BOOST_ERROR( poFile.GetFullName().utf8_string()
356 << ":" << entry.m_line << " translation adds extra '%" << conv
357 << "' specifier (msgid=\"" << entry.m_msgId << "\" msgstr=\""
358 << msgStr << "\")" );
359 break;
360 }
361 }
362 }
363 }
364 }
365
366 BOOST_CHECK_EQUAL( violations, 0 );
367}
368
369
@ NONE
Definition eda_shape.h:72
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
BOOST_CHECK_EQUAL(result, "25.4")