/* * Verify that translations in the .po file have the same number and type of * printf-style format strings. * * Copyright © 2021-2023 by OpenPrinting. * Copyright © 2007-2017 by Apple Inc. * Copyright © 1997-2007 by Easy Software Products, all rights reserved. * * Licensed under Apache License v2.0. See the file "LICENSE" for more information. * * Usage: * * checkpo filename.{po,strings} [... filenameN.{po,strings}] * * Compile with: * * gcc -o checkpo checkpo.c `cups-config --libs` */ #include /* * Local functions... */ static char *abbreviate(const char *s, char *buf, int bufsize); static cups_array_t *collect_formats(const char *id); static void free_formats(cups_array_t *fmts); /* * 'main()' - Validate .po and .strings files. */ int /* O - Exit code */ main(int argc, /* I - Number of command-line args */ char *argv[]) /* I - Command-line arguments */ { int i; /* Looping var */ cups_array_t *po; /* .po file */ _cups_message_t *msg; /* Current message */ cups_array_t *idfmts, /* Format strings in msgid */ *strfmts; /* Format strings in msgstr */ char *idfmt, /* Current msgid format string */ *strfmt; /* Current msgstr format string */ int fmtidx; /* Format index */ int status, /* Exit status */ pass, /* Pass/fail status */ untranslated; /* Untranslated messages */ char idbuf[80], /* Abbreviated msgid */ strbuf[80]; /* Abbreviated msgstr */ if (argc < 2) { puts("Usage: checkpo filename.{po,strings} [... filenameN.{po,strings}]"); return (1); } /* * Check every .po or .strings file on the command-line... */ for (i = 1, status = 0; i < argc; i ++) { /* * Use the CUPS .po loader to get the message strings... */ if (strstr(argv[i], ".strings")) po = _cupsMessageLoad(argv[i], _CUPS_MESSAGE_STRINGS); else po = _cupsMessageLoad(argv[i], _CUPS_MESSAGE_PO | _CUPS_MESSAGE_EMPTY); if (!po) { perror(argv[i]); return (1); } if (i > 1) putchar('\n'); printf("%s: ", argv[i]); fflush(stdout); /* * Scan every message for a % string and then match them up with * the corresponding string in the translation... */ pass = 1; untranslated = 0; for (msg = (_cups_message_t *)cupsArrayFirst(po); msg; msg = (_cups_message_t *)cupsArrayNext(po)) { /* * Make sure filter message prefixes are not translated... */ if (!strncmp(msg->msg, "ALERT:", 6) || !strncmp(msg->msg, "CRIT:", 5) || !strncmp(msg->msg, "DEBUG:", 6) || !strncmp(msg->msg, "DEBUG2:", 7) || !strncmp(msg->msg, "EMERG:", 6) || !strncmp(msg->msg, "ERROR:", 6) || !strncmp(msg->msg, "INFO:", 5) || !strncmp(msg->msg, "NOTICE:", 7) || !strncmp(msg->msg, "WARNING:", 8)) { if (pass) { pass = 0; puts("FAIL"); } printf(" Bad prefix on filter message \"%s\"\n", abbreviate(msg->msg, idbuf, sizeof(idbuf))); } idfmt = msg->msg + strlen(msg->msg) - 1; if (idfmt >= msg->msg && *idfmt == '\n') { if (pass) { pass = 0; puts("FAIL"); } printf(" Trailing newline in message \"%s\"\n", abbreviate(msg->msg, idbuf, sizeof(idbuf))); } for (; idfmt >= msg->msg; idfmt --) if (!isspace(*idfmt & 255)) break; if (idfmt >= msg->msg && *idfmt == '!') { if (pass) { pass = 0; puts("FAIL"); } printf(" Exclamation in message \"%s\"\n", abbreviate(msg->msg, idbuf, sizeof(idbuf))); } if ((idfmt - 2) >= msg->msg && !strncmp(idfmt - 2, "...", 3)) { if (pass) { pass = 0; puts("FAIL"); } printf(" Ellipsis in message \"%s\"\n", abbreviate(msg->msg, idbuf, sizeof(idbuf))); } if (!msg->str || !msg->str[0]) { untranslated ++; continue; } else if (strchr(msg->msg, '%')) { idfmts = collect_formats(msg->msg); strfmts = collect_formats(msg->str); fmtidx = 0; for (strfmt = (char *)cupsArrayFirst(strfmts); strfmt; strfmt = (char *)cupsArrayNext(strfmts)) { if (isdigit(strfmt[1] & 255) && strfmt[2] == '$') { /* * Handle positioned format stuff... */ fmtidx = strfmt[1] - '1'; strfmt += 3; if ((idfmt = (char *)cupsArrayIndex(idfmts, fmtidx)) != NULL) idfmt ++; } else { /* * Compare against the current format... */ idfmt = (char *)cupsArrayIndex(idfmts, fmtidx); } fmtidx ++; if (!idfmt || strcmp(strfmt, idfmt)) break; } if (cupsArrayCount(strfmts) != cupsArrayCount(idfmts) || strfmt) { if (pass) { pass = 0; puts("FAIL"); } printf(" Bad translation string \"%s\"\n for \"%s\"\n", abbreviate(msg->str, strbuf, sizeof(strbuf)), abbreviate(msg->msg, idbuf, sizeof(idbuf))); fputs(" Translation formats:", stdout); for (strfmt = (char *)cupsArrayFirst(strfmts); strfmt; strfmt = (char *)cupsArrayNext(strfmts)) printf(" %s", strfmt); fputs("\n Original formats:", stdout); for (idfmt = (char *)cupsArrayFirst(idfmts); idfmt; idfmt = (char *)cupsArrayNext(idfmts)) printf(" %s", idfmt); putchar('\n'); putchar('\n'); } free_formats(idfmts); free_formats(strfmts); } /* * Only allow \\, \n, \r, \t, \", and \### character escapes... */ for (strfmt = msg->str; *strfmt; strfmt ++) { if (*strfmt == '\\') { strfmt ++; if (*strfmt != '\\' && *strfmt != 'n' && *strfmt != 'r' && *strfmt != 't' && *strfmt != '\"' && !isdigit(*strfmt & 255)) { if (pass) { pass = 0; puts("FAIL"); } printf(" Bad escape \\%c in filter message \"%s\"\n" " for \"%s\"\n", strfmt[1], abbreviate(msg->str, strbuf, sizeof(strbuf)), abbreviate(msg->msg, idbuf, sizeof(idbuf))); break; } } } } if (pass) { int count = cupsArrayCount(po); /* Total number of messages */ if (untranslated >= (count / 10) && strcmp(argv[i], "cups.pot")) { /* * Only allow 10% of messages to be untranslated before we fail... */ pass = 0; puts("FAIL"); printf(" Too many untranslated messages (%d of %d or %.1f%% are translated)\n", count - untranslated, count, 100.0 - 100.0 * untranslated / count); } else if (untranslated > 0) printf("PASS (%d of %d or %.1f%% are translated)\n", count - untranslated, count, 100.0 - 100.0 * untranslated / count); else puts("PASS"); } if (!pass) status = 1; _cupsMessageFree(po); } return (status); } /* * 'abbreviate()' - Abbreviate a message string as needed. */ static char * /* O - Abbreviated string */ abbreviate(const char *s, /* I - String to abbreviate */ char *buf, /* I - Buffer */ int bufsize) /* I - Size of buffer */ { char *bufptr; /* Pointer into buffer */ for (bufptr = buf, bufsize -= 4; *s && bufsize > 0; s ++) { if (*s == '\n') { if (bufsize < 2) break; *bufptr++ = '\\'; *bufptr++ = 'n'; bufsize -= 2; } else if (*s == '\t') { if (bufsize < 2) break; *bufptr++ = '\\'; *bufptr++ = 't'; bufsize -= 2; } else if (*s >= 0 && *s < ' ') { if (bufsize < 4) break; snprintf(bufptr, (size_t)bufsize, "\\%03o", *s); bufptr += 4; bufsize -= 4; } else { *bufptr++ = *s; bufsize --; } } if (*s) memcpy(bufptr, "...", 4); else *bufptr = '\0'; return (buf); } /* * 'collect_formats()' - Collect all of the format strings in the msgid. */ static cups_array_t * /* O - Array of format strings */ collect_formats(const char *id) /* I - msgid string */ { cups_array_t *fmts; /* Array of format strings */ char buf[255], /* Format string buffer */ *bufptr; /* Pointer into format string */ fmts = cupsArrayNew(NULL, NULL); while ((id = strchr(id, '%')) != NULL) { if (id[1] == '%') { /* * Skip %%... */ id += 2; continue; } for (bufptr = buf; *id && bufptr < (buf + sizeof(buf) - 1); id ++) { *bufptr++ = *id; if (strchr("CDEFGIOSUXcdeifgopsux", *id)) { id ++; break; } } *bufptr = '\0'; cupsArrayAdd(fmts, strdup(buf)); } return (fmts); } /* * 'free_formats()' - Free all of the format strings. */ static void free_formats(cups_array_t *fmts) /* I - Array of format strings */ { char *s; /* Current string */ for (s = (char *)cupsArrayFirst(fmts); s; s = (char *)cupsArrayNext(fmts)) free(s); cupsArrayDelete(fmts); }