]>
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); | |
6d086e08 MS |
28 | static cups_array_t *cups_load_strings(const char *filename); |
29 | static int cups_read_strings(cups_file_t *fp, char *buffer, size_t bufsize, char **id, char **str); | |
30 | static char *cups_scan_strings(char *buffer); | |
c277e2f8 MS |
31 | static void free_formats(cups_array_t *fmts); |
32 | ||
33 | ||
34 | /* | |
6d086e08 | 35 | * 'main()' - Validate .po and .strings files. |
c277e2f8 MS |
36 | */ |
37 | ||
38 | int /* O - Exit code */ | |
39 | main(int argc, /* I - Number of command-line args */ | |
40 | char *argv[]) /* I - Command-line arguments */ | |
41 | { | |
42 | int i; /* Looping var */ | |
43 | cups_array_t *po; /* .po file */ | |
44 | _cups_message_t *msg; /* Current message */ | |
45 | cups_array_t *idfmts, /* Format strings in msgid */ | |
46 | *strfmts; /* Format strings in msgstr */ | |
47 | char *idfmt, /* Current msgid format string */ | |
48 | *strfmt; /* Current msgstr format string */ | |
91c84a35 | 49 | int fmtidx; /* Format index */ |
c277e2f8 MS |
50 | int status, /* Exit status */ |
51 | pass, /* Pass/fail status */ | |
52 | untranslated; /* Untranslated messages */ | |
53 | char idbuf[80], /* Abbreviated msgid */ | |
54 | strbuf[80]; /* Abbreviated msgstr */ | |
55 | ||
56 | ||
57 | if (argc < 2) | |
58 | { | |
6d086e08 | 59 | puts("Usage: checkpo filename.{po,strings} [... filenameN.{po,strings}]"); |
c277e2f8 MS |
60 | return (1); |
61 | } | |
62 | ||
63 | /* | |
6d086e08 | 64 | * Check every .po or .strings file on the command-line... |
c277e2f8 MS |
65 | */ |
66 | ||
67 | for (i = 1, status = 0; i < argc; i ++) | |
68 | { | |
69 | /* | |
70 | * Use the CUPS .po loader to get the message strings... | |
71 | */ | |
72 | ||
6d086e08 MS |
73 | if (strstr(argv[i], ".strings")) |
74 | po = cups_load_strings(argv[i]); | |
75 | else | |
76 | po = _cupsMessageLoad(argv[i], 1); | |
77 | ||
78 | if (!po) | |
c277e2f8 MS |
79 | { |
80 | perror(argv[i]); | |
81 | return (1); | |
82 | } | |
83 | ||
0837b7e8 MS |
84 | if (i > 1) |
85 | putchar('\n'); | |
c277e2f8 MS |
86 | printf("%s: ", argv[i]); |
87 | fflush(stdout); | |
88 | ||
89 | /* | |
90 | * Scan every message for a % string and then match them up with | |
91 | * the corresponding string in the translation... | |
92 | */ | |
93 | ||
94 | pass = 1; | |
95 | untranslated = 0; | |
96 | ||
97 | for (msg = (_cups_message_t *)cupsArrayFirst(po); | |
98 | msg; | |
99 | msg = (_cups_message_t *)cupsArrayNext(po)) | |
100 | { | |
0837b7e8 MS |
101 | /* |
102 | * Make sure filter message prefixes are not translated... | |
103 | */ | |
104 | ||
105 | if (!strncmp(msg->id, "ALERT:", 6) || !strncmp(msg->id, "CRIT:", 5) || | |
106 | !strncmp(msg->id, "DEBUG:", 6) || !strncmp(msg->id, "DEBUG2:", 7) || | |
107 | !strncmp(msg->id, "EMERG:", 6) || !strncmp(msg->id, "ERROR:", 6) || | |
108 | !strncmp(msg->id, "INFO:", 5) || !strncmp(msg->id, "NOTICE:", 7) || | |
109 | !strncmp(msg->id, "WARNING:", 8)) | |
110 | { | |
111 | if (pass) | |
112 | { | |
113 | pass = 0; | |
114 | puts("FAIL"); | |
115 | } | |
116 | ||
117 | printf(" Bad prefix on filter message \"%s\"\n", | |
118 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
119 | } | |
120 | ||
121 | idfmt = msg->id + strlen(msg->id) - 1; | |
122 | if (idfmt >= msg->id && *idfmt == '\n') | |
123 | { | |
124 | if (pass) | |
125 | { | |
126 | pass = 0; | |
127 | puts("FAIL"); | |
128 | } | |
129 | ||
130 | printf(" Trailing newline in message \"%s\"\n", | |
131 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
132 | } | |
133 | ||
134 | for (; idfmt >= msg->id; idfmt --) | |
135 | if (!isspace(*idfmt & 255)) | |
136 | break; | |
137 | ||
138 | if (idfmt >= msg->id && *idfmt == '!') | |
139 | { | |
140 | if (pass) | |
141 | { | |
142 | pass = 0; | |
143 | puts("FAIL"); | |
144 | } | |
145 | ||
146 | printf(" Exclamation in message \"%s\"\n", | |
147 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
148 | } | |
149 | ||
150 | if ((idfmt - 2) >= msg->id && !strncmp(idfmt - 2, "...", 3)) | |
151 | { | |
152 | if (pass) | |
153 | { | |
154 | pass = 0; | |
155 | puts("FAIL"); | |
156 | } | |
157 | ||
158 | printf(" Ellipsis in message \"%s\"\n", | |
159 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
160 | } | |
161 | ||
162 | ||
c277e2f8 MS |
163 | if (!msg->str || !msg->str[0]) |
164 | { | |
165 | untranslated ++; | |
166 | continue; | |
167 | } | |
168 | else if (strchr(msg->id, '%')) | |
169 | { | |
170 | idfmts = collect_formats(msg->id); | |
171 | strfmts = collect_formats(msg->str); | |
172 | fmtidx = 0; | |
173 | ||
174 | for (strfmt = (char *)cupsArrayFirst(strfmts); | |
175 | strfmt; | |
176 | strfmt = (char *)cupsArrayNext(strfmts)) | |
177 | { | |
178 | if (isdigit(strfmt[1] & 255) && strfmt[2] == '$') | |
179 | { | |
180 | /* | |
181 | * Handle positioned format stuff... | |
182 | */ | |
183 | ||
184 | fmtidx = strfmt[1] - '1'; | |
185 | strfmt += 3; | |
186 | if ((idfmt = (char *)cupsArrayIndex(idfmts, fmtidx)) != NULL) | |
187 | idfmt ++; | |
188 | } | |
189 | else | |
190 | { | |
191 | /* | |
192 | * Compare against the current format... | |
193 | */ | |
194 | ||
195 | idfmt = (char *)cupsArrayIndex(idfmts, fmtidx); | |
196 | } | |
197 | ||
198 | fmtidx ++; | |
199 | ||
200 | if (!idfmt || strcmp(strfmt, idfmt)) | |
201 | break; | |
c277e2f8 MS |
202 | } |
203 | ||
204 | if (cupsArrayCount(strfmts) != cupsArrayCount(idfmts) || strfmt) | |
205 | { | |
206 | if (pass) | |
207 | { | |
208 | pass = 0; | |
209 | puts("FAIL"); | |
210 | } | |
211 | ||
212 | printf(" Bad translation string \"%s\"\n for \"%s\"\n", | |
213 | abbreviate(msg->str, strbuf, sizeof(strbuf)), | |
214 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
215 | fputs(" Translation formats:", stdout); | |
216 | for (strfmt = (char *)cupsArrayFirst(strfmts); | |
217 | strfmt; | |
218 | strfmt = (char *)cupsArrayNext(strfmts)) | |
219 | printf(" %s", strfmt); | |
220 | fputs("\n Original formats:", stdout); | |
221 | for (idfmt = (char *)cupsArrayFirst(idfmts); | |
222 | idfmt; | |
223 | idfmt = (char *)cupsArrayNext(idfmts)) | |
224 | printf(" %s", idfmt); | |
225 | putchar('\n'); | |
f0ab5bff | 226 | putchar('\n'); |
c277e2f8 MS |
227 | } |
228 | ||
229 | free_formats(idfmts); | |
230 | free_formats(strfmts); | |
231 | } | |
232 | ||
8b116e60 MS |
233 | /* |
234 | * Only allow \\, \n, \r, \t, \", and \### character escapes... | |
235 | */ | |
236 | ||
237 | for (strfmt = msg->str; *strfmt; strfmt ++) | |
238 | if (*strfmt == '\\' && | |
239 | strfmt[1] != '\\' && strfmt[1] != 'n' && strfmt[1] != 'r' && | |
240 | strfmt[1] != 't' && strfmt[1] != '\"' && !isdigit(strfmt[1] & 255)) | |
241 | { | |
242 | if (pass) | |
243 | { | |
244 | pass = 0; | |
245 | puts("FAIL"); | |
246 | } | |
247 | ||
248 | printf(" Bad escape \\%c in filter message \"%s\"\n" | |
0837b7e8 | 249 | " for \"%s\"\n", strfmt[1], |
8b116e60 MS |
250 | abbreviate(msg->str, strbuf, sizeof(strbuf)), |
251 | abbreviate(msg->id, idbuf, sizeof(idbuf))); | |
252 | break; | |
253 | } | |
c277e2f8 MS |
254 | } |
255 | ||
256 | if (pass) | |
257 | { | |
0837b7e8 MS |
258 | if ((untranslated * 10) >= cupsArrayCount(po) && |
259 | strcmp(argv[i], "cups.pot")) | |
c277e2f8 MS |
260 | { |
261 | /* | |
262 | * Only allow 10% of messages to be untranslated before we fail... | |
263 | */ | |
264 | ||
265 | pass = 0; | |
266 | puts("FAIL"); | |
0837b7e8 | 267 | printf(" Too many untranslated messages (%d of %d)\n", |
f0ab5bff | 268 | untranslated, cupsArrayCount(po)); |
c277e2f8 MS |
269 | } |
270 | else if (untranslated > 0) | |
0837b7e8 | 271 | printf("PASS (%d of %d untranslated)\n", untranslated, |
c277e2f8 MS |
272 | cupsArrayCount(po)); |
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 | ||
6d086e08 MS |
389 | /* |
390 | * 'cups_load_strings()' - Load a .strings file into a _cups_msg_t array. | |
391 | */ | |
392 | ||
393 | static cups_array_t * /* O - CUPS array of _cups_msg_t values */ | |
394 | cups_load_strings(const char *filename) /* I - File to load */ | |
395 | { | |
396 | cups_file_t *fp; /* .strings file */ | |
397 | cups_array_t *po; /* Localization array */ | |
398 | _cups_message_t *m; /* Localization message */ | |
399 | char buffer[8192], /* Message buffer */ | |
400 | *id, /* ID string */ | |
401 | *str; /* Translated message */ | |
402 | ||
403 | ||
404 | if ((fp = cupsFileOpen(filename, "r")) == NULL) | |
405 | return (NULL); | |
406 | ||
407 | po = _cupsMessageNew(NULL); | |
408 | ||
409 | while (cups_read_strings(fp, buffer, sizeof(buffer), &id, &str)) | |
410 | { | |
411 | if ((m = malloc(sizeof(_cups_message_t))) == NULL) | |
412 | break; | |
413 | ||
414 | m->id = strdup(id); | |
415 | m->str = strdup(str); | |
416 | ||
417 | if (m->id && m->str) | |
418 | cupsArrayAdd(po, m); | |
419 | else | |
420 | { | |
421 | if (m->id) | |
422 | free(m->id); | |
423 | ||
424 | if (m->str) | |
425 | free(m->str); | |
426 | ||
427 | free(m); | |
428 | ||
429 | cupsArrayDelete(po); | |
430 | po = NULL; | |
431 | break; | |
432 | } | |
433 | } | |
434 | ||
435 | cupsFileClose(fp); | |
436 | ||
437 | return (po); | |
438 | } | |
439 | ||
440 | ||
441 | /* | |
442 | * 'cups_read_strings()' - Read a pair of strings from a .strings file. | |
443 | */ | |
444 | ||
445 | static int /* O - 1 on success, 0 on failure */ | |
446 | cups_read_strings(cups_file_t *strings, /* I - .strings file */ | |
447 | char *buffer, /* I - Line buffer */ | |
448 | size_t bufsize, /* I - Size of line buffer */ | |
449 | char **id, /* O - Pointer to ID string */ | |
450 | char **str) /* O - Pointer to translation string */ | |
451 | { | |
452 | char *bufptr; /* Pointer into buffer */ | |
453 | ||
454 | ||
455 | while (cupsFileGets(strings, buffer, bufsize)) | |
456 | { | |
457 | if (buffer[0] != '\"') | |
458 | continue; | |
459 | ||
460 | *id = buffer + 1; | |
461 | bufptr = cups_scan_strings(buffer); | |
462 | ||
463 | if (*bufptr != '\"') | |
464 | continue; | |
465 | ||
466 | *bufptr++ = '\0'; | |
467 | ||
468 | while (*bufptr && *bufptr != '\"') | |
469 | bufptr ++; | |
470 | ||
471 | if (!*bufptr) | |
472 | continue; | |
473 | ||
474 | *str = bufptr + 1; | |
475 | bufptr = cups_scan_strings(bufptr); | |
476 | ||
477 | if (*bufptr != '\"') | |
478 | continue; | |
479 | ||
480 | *bufptr = '\0'; | |
481 | ||
482 | return (1); | |
483 | } | |
484 | ||
485 | return (0); | |
486 | } | |
487 | ||
488 | ||
489 | /* | |
490 | * 'cups_scan_strings()' - Scan a quoted string. | |
491 | */ | |
492 | ||
493 | static char * /* O - End of string */ | |
494 | cups_scan_strings(char *buffer) /* I - Start of string */ | |
495 | { | |
496 | char *bufptr; /* Pointer into string */ | |
497 | ||
498 | ||
499 | for (bufptr = buffer + 1; *bufptr && *bufptr != '\"'; bufptr ++) | |
500 | { | |
501 | if (*bufptr == '\\') | |
502 | { | |
503 | if (bufptr[1] >= '0' && bufptr[1] <= '3' && | |
504 | bufptr[2] >= '0' && bufptr[2] <= '7' && | |
505 | bufptr[3] >= '0' && bufptr[3] <= '7') | |
506 | { | |
507 | /* | |
508 | * Decode \nnn octal escape... | |
509 | */ | |
510 | ||
511 | *bufptr = (char)(((((bufptr[1] - '0') << 3) | (bufptr[2] - '0')) << 3) | (bufptr[3] - '0')); | |
512 | _cups_strcpy(bufptr + 1, bufptr + 4); | |
513 | } | |
514 | else | |
515 | { | |
516 | /* | |
517 | * Decode \C escape... | |
518 | */ | |
519 | ||
520 | _cups_strcpy(bufptr, bufptr + 1); | |
521 | if (*bufptr == 'n') | |
522 | *bufptr = '\n'; | |
523 | else if (*bufptr == 'r') | |
524 | *bufptr = '\r'; | |
525 | else if (*bufptr == 't') | |
526 | *bufptr = '\t'; | |
527 | } | |
528 | } | |
529 | } | |
530 | ||
531 | return (bufptr); | |
532 | } | |
533 | ||
534 | ||
c277e2f8 MS |
535 | /* |
536 | * 'free_formats()' - Free all of the format strings. | |
537 | */ | |
538 | ||
539 | static void | |
540 | free_formats(cups_array_t *fmts) /* I - Array of format strings */ | |
541 | { | |
542 | char *s; /* Current string */ | |
543 | ||
544 | ||
545 | for (s = (char *)cupsArrayFirst(fmts); s; s = (char *)cupsArrayNext(fmts)) | |
546 | free(s); | |
547 | ||
548 | cupsArrayDelete(fmts); | |
549 | } |