]>
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 | * |
503b54c9 MS |
8 | * These coded instructions, statements, and computer programs are the |
9 | * property of Apple Inc. and are protected by Federal copyright | |
10 | * law. Distribution and use rights are outlined in the file "LICENSE.txt" | |
11 | * which should have been included with this file. If this file is | |
57b7b66b | 12 | * missing or damaged, see the license at "http://www.cups.org/". |
f0ab5bff | 13 | * |
c277e2f8 MS |
14 | * Usage: |
15 | * | |
6d086e08 | 16 | * checkpo filename.{po,strings} [... filenameN.{po,strings}] |
c277e2f8 MS |
17 | * |
18 | * Compile with: | |
19 | * | |
20 | * gcc -o checkpo checkpo.c `cups-config --libs` | |
c277e2f8 MS |
21 | */ |
22 | ||
71e16022 | 23 | #include <cups/cups-private.h> |
c277e2f8 MS |
24 | |
25 | ||
26 | /* | |
27 | * Local functions... | |
28 | */ | |
29 | ||
30 | static char *abbreviate(const char *s, char *buf, int bufsize); | |
31 | static cups_array_t *collect_formats(const char *id); | |
6d086e08 MS |
32 | static cups_array_t *cups_load_strings(const char *filename); |
33 | static int cups_read_strings(cups_file_t *fp, char *buffer, size_t bufsize, char **id, char **str); | |
34 | static char *cups_scan_strings(char *buffer); | |
c277e2f8 MS |
35 | static void free_formats(cups_array_t *fmts); |
36 | ||
37 | ||
38 | /* | |
6d086e08 | 39 | * 'main()' - Validate .po and .strings files. |
c277e2f8 MS |
40 | */ |
41 | ||
42 | int /* O - Exit code */ | |
43 | main(int argc, /* I - Number of command-line args */ | |
44 | char *argv[]) /* I - Command-line arguments */ | |
45 | { | |
46 | int i; /* Looping var */ | |
47 | cups_array_t *po; /* .po file */ | |
48 | _cups_message_t *msg; /* Current message */ | |
49 | cups_array_t *idfmts, /* Format strings in msgid */ | |
50 | *strfmts; /* Format strings in msgstr */ | |
51 | char *idfmt, /* Current msgid format string */ | |
52 | *strfmt; /* Current msgstr format string */ | |
91c84a35 | 53 | int fmtidx; /* Format index */ |
c277e2f8 MS |
54 | int status, /* Exit status */ |
55 | pass, /* Pass/fail status */ | |
56 | untranslated; /* Untranslated messages */ | |
57 | char idbuf[80], /* Abbreviated msgid */ | |
58 | strbuf[80]; /* Abbreviated msgstr */ | |
59 | ||
60 | ||
61 | if (argc < 2) | |
62 | { | |
6d086e08 | 63 | puts("Usage: checkpo filename.{po,strings} [... filenameN.{po,strings}]"); |
c277e2f8 MS |
64 | return (1); |
65 | } | |
66 | ||
67 | /* | |
6d086e08 | 68 | * Check every .po or .strings file on the command-line... |
c277e2f8 MS |
69 | */ |
70 | ||
71 | for (i = 1, status = 0; i < argc; i ++) | |
72 | { | |
73 | /* | |
74 | * Use the CUPS .po loader to get the message strings... | |
75 | */ | |
76 | ||
6d086e08 MS |
77 | if (strstr(argv[i], ".strings")) |
78 | po = cups_load_strings(argv[i]); | |
79 | else | |
80 | po = _cupsMessageLoad(argv[i], 1); | |
81 | ||
82 | if (!po) | |
c277e2f8 MS |
83 | { |
84 | perror(argv[i]); | |
85 | return (1); | |
86 | } | |
87 | ||
0837b7e8 MS |
88 | if (i > 1) |
89 | putchar('\n'); | |
c277e2f8 MS |
90 | printf("%s: ", argv[i]); |
91 | fflush(stdout); | |
92 | ||
93 | /* | |
94 | * Scan every message for a % string and then match them up with | |
95 | * the corresponding string in the translation... | |
96 | */ | |
97 | ||
98 | pass = 1; | |
99 | untranslated = 0; | |
100 | ||
101 | for (msg = (_cups_message_t *)cupsArrayFirst(po); | |
102 | msg; | |
103 | msg = (_cups_message_t *)cupsArrayNext(po)) | |
104 | { | |
0837b7e8 MS |
105 | /* |
106 | * Make sure filter message prefixes are not translated... | |
107 | */ | |
108 | ||
109 | if (!strncmp(msg->id, "ALERT:", 6) || !strncmp(msg->id, "CRIT:", 5) || | |
110 | !strncmp(msg->id, "DEBUG:", 6) || !strncmp(msg->id, "DEBUG2:", 7) || | |
111 | !strncmp(msg->id, "EMERG:", 6) || !strncmp(msg->id, "ERROR:", 6) || | |
112 | !strncmp(msg->id, "INFO:", 5) || !strncmp(msg->id, "NOTICE:", 7) || | |
113 | !strncmp(msg->id, "WARNING:", 8)) | |
114 | { | |
115 | if (pass) | |
116 | { | |
117 | pass = 0; | |
118 | puts("FAIL"); | |
119 | } | |
120 | ||
121 | printf(" Bad prefix on filter message \"%s\"\n", | |
122 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
123 | } | |
124 | ||
125 | idfmt = msg->id + strlen(msg->id) - 1; | |
126 | if (idfmt >= msg->id && *idfmt == '\n') | |
127 | { | |
128 | if (pass) | |
129 | { | |
130 | pass = 0; | |
131 | puts("FAIL"); | |
132 | } | |
133 | ||
134 | printf(" Trailing newline in message \"%s\"\n", | |
135 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
136 | } | |
137 | ||
138 | for (; idfmt >= msg->id; idfmt --) | |
139 | if (!isspace(*idfmt & 255)) | |
140 | break; | |
141 | ||
142 | if (idfmt >= msg->id && *idfmt == '!') | |
143 | { | |
144 | if (pass) | |
145 | { | |
146 | pass = 0; | |
147 | puts("FAIL"); | |
148 | } | |
149 | ||
150 | printf(" Exclamation in message \"%s\"\n", | |
151 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
152 | } | |
153 | ||
154 | if ((idfmt - 2) >= msg->id && !strncmp(idfmt - 2, "...", 3)) | |
155 | { | |
156 | if (pass) | |
157 | { | |
158 | pass = 0; | |
159 | puts("FAIL"); | |
160 | } | |
161 | ||
162 | printf(" Ellipsis in message \"%s\"\n", | |
163 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
164 | } | |
165 | ||
166 | ||
c277e2f8 MS |
167 | if (!msg->str || !msg->str[0]) |
168 | { | |
169 | untranslated ++; | |
170 | continue; | |
171 | } | |
172 | else if (strchr(msg->id, '%')) | |
173 | { | |
174 | idfmts = collect_formats(msg->id); | |
175 | strfmts = collect_formats(msg->str); | |
176 | fmtidx = 0; | |
177 | ||
178 | for (strfmt = (char *)cupsArrayFirst(strfmts); | |
179 | strfmt; | |
180 | strfmt = (char *)cupsArrayNext(strfmts)) | |
181 | { | |
182 | if (isdigit(strfmt[1] & 255) && strfmt[2] == '$') | |
183 | { | |
184 | /* | |
185 | * Handle positioned format stuff... | |
186 | */ | |
187 | ||
188 | fmtidx = strfmt[1] - '1'; | |
189 | strfmt += 3; | |
190 | if ((idfmt = (char *)cupsArrayIndex(idfmts, fmtidx)) != NULL) | |
191 | idfmt ++; | |
192 | } | |
193 | else | |
194 | { | |
195 | /* | |
196 | * Compare against the current format... | |
197 | */ | |
198 | ||
199 | idfmt = (char *)cupsArrayIndex(idfmts, fmtidx); | |
200 | } | |
201 | ||
202 | fmtidx ++; | |
203 | ||
204 | if (!idfmt || strcmp(strfmt, idfmt)) | |
205 | break; | |
c277e2f8 MS |
206 | } |
207 | ||
208 | if (cupsArrayCount(strfmts) != cupsArrayCount(idfmts) || strfmt) | |
209 | { | |
210 | if (pass) | |
211 | { | |
212 | pass = 0; | |
213 | puts("FAIL"); | |
214 | } | |
215 | ||
216 | printf(" Bad translation string \"%s\"\n for \"%s\"\n", | |
217 | abbreviate(msg->str, strbuf, sizeof(strbuf)), | |
218 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
219 | fputs(" Translation formats:", stdout); | |
220 | for (strfmt = (char *)cupsArrayFirst(strfmts); | |
221 | strfmt; | |
222 | strfmt = (char *)cupsArrayNext(strfmts)) | |
223 | printf(" %s", strfmt); | |
224 | fputs("\n Original formats:", stdout); | |
225 | for (idfmt = (char *)cupsArrayFirst(idfmts); | |
226 | idfmt; | |
227 | idfmt = (char *)cupsArrayNext(idfmts)) | |
228 | printf(" %s", idfmt); | |
229 | putchar('\n'); | |
f0ab5bff | 230 | putchar('\n'); |
c277e2f8 MS |
231 | } |
232 | ||
233 | free_formats(idfmts); | |
234 | free_formats(strfmts); | |
235 | } | |
236 | ||
8b116e60 MS |
237 | /* |
238 | * Only allow \\, \n, \r, \t, \", and \### character escapes... | |
239 | */ | |
240 | ||
241 | for (strfmt = msg->str; *strfmt; strfmt ++) | |
242 | if (*strfmt == '\\' && | |
243 | strfmt[1] != '\\' && strfmt[1] != 'n' && strfmt[1] != 'r' && | |
244 | strfmt[1] != 't' && strfmt[1] != '\"' && !isdigit(strfmt[1] & 255)) | |
245 | { | |
246 | if (pass) | |
247 | { | |
248 | pass = 0; | |
249 | puts("FAIL"); | |
250 | } | |
251 | ||
252 | printf(" Bad escape \\%c in filter message \"%s\"\n" | |
0837b7e8 | 253 | " for \"%s\"\n", strfmt[1], |
8b116e60 MS |
254 | abbreviate(msg->str, strbuf, sizeof(strbuf)), |
255 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
256 | break; | |
257 | } | |
c277e2f8 MS |
258 | } |
259 | ||
260 | if (pass) | |
261 | { | |
0837b7e8 MS |
262 | if ((untranslated * 10) >= cupsArrayCount(po) && |
263 | strcmp(argv[i], "cups.pot")) | |
c277e2f8 MS |
264 | { |
265 | /* | |
266 | * Only allow 10% of messages to be untranslated before we fail... | |
267 | */ | |
268 | ||
269 | pass = 0; | |
270 | puts("FAIL"); | |
0837b7e8 | 271 | printf(" Too many untranslated messages (%d of %d)\n", |
f0ab5bff | 272 | untranslated, cupsArrayCount(po)); |
c277e2f8 MS |
273 | } |
274 | else if (untranslated > 0) | |
0837b7e8 | 275 | printf("PASS (%d of %d untranslated)\n", untranslated, |
c277e2f8 MS |
276 | cupsArrayCount(po)); |
277 | else | |
0837b7e8 | 278 | puts("PASS"); |
c277e2f8 MS |
279 | } |
280 | ||
281 | if (!pass) | |
282 | status = 1; | |
283 | ||
284 | _cupsMessageFree(po); | |
285 | } | |
286 | ||
287 | return (status); | |
288 | } | |
289 | ||
290 | ||
291 | /* | |
292 | * 'abbreviate()' - Abbreviate a message string as needed. | |
293 | */ | |
294 | ||
295 | static char * /* O - Abbreviated string */ | |
296 | abbreviate(const char *s, /* I - String to abbreviate */ | |
297 | char *buf, /* I - Buffer */ | |
298 | int bufsize) /* I - Size of buffer */ | |
299 | { | |
300 | char *bufptr; /* Pointer into buffer */ | |
301 | ||
302 | ||
303 | for (bufptr = buf, bufsize -= 4; *s && bufsize > 0; s ++) | |
304 | { | |
305 | if (*s == '\n') | |
306 | { | |
307 | if (bufsize < 2) | |
308 | break; | |
309 | ||
310 | *bufptr++ = '\\'; | |
311 | *bufptr++ = 'n'; | |
312 | bufsize -= 2; | |
313 | } | |
314 | else if (*s == '\t') | |
315 | { | |
316 | if (bufsize < 2) | |
317 | break; | |
318 | ||
319 | *bufptr++ = '\\'; | |
320 | *bufptr++ = 't'; | |
321 | bufsize -= 2; | |
322 | } | |
323 | else if (*s >= 0 && *s < ' ') | |
324 | { | |
325 | if (bufsize < 4) | |
326 | break; | |
327 | ||
328 | sprintf(bufptr, "\\%03o", *s); | |
329 | bufptr += 4; | |
330 | bufsize -= 4; | |
331 | } | |
332 | else | |
333 | { | |
334 | *bufptr++ = *s; | |
335 | bufsize --; | |
336 | } | |
337 | } | |
338 | ||
339 | if (*s) | |
5a9febac | 340 | memcpy(bufptr, "...", 4); |
c277e2f8 MS |
341 | else |
342 | *bufptr = '\0'; | |
343 | ||
344 | return (buf); | |
345 | } | |
346 | ||
347 | ||
348 | /* | |
349 | * 'collect_formats()' - Collect all of the format strings in the msgid. | |
350 | */ | |
351 | ||
352 | static cups_array_t * /* O - Array of format strings */ | |
353 | collect_formats(const char *id) /* I - msgid string */ | |
354 | { | |
355 | cups_array_t *fmts; /* Array of format strings */ | |
356 | char buf[255], /* Format string buffer */ | |
357 | *bufptr; /* Pointer into format string */ | |
358 | ||
359 | ||
360 | fmts = cupsArrayNew(NULL, NULL); | |
361 | ||
362 | while ((id = strchr(id, '%')) != NULL) | |
363 | { | |
364 | if (id[1] == '%') | |
365 | { | |
366 | /* | |
367 | * Skip %%... | |
368 | */ | |
369 | ||
370 | id += 2; | |
371 | continue; | |
372 | } | |
373 | ||
374 | for (bufptr = buf; *id && bufptr < (buf + sizeof(buf) - 1); id ++) | |
375 | { | |
376 | *bufptr++ = *id; | |
377 | ||
378 | if (strchr("CDEFGIOSUXcdeifgopsux", *id)) | |
379 | { | |
380 | id ++; | |
381 | break; | |
382 | } | |
383 | } | |
384 | ||
385 | *bufptr = '\0'; | |
386 | cupsArrayAdd(fmts, strdup(buf)); | |
387 | } | |
388 | ||
389 | return (fmts); | |
390 | } | |
391 | ||
392 | ||
6d086e08 MS |
393 | /* |
394 | * 'cups_load_strings()' - Load a .strings file into a _cups_msg_t array. | |
395 | */ | |
396 | ||
397 | static cups_array_t * /* O - CUPS array of _cups_msg_t values */ | |
398 | cups_load_strings(const char *filename) /* I - File to load */ | |
399 | { | |
400 | cups_file_t *fp; /* .strings file */ | |
401 | cups_array_t *po; /* Localization array */ | |
402 | _cups_message_t *m; /* Localization message */ | |
403 | char buffer[8192], /* Message buffer */ | |
404 | *id, /* ID string */ | |
405 | *str; /* Translated message */ | |
406 | ||
407 | ||
408 | if ((fp = cupsFileOpen(filename, "r")) == NULL) | |
409 | return (NULL); | |
410 | ||
411 | po = _cupsMessageNew(NULL); | |
412 | ||
413 | while (cups_read_strings(fp, buffer, sizeof(buffer), &id, &str)) | |
414 | { | |
415 | if ((m = malloc(sizeof(_cups_message_t))) == NULL) | |
416 | break; | |
417 | ||
418 | m->id = strdup(id); | |
419 | m->str = strdup(str); | |
420 | ||
421 | if (m->id && m->str) | |
422 | cupsArrayAdd(po, m); | |
423 | else | |
424 | { | |
425 | if (m->id) | |
426 | free(m->id); | |
427 | ||
428 | if (m->str) | |
429 | free(m->str); | |
430 | ||
431 | free(m); | |
432 | ||
433 | cupsArrayDelete(po); | |
434 | po = NULL; | |
435 | break; | |
436 | } | |
437 | } | |
438 | ||
439 | cupsFileClose(fp); | |
440 | ||
441 | return (po); | |
442 | } | |
443 | ||
444 | ||
445 | /* | |
446 | * 'cups_read_strings()' - Read a pair of strings from a .strings file. | |
447 | */ | |
448 | ||
449 | static int /* O - 1 on success, 0 on failure */ | |
450 | cups_read_strings(cups_file_t *strings, /* I - .strings file */ | |
451 | char *buffer, /* I - Line buffer */ | |
452 | size_t bufsize, /* I - Size of line buffer */ | |
453 | char **id, /* O - Pointer to ID string */ | |
454 | char **str) /* O - Pointer to translation string */ | |
455 | { | |
456 | char *bufptr; /* Pointer into buffer */ | |
457 | ||
458 | ||
459 | while (cupsFileGets(strings, buffer, bufsize)) | |
460 | { | |
461 | if (buffer[0] != '\"') | |
462 | continue; | |
463 | ||
464 | *id = buffer + 1; | |
465 | bufptr = cups_scan_strings(buffer); | |
466 | ||
467 | if (*bufptr != '\"') | |
468 | continue; | |
469 | ||
470 | *bufptr++ = '\0'; | |
471 | ||
472 | while (*bufptr && *bufptr != '\"') | |
473 | bufptr ++; | |
474 | ||
475 | if (!*bufptr) | |
476 | continue; | |
477 | ||
478 | *str = bufptr + 1; | |
479 | bufptr = cups_scan_strings(bufptr); | |
480 | ||
481 | if (*bufptr != '\"') | |
482 | continue; | |
483 | ||
484 | *bufptr = '\0'; | |
485 | ||
486 | return (1); | |
487 | } | |
488 | ||
489 | return (0); | |
490 | } | |
491 | ||
492 | ||
493 | /* | |
494 | * 'cups_scan_strings()' - Scan a quoted string. | |
495 | */ | |
496 | ||
497 | static char * /* O - End of string */ | |
498 | cups_scan_strings(char *buffer) /* I - Start of string */ | |
499 | { | |
500 | char *bufptr; /* Pointer into string */ | |
501 | ||
502 | ||
503 | for (bufptr = buffer + 1; *bufptr && *bufptr != '\"'; bufptr ++) | |
504 | { | |
505 | if (*bufptr == '\\') | |
506 | { | |
507 | if (bufptr[1] >= '0' && bufptr[1] <= '3' && | |
508 | bufptr[2] >= '0' && bufptr[2] <= '7' && | |
509 | bufptr[3] >= '0' && bufptr[3] <= '7') | |
510 | { | |
511 | /* | |
512 | * Decode \nnn octal escape... | |
513 | */ | |
514 | ||
515 | *bufptr = (char)(((((bufptr[1] - '0') << 3) | (bufptr[2] - '0')) << 3) | (bufptr[3] - '0')); | |
516 | _cups_strcpy(bufptr + 1, bufptr + 4); | |
517 | } | |
518 | else | |
519 | { | |
520 | /* | |
521 | * Decode \C escape... | |
522 | */ | |
523 | ||
524 | _cups_strcpy(bufptr, bufptr + 1); | |
525 | if (*bufptr == 'n') | |
526 | *bufptr = '\n'; | |
527 | else if (*bufptr == 'r') | |
528 | *bufptr = '\r'; | |
529 | else if (*bufptr == 't') | |
530 | *bufptr = '\t'; | |
531 | } | |
532 | } | |
533 | } | |
534 | ||
535 | return (bufptr); | |
536 | } | |
537 | ||
538 | ||
c277e2f8 MS |
539 | /* |
540 | * 'free_formats()' - Free all of the format strings. | |
541 | */ | |
542 | ||
543 | static void | |
544 | free_formats(cups_array_t *fmts) /* I - Array of format strings */ | |
545 | { | |
546 | char *s; /* Current string */ | |
547 | ||
548 | ||
549 | for (s = (char *)cupsArrayFirst(fmts); s; s = (char *)cupsArrayNext(fmts)) | |
550 | free(s); | |
551 | ||
552 | cupsArrayDelete(fmts); | |
553 | } |