]>
Commit | Line | Data |
---|---|---|
c277e2f8 | 1 | /* |
503b54c9 MS |
2 | * Verify that translations in the .po file have the same number and type of |
3 | * printf-style format strings. | |
c277e2f8 | 4 | * |
6d086e08 | 5 | * Copyright 2007-2017 by Apple Inc. |
503b54c9 | 6 | * Copyright 1997-2007 by Easy Software Products, all rights reserved. |
c277e2f8 | 7 | * |
e3101897 | 8 | * Licensed under Apache License v2.0. See the file "LICENSE" for more information. |
f0ab5bff | 9 | * |
c277e2f8 MS |
10 | * Usage: |
11 | * | |
6d086e08 | 12 | * checkpo filename.{po,strings} [... filenameN.{po,strings}] |
c277e2f8 MS |
13 | * |
14 | * Compile with: | |
15 | * | |
16 | * gcc -o checkpo checkpo.c `cups-config --libs` | |
c277e2f8 MS |
17 | */ |
18 | ||
71e16022 | 19 | #include <cups/cups-private.h> |
c277e2f8 MS |
20 | |
21 | ||
22 | /* | |
23 | * Local functions... | |
24 | */ | |
25 | ||
26 | static char *abbreviate(const char *s, char *buf, int bufsize); | |
27 | static cups_array_t *collect_formats(const char *id); | |
28 | static void free_formats(cups_array_t *fmts); | |
29 | ||
30 | ||
31 | /* | |
6d086e08 | 32 | * 'main()' - Validate .po and .strings files. |
c277e2f8 MS |
33 | */ |
34 | ||
35 | int /* O - Exit code */ | |
36 | main(int argc, /* I - Number of command-line args */ | |
37 | char *argv[]) /* I - Command-line arguments */ | |
38 | { | |
39 | int i; /* Looping var */ | |
40 | cups_array_t *po; /* .po file */ | |
41 | _cups_message_t *msg; /* Current message */ | |
42 | cups_array_t *idfmts, /* Format strings in msgid */ | |
43 | *strfmts; /* Format strings in msgstr */ | |
44 | char *idfmt, /* Current msgid format string */ | |
45 | *strfmt; /* Current msgstr format string */ | |
91c84a35 | 46 | int fmtidx; /* Format index */ |
c277e2f8 MS |
47 | int status, /* Exit status */ |
48 | pass, /* Pass/fail status */ | |
49 | untranslated; /* Untranslated messages */ | |
50 | char idbuf[80], /* Abbreviated msgid */ | |
51 | strbuf[80]; /* Abbreviated msgstr */ | |
52 | ||
53 | ||
54 | if (argc < 2) | |
55 | { | |
6d086e08 | 56 | puts("Usage: checkpo filename.{po,strings} [... filenameN.{po,strings}]"); |
c277e2f8 MS |
57 | return (1); |
58 | } | |
59 | ||
60 | /* | |
6d086e08 | 61 | * Check every .po or .strings file on the command-line... |
c277e2f8 MS |
62 | */ |
63 | ||
64 | for (i = 1, status = 0; i < argc; i ++) | |
65 | { | |
66 | /* | |
67 | * Use the CUPS .po loader to get the message strings... | |
68 | */ | |
69 | ||
6d086e08 | 70 | if (strstr(argv[i], ".strings")) |
84de5e92 | 71 | po = _cupsMessageLoad(argv[i], _CUPS_MESSAGE_STRINGS); |
6d086e08 | 72 | else |
fe0d6115 | 73 | po = _cupsMessageLoad(argv[i], _CUPS_MESSAGE_PO | _CUPS_MESSAGE_EMPTY); |
6d086e08 MS |
74 | |
75 | if (!po) | |
c277e2f8 MS |
76 | { |
77 | perror(argv[i]); | |
78 | return (1); | |
79 | } | |
80 | ||
0837b7e8 MS |
81 | if (i > 1) |
82 | putchar('\n'); | |
c277e2f8 MS |
83 | printf("%s: ", argv[i]); |
84 | fflush(stdout); | |
85 | ||
86 | /* | |
87 | * Scan every message for a % string and then match them up with | |
88 | * the corresponding string in the translation... | |
89 | */ | |
90 | ||
91 | pass = 1; | |
92 | untranslated = 0; | |
93 | ||
94 | for (msg = (_cups_message_t *)cupsArrayFirst(po); | |
95 | msg; | |
96 | msg = (_cups_message_t *)cupsArrayNext(po)) | |
97 | { | |
0837b7e8 MS |
98 | /* |
99 | * Make sure filter message prefixes are not translated... | |
100 | */ | |
101 | ||
89550f3f MS |
102 | if (!strncmp(msg->msg, "ALERT:", 6) || !strncmp(msg->msg, "CRIT:", 5) || |
103 | !strncmp(msg->msg, "DEBUG:", 6) || !strncmp(msg->msg, "DEBUG2:", 7) || | |
104 | !strncmp(msg->msg, "EMERG:", 6) || !strncmp(msg->msg, "ERROR:", 6) || | |
105 | !strncmp(msg->msg, "INFO:", 5) || !strncmp(msg->msg, "NOTICE:", 7) || | |
106 | !strncmp(msg->msg, "WARNING:", 8)) | |
0837b7e8 MS |
107 | { |
108 | if (pass) | |
109 | { | |
110 | pass = 0; | |
111 | puts("FAIL"); | |
112 | } | |
113 | ||
114 | printf(" Bad prefix on filter message \"%s\"\n", | |
89550f3f | 115 | abbreviate(msg->msg, idbuf, sizeof(idbuf))); |
0837b7e8 MS |
116 | } |
117 | ||
89550f3f MS |
118 | idfmt = msg->msg + strlen(msg->msg) - 1; |
119 | if (idfmt >= msg->msg && *idfmt == '\n') | |
0837b7e8 MS |
120 | { |
121 | if (pass) | |
122 | { | |
123 | pass = 0; | |
124 | puts("FAIL"); | |
125 | } | |
126 | ||
127 | printf(" Trailing newline in message \"%s\"\n", | |
89550f3f | 128 | abbreviate(msg->msg, idbuf, sizeof(idbuf))); |
0837b7e8 MS |
129 | } |
130 | ||
89550f3f | 131 | for (; idfmt >= msg->msg; idfmt --) |
0837b7e8 MS |
132 | if (!isspace(*idfmt & 255)) |
133 | break; | |
134 | ||
89550f3f | 135 | if (idfmt >= msg->msg && *idfmt == '!') |
0837b7e8 MS |
136 | { |
137 | if (pass) | |
138 | { | |
139 | pass = 0; | |
140 | puts("FAIL"); | |
141 | } | |
142 | ||
143 | printf(" Exclamation in message \"%s\"\n", | |
89550f3f | 144 | abbreviate(msg->msg, idbuf, sizeof(idbuf))); |
0837b7e8 MS |
145 | } |
146 | ||
89550f3f | 147 | if ((idfmt - 2) >= msg->msg && !strncmp(idfmt - 2, "...", 3)) |
0837b7e8 MS |
148 | { |
149 | if (pass) | |
150 | { | |
151 | pass = 0; | |
152 | puts("FAIL"); | |
153 | } | |
154 | ||
155 | printf(" Ellipsis in message \"%s\"\n", | |
89550f3f | 156 | abbreviate(msg->msg, idbuf, sizeof(idbuf))); |
0837b7e8 MS |
157 | } |
158 | ||
c277e2f8 MS |
159 | if (!msg->str || !msg->str[0]) |
160 | { | |
161 | untranslated ++; | |
162 | continue; | |
163 | } | |
89550f3f | 164 | else if (strchr(msg->msg, '%')) |
c277e2f8 | 165 | { |
89550f3f | 166 | idfmts = collect_formats(msg->msg); |
c277e2f8 MS |
167 | strfmts = collect_formats(msg->str); |
168 | fmtidx = 0; | |
169 | ||
170 | for (strfmt = (char *)cupsArrayFirst(strfmts); | |
171 | strfmt; | |
172 | strfmt = (char *)cupsArrayNext(strfmts)) | |
173 | { | |
174 | if (isdigit(strfmt[1] & 255) && strfmt[2] == '$') | |
175 | { | |
176 | /* | |
177 | * Handle positioned format stuff... | |
178 | */ | |
179 | ||
180 | fmtidx = strfmt[1] - '1'; | |
181 | strfmt += 3; | |
182 | if ((idfmt = (char *)cupsArrayIndex(idfmts, fmtidx)) != NULL) | |
183 | idfmt ++; | |
184 | } | |
185 | else | |
186 | { | |
187 | /* | |
188 | * Compare against the current format... | |
189 | */ | |
190 | ||
191 | idfmt = (char *)cupsArrayIndex(idfmts, fmtidx); | |
192 | } | |
193 | ||
194 | fmtidx ++; | |
195 | ||
196 | if (!idfmt || strcmp(strfmt, idfmt)) | |
197 | break; | |
c277e2f8 MS |
198 | } |
199 | ||
200 | if (cupsArrayCount(strfmts) != cupsArrayCount(idfmts) || strfmt) | |
201 | { | |
202 | if (pass) | |
203 | { | |
204 | pass = 0; | |
205 | puts("FAIL"); | |
206 | } | |
207 | ||
208 | printf(" Bad translation string \"%s\"\n for \"%s\"\n", | |
209 | abbreviate(msg->str, strbuf, sizeof(strbuf)), | |
89550f3f | 210 | abbreviate(msg->msg, idbuf, sizeof(idbuf))); |
c277e2f8 MS |
211 | fputs(" Translation formats:", stdout); |
212 | for (strfmt = (char *)cupsArrayFirst(strfmts); | |
213 | strfmt; | |
214 | strfmt = (char *)cupsArrayNext(strfmts)) | |
215 | printf(" %s", strfmt); | |
216 | fputs("\n Original formats:", stdout); | |
217 | for (idfmt = (char *)cupsArrayFirst(idfmts); | |
218 | idfmt; | |
219 | idfmt = (char *)cupsArrayNext(idfmts)) | |
220 | printf(" %s", idfmt); | |
221 | putchar('\n'); | |
f0ab5bff | 222 | putchar('\n'); |
c277e2f8 MS |
223 | } |
224 | ||
225 | free_formats(idfmts); | |
226 | free_formats(strfmts); | |
227 | } | |
228 | ||
8b116e60 MS |
229 | /* |
230 | * Only allow \\, \n, \r, \t, \", and \### character escapes... | |
231 | */ | |
232 | ||
233 | for (strfmt = msg->str; *strfmt; strfmt ++) | |
84de5e92 MS |
234 | { |
235 | if (*strfmt == '\\') | |
236 | { | |
237 | strfmt ++; | |
238 | ||
239 | if (*strfmt != '\\' && *strfmt != 'n' && *strfmt != 'r' && *strfmt != 't' && *strfmt != '\"' && !isdigit(*strfmt & 255)) | |
8b116e60 | 240 | { |
84de5e92 MS |
241 | if (pass) |
242 | { | |
243 | pass = 0; | |
244 | puts("FAIL"); | |
245 | } | |
246 | ||
247 | printf(" Bad escape \\%c in filter message \"%s\"\n" | |
248 | " for \"%s\"\n", strfmt[1], | |
249 | abbreviate(msg->str, strbuf, sizeof(strbuf)), | |
250 | abbreviate(msg->msg, idbuf, sizeof(idbuf))); | |
251 | break; | |
8b116e60 | 252 | } |
84de5e92 MS |
253 | } |
254 | } | |
c277e2f8 MS |
255 | } |
256 | ||
257 | if (pass) | |
258 | { | |
340e5f8f MS |
259 | int count = cupsArrayCount(po); /* Total number of messages */ |
260 | ||
261 | if (untranslated >= (count / 10) && strcmp(argv[i], "cups.pot")) | |
c277e2f8 MS |
262 | { |
263 | /* | |
264 | * Only allow 10% of messages to be untranslated before we fail... | |
265 | */ | |
266 | ||
267 | pass = 0; | |
268 | puts("FAIL"); | |
340e5f8f | 269 | printf(" Too many untranslated messages (%d of %d or %.1f%% are translated)\n", count - untranslated, count, 100.0 - 100.0 * untranslated / count); |
c277e2f8 MS |
270 | } |
271 | else if (untranslated > 0) | |
340e5f8f | 272 | printf("PASS (%d of %d or %.1f%% are translated)\n", count - untranslated, count, 100.0 - 100.0 * untranslated / count); |
c277e2f8 | 273 | else |
0837b7e8 | 274 | puts("PASS"); |
c277e2f8 MS |
275 | } |
276 | ||
277 | if (!pass) | |
278 | status = 1; | |
279 | ||
280 | _cupsMessageFree(po); | |
281 | } | |
282 | ||
283 | return (status); | |
284 | } | |
285 | ||
286 | ||
287 | /* | |
288 | * 'abbreviate()' - Abbreviate a message string as needed. | |
289 | */ | |
290 | ||
291 | static char * /* O - Abbreviated string */ | |
292 | abbreviate(const char *s, /* I - String to abbreviate */ | |
293 | char *buf, /* I - Buffer */ | |
294 | int bufsize) /* I - Size of buffer */ | |
295 | { | |
296 | char *bufptr; /* Pointer into buffer */ | |
297 | ||
298 | ||
299 | for (bufptr = buf, bufsize -= 4; *s && bufsize > 0; s ++) | |
300 | { | |
301 | if (*s == '\n') | |
302 | { | |
303 | if (bufsize < 2) | |
304 | break; | |
305 | ||
306 | *bufptr++ = '\\'; | |
307 | *bufptr++ = 'n'; | |
308 | bufsize -= 2; | |
309 | } | |
310 | else if (*s == '\t') | |
311 | { | |
312 | if (bufsize < 2) | |
313 | break; | |
314 | ||
315 | *bufptr++ = '\\'; | |
316 | *bufptr++ = 't'; | |
317 | bufsize -= 2; | |
318 | } | |
319 | else if (*s >= 0 && *s < ' ') | |
320 | { | |
321 | if (bufsize < 4) | |
322 | break; | |
323 | ||
324 | sprintf(bufptr, "\\%03o", *s); | |
325 | bufptr += 4; | |
326 | bufsize -= 4; | |
327 | } | |
328 | else | |
329 | { | |
330 | *bufptr++ = *s; | |
331 | bufsize --; | |
332 | } | |
333 | } | |
334 | ||
335 | if (*s) | |
5a9febac | 336 | memcpy(bufptr, "...", 4); |
c277e2f8 MS |
337 | else |
338 | *bufptr = '\0'; | |
339 | ||
340 | return (buf); | |
341 | } | |
342 | ||
343 | ||
344 | /* | |
345 | * 'collect_formats()' - Collect all of the format strings in the msgid. | |
346 | */ | |
347 | ||
348 | static cups_array_t * /* O - Array of format strings */ | |
349 | collect_formats(const char *id) /* I - msgid string */ | |
350 | { | |
351 | cups_array_t *fmts; /* Array of format strings */ | |
352 | char buf[255], /* Format string buffer */ | |
353 | *bufptr; /* Pointer into format string */ | |
354 | ||
355 | ||
356 | fmts = cupsArrayNew(NULL, NULL); | |
357 | ||
358 | while ((id = strchr(id, '%')) != NULL) | |
359 | { | |
360 | if (id[1] == '%') | |
361 | { | |
362 | /* | |
363 | * Skip %%... | |
364 | */ | |
365 | ||
366 | id += 2; | |
367 | continue; | |
368 | } | |
369 | ||
370 | for (bufptr = buf; *id && bufptr < (buf + sizeof(buf) - 1); id ++) | |
371 | { | |
372 | *bufptr++ = *id; | |
373 | ||
374 | if (strchr("CDEFGIOSUXcdeifgopsux", *id)) | |
375 | { | |
376 | id ++; | |
377 | break; | |
378 | } | |
379 | } | |
380 | ||
381 | *bufptr = '\0'; | |
382 | cupsArrayAdd(fmts, strdup(buf)); | |
383 | } | |
384 | ||
385 | return (fmts); | |
386 | } | |
387 | ||
388 | ||
389 | /* | |
390 | * 'free_formats()' - Free all of the format strings. | |
391 | */ | |
392 | ||
393 | static void | |
394 | free_formats(cups_array_t *fmts) /* I - Array of format strings */ | |
395 | { | |
396 | char *s; /* Current string */ | |
397 | ||
398 | ||
399 | for (s = (char *)cupsArrayFirst(fmts); s; s = (char *)cupsArrayNext(fmts)) | |
400 | free(s); | |
401 | ||
402 | cupsArrayDelete(fmts); | |
403 | } |