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