1/* GNU gettext - internationalization aids 2 Copyright (C) 1995-1998, 2000-2007 Free Software Foundation, Inc. 3 This file was written by Peter Miller <millerp@canb.auug.org.au> 4 5 This program is free software: you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published by 7 the Free Software Foundation; either version 3 of the License, or 8 (at your option) any later version. 9 10 This program is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with this program. If not, see <http://www.gnu.org/licenses/>. */ 17 18#ifdef HAVE_CONFIG_H 19# include <config.h> 20#endif 21 22#include <getopt.h> 23#include <limits.h> 24#include <stdbool.h> 25#include <stdio.h> 26#include <stdlib.h> 27#include <locale.h> 28 29#include "closeout.h" 30#include "dir-list.h" 31#include "error.h" 32#include "error-progname.h" 33#include "progname.h" 34#include "relocatable.h" 35#include "basename.h" 36#include "message.h" 37#include "read-catalog.h" 38#include "read-po.h" 39#include "read-properties.h" 40#include "read-stringtable.h" 41#include "msgl-iconv.h" 42#include "c-strstr.h" 43#include "c-strcase.h" 44#include "propername.h" 45#include "gettext.h" 46 47#define _(str) gettext (str) 48 49 50/* Apply the .pot file to each of the domains in the PO file. */ 51static bool multi_domain_mode = false; 52 53/* Whether to consider fuzzy messages as translations. */ 54static bool include_fuzzies = false; 55 56/* Whether to consider untranslated messages as translations. */ 57static bool include_untranslated = false; 58 59/* Long options. */ 60static const struct option long_options[] = 61{ 62 { "directory", required_argument, NULL, 'D' }, 63 { "help", no_argument, NULL, 'h' }, 64 { "multi-domain", no_argument, NULL, 'm' }, 65 { "properties-input", no_argument, NULL, 'P' }, 66 { "stringtable-input", no_argument, NULL, CHAR_MAX + 1 }, 67 { "use-fuzzy", no_argument, NULL, CHAR_MAX + 2 }, 68 { "use-untranslated", no_argument, NULL, CHAR_MAX + 3 }, 69 { "version", no_argument, NULL, 'V' }, 70 { NULL, 0, NULL, 0 } 71}; 72 73 74/* Forward declaration of local functions. */ 75static void usage (int status) 76#if defined __GNUC__ && ((__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || __GNUC__ > 2) 77 __attribute__ ((noreturn)) 78#endif 79; 80static void compare (const char *fn1, const char *fn2, 81 catalog_input_format_ty input_syntax); 82 83 84int 85main (int argc, char *argv[]) 86{ 87 int optchar; 88 bool do_help; 89 bool do_version; 90 catalog_input_format_ty input_syntax = &input_format_po; 91 92 /* Set program name for messages. */ 93 set_program_name (argv[0]); 94 error_print_progname = maybe_print_progname; 95 gram_max_allowed_errors = UINT_MAX; 96 97#ifdef HAVE_SETLOCALE 98 /* Set locale via LC_ALL. */ 99 setlocale (LC_ALL, ""); 100#endif 101 102 /* Set the text message domain. */ 103 bindtextdomain (PACKAGE, relocate (LOCALEDIR)); 104 bindtextdomain ("bison-runtime", relocate (BISON_LOCALEDIR)); 105 textdomain (PACKAGE); 106 107 /* Ensure that write errors on stdout are detected. */ 108 atexit (close_stdout); 109 110 do_help = false; 111 do_version = false; 112 while ((optchar = getopt_long (argc, argv, "D:hmPV", long_options, NULL)) 113 != EOF) 114 switch (optchar) 115 { 116 case '\0': /* long option */ 117 break; 118 119 case 'D': 120 dir_list_append (optarg); 121 break; 122 123 case 'h': 124 do_help = true; 125 break; 126 127 case 'm': 128 multi_domain_mode = true; 129 break; 130 131 case 'P': 132 input_syntax = &input_format_properties; 133 break; 134 135 case 'V': 136 do_version = true; 137 break; 138 139 case CHAR_MAX + 1: /* --stringtable-input */ 140 input_syntax = &input_format_stringtable; 141 break; 142 143 case CHAR_MAX + 2: /* --use-fuzzy */ 144 include_fuzzies = true; 145 break; 146 147 case CHAR_MAX + 3: /* --use-untranslated */ 148 include_untranslated = true; 149 break; 150 151 default: 152 usage (EXIT_FAILURE); 153 break; 154 } 155 156 /* Version information is requested. */ 157 if (do_version) 158 { 159 printf ("%s (GNU %s) %s\n", basename (program_name), PACKAGE, VERSION); 160 /* xgettext: no-wrap */ 161 printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\ 162License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n\ 163This is free software: you are free to change and redistribute it.\n\ 164There is NO WARRANTY, to the extent permitted by law.\n\ 165"), 166 "1995-1998, 2000-2007"); 167 printf (_("Written by %s.\n"), proper_name ("Peter Miller")); 168 exit (EXIT_SUCCESS); 169 } 170 171 /* Help is requested. */ 172 if (do_help) 173 usage (EXIT_SUCCESS); 174 175 /* Test whether we have an .po file name as argument. */ 176 if (optind >= argc) 177 { 178 error (EXIT_SUCCESS, 0, _("no input files given")); 179 usage (EXIT_FAILURE); 180 } 181 if (optind + 2 != argc) 182 { 183 error (EXIT_SUCCESS, 0, _("exactly 2 input files required")); 184 usage (EXIT_FAILURE); 185 } 186 187 /* compare the two files */ 188 compare (argv[optind], argv[optind + 1], input_syntax); 189 exit (EXIT_SUCCESS); 190} 191 192 193/* Display usage information and exit. */ 194static void 195usage (int status) 196{ 197 if (status != EXIT_SUCCESS) 198 fprintf (stderr, _("Try `%s --help' for more information.\n"), 199 program_name); 200 else 201 { 202 printf (_("\ 203Usage: %s [OPTION] def.po ref.pot\n\ 204"), program_name); 205 printf ("\n"); 206 /* xgettext: no-wrap */ 207 printf (_("\ 208Compare two Uniforum style .po files to check that both contain the same\n\ 209set of msgid strings. The def.po file is an existing PO file with the\n\ 210translations. The ref.pot file is the last created PO file, or a PO Template\n\ 211file (generally created by xgettext). This is useful for checking that\n\ 212you have translated each and every message in your program. Where an exact\n\ 213match cannot be found, fuzzy matching is used to produce better diagnostics.\n\ 214")); 215 printf ("\n"); 216 printf (_("\ 217Mandatory arguments to long options are mandatory for short options too.\n")); 218 printf ("\n"); 219 printf (_("\ 220Input file location:\n")); 221 printf (_("\ 222 def.po translations\n")); 223 printf (_("\ 224 ref.pot references to the sources\n")); 225 printf (_("\ 226 -D, --directory=DIRECTORY add DIRECTORY to list for input files search\n")); 227 printf ("\n"); 228 printf (_("\ 229Operation modifiers:\n")); 230 printf (_("\ 231 -m, --multi-domain apply ref.pot to each of the domains in def.po\n")); 232 printf (_("\ 233 --use-fuzzy consider fuzzy entries\n")); 234 printf (_("\ 235 --use-untranslated consider untranslated entries\n")); 236 printf ("\n"); 237 printf (_("\ 238Input file syntax:\n")); 239 printf (_("\ 240 -P, --properties-input input files are in Java .properties syntax\n")); 241 printf (_("\ 242 --stringtable-input input files are in NeXTstep/GNUstep .strings\n\ 243 syntax\n")); 244 printf ("\n"); 245 printf (_("\ 246Informative output:\n")); 247 printf (_("\ 248 -h, --help display this help and exit\n")); 249 printf (_("\ 250 -V, --version output version information and exit\n")); 251 printf ("\n"); 252 /* TRANSLATORS: The placeholder indicates the bug-reporting address 253 for this package. Please add _another line_ saying 254 "Report translation bugs to <...>\n" with the address for translation 255 bugs (typically your translation team's web or email address). */ 256 fputs (_("Report bugs to <bug-gnu-gettext@gnu.org>.\n"), stdout); 257 } 258 259 exit (status); 260} 261 262 263/* Return true if a message should be kept. */ 264static bool 265is_message_selected (const message_ty *mp) 266{ 267 /* Always keep the header entry. */ 268 if (is_header (mp)) 269 return true; 270 271 return !mp->obsolete; 272} 273 274 275/* Remove obsolete messages from a message list. Return the modified list. */ 276static msgdomain_list_ty * 277remove_obsoletes (msgdomain_list_ty *mdlp) 278{ 279 size_t k; 280 281 for (k = 0; k < mdlp->nitems; k++) 282 message_list_remove_if_not (mdlp->item[k]->messages, is_message_selected); 283 284 return mdlp; 285} 286 287 288static void 289match_domain (const char *fn1, const char *fn2, 290 message_list_ty *defmlp, message_list_ty *refmlp, 291 int *nerrors) 292{ 293 size_t j; 294 295 for (j = 0; j < refmlp->nitems; j++) 296 { 297 message_ty *refmsg; 298 message_ty *defmsg; 299 300 refmsg = refmlp->item[j]; 301 302 /* See if it is in the other file. */ 303 defmsg = message_list_search (defmlp, refmsg->msgctxt, refmsg->msgid); 304 if (defmsg) 305 { 306 if (!include_untranslated && defmsg->msgstr[0] == '\0') 307 { 308 (*nerrors)++; 309 po_gram_error_at_line (&defmsg->pos, _("\ 310this message is untranslated")); 311 } 312 else if (!include_fuzzies && defmsg->is_fuzzy && !is_header (defmsg)) 313 { 314 (*nerrors)++; 315 po_gram_error_at_line (&defmsg->pos, _("\ 316this message needs to be reviewed by the translator")); 317 } 318 else 319 defmsg->used = 1; 320 } 321 else 322 { 323 /* If the message was not defined at all, try to find a very 324 similar message, it could be a typo, or the suggestion may 325 help. */ 326 (*nerrors)++; 327 defmsg = 328 message_list_search_fuzzy (defmlp, refmsg->msgctxt, refmsg->msgid); 329 if (defmsg) 330 { 331 po_gram_error_at_line (&refmsg->pos, _("\ 332this message is used but not defined...")); 333 error_message_count--; 334 po_gram_error_at_line (&defmsg->pos, _("\ 335...but this definition is similar")); 336 defmsg->used = 1; 337 } 338 else 339 po_gram_error_at_line (&refmsg->pos, _("\ 340this message is used but not defined in %s"), fn1); 341 } 342 } 343} 344 345 346static void 347compare (const char *fn1, const char *fn2, catalog_input_format_ty input_syntax) 348{ 349 msgdomain_list_ty *def; 350 msgdomain_list_ty *ref; 351 int nerrors; 352 size_t j, k; 353 message_list_ty *empty_list; 354 355 /* This is the master file, created by a human. */ 356 def = remove_obsoletes (read_catalog_file (fn1, input_syntax)); 357 358 /* This is the generated file, created by groping the sources with 359 the xgettext program. */ 360 ref = remove_obsoletes (read_catalog_file (fn2, input_syntax)); 361 362 /* The references file can be either in ASCII or in UTF-8. If it is 363 in UTF-8, we have to convert the definitions to UTF-8 as well. */ 364 { 365 bool was_utf8 = false; 366 for (k = 0; k < ref->nitems; k++) 367 { 368 message_list_ty *mlp = ref->item[k]->messages; 369 370 for (j = 0; j < mlp->nitems; j++) 371 if (is_header (mlp->item[j]) /* && !mlp->item[j]->obsolete */) 372 { 373 const char *header = mlp->item[j]->msgstr; 374 375 if (header != NULL) 376 { 377 const char *charsetstr = c_strstr (header, "charset="); 378 379 if (charsetstr != NULL) 380 { 381 size_t len; 382 383 charsetstr += strlen ("charset="); 384 len = strcspn (charsetstr, " \t\n"); 385 if (len == strlen ("UTF-8") 386 && c_strncasecmp (charsetstr, "UTF-8", len) == 0) 387 was_utf8 = true; 388 } 389 } 390 } 391 } 392 if (was_utf8) 393 def = iconv_msgdomain_list (def, "UTF-8", true, fn1); 394 } 395 396 empty_list = message_list_alloc (false); 397 398 /* Every entry in the xgettext generated file must be matched by a 399 (single) entry in the human created file. */ 400 nerrors = 0; 401 if (!multi_domain_mode) 402 for (k = 0; k < ref->nitems; k++) 403 { 404 const char *domain = ref->item[k]->domain; 405 message_list_ty *refmlp = ref->item[k]->messages; 406 message_list_ty *defmlp; 407 408 defmlp = msgdomain_list_sublist (def, domain, false); 409 if (defmlp == NULL) 410 defmlp = empty_list; 411 412 match_domain (fn1, fn2, defmlp, refmlp, &nerrors); 413 } 414 else 415 { 416 /* Apply the references messages in the default domain to each of 417 the definition domains. */ 418 message_list_ty *refmlp = ref->item[0]->messages; 419 420 for (k = 0; k < def->nitems; k++) 421 { 422 message_list_ty *defmlp = def->item[k]->messages; 423 424 /* Ignore the default message domain if it has no messages. */ 425 if (k > 0 || defmlp->nitems > 0) 426 match_domain (fn1, fn2, defmlp, refmlp, &nerrors); 427 } 428 } 429 430 /* Look for messages in the definition file, which are not present 431 in the reference file, indicating messages which defined but not 432 used in the program. */ 433 for (k = 0; k < def->nitems; ++k) 434 { 435 message_list_ty *defmlp = def->item[k]->messages; 436 437 for (j = 0; j < defmlp->nitems; j++) 438 { 439 message_ty *defmsg = defmlp->item[j]; 440 441 if (!defmsg->used) 442 po_gram_error_at_line (&defmsg->pos, 443 _("warning: this message is not used")); 444 } 445 } 446 447 /* Exit with status 1 on any error. */ 448 if (nerrors > 0) 449 error (EXIT_FAILURE, 0, 450 ngettext ("found %d fatal error", "found %d fatal errors", nerrors), 451 nerrors); 452} 453