• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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