1 /* Manipulates attributes of messages in translation catalogs.
2 Copyright (C) 2001-2007, 2009-2010, 2012-2014, 2016, 2018-2023 Free Software Foundation, Inc.
3 Written by Bruno Haible <haible@clisp.cons.org>, 2001.
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 <https://www.gnu.org/licenses/>. */
17
18
19 #ifdef HAVE_CONFIG_H
20 # include "config.h"
21 #endif
22
23 #include <getopt.h>
24 #include <limits.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <locale.h>
28
29 #include <textstyle.h>
30
31 #include "noreturn.h"
32 #include "closeout.h"
33 #include "dir-list.h"
34 #include "error.h"
35 #include "error-progname.h"
36 #include "progname.h"
37 #include "relocatable.h"
38 #include "basename-lgpl.h"
39 #include "message.h"
40 #include "read-catalog.h"
41 #include "read-po.h"
42 #include "read-properties.h"
43 #include "read-stringtable.h"
44 #include "write-catalog.h"
45 #include "write-po.h"
46 #include "write-properties.h"
47 #include "write-stringtable.h"
48 #include "propername.h"
49 #include "xalloc.h"
50 #include "gettext.h"
51
52 #define _(str) gettext (str)
53
54
55 /* Force output of PO file even if empty. */
56 static int force_po;
57
58 /* Bit mask of subsets to remove. */
59 enum
60 {
61 REMOVE_UNTRANSLATED = 1 << 0,
62 REMOVE_TRANSLATED = 1 << 1,
63 REMOVE_FUZZY = 1 << 2,
64 REMOVE_NONFUZZY = 1 << 3,
65 REMOVE_OBSOLETE = 1 << 4,
66 REMOVE_NONOBSOLETE = 1 << 5
67 };
68 static int to_remove;
69
70 /* Bit mask of actions to perform on all messages. */
71 enum
72 {
73 SET_FUZZY = 1 << 0,
74 RESET_FUZZY = 1 << 1,
75 SET_OBSOLETE = 1 << 2,
76 RESET_OBSOLETE = 1 << 3,
77 REMOVE_PREV = 1 << 4,
78 ADD_PREV = 1 << 5,
79 REMOVE_TRANSLATION = 1 << 6
80 };
81 static int to_change;
82
83 /* Long options. */
84 static const struct option long_options[] =
85 {
86 { "add-location", optional_argument, NULL, 'n' },
87 { "clear-fuzzy", no_argument, NULL, CHAR_MAX + 8 },
88 { "clear-obsolete", no_argument, NULL, CHAR_MAX + 10 },
89 { "clear-previous", no_argument, NULL, CHAR_MAX + 18 },
90 { "empty", no_argument, NULL, CHAR_MAX + 23 },
91 { "color", optional_argument, NULL, CHAR_MAX + 19 },
92 { "directory", required_argument, NULL, 'D' },
93 { "escape", no_argument, NULL, 'E' },
94 { "force-po", no_argument, &force_po, 1 },
95 { "fuzzy", no_argument, NULL, CHAR_MAX + 11 },
96 { "help", no_argument, NULL, 'h' },
97 { "ignore-file", required_argument, NULL, CHAR_MAX + 15 },
98 { "indent", no_argument, NULL, 'i' },
99 { "no-escape", no_argument, NULL, 'e' },
100 { "no-fuzzy", no_argument, NULL, CHAR_MAX + 3 },
101 { "no-location", no_argument, NULL, CHAR_MAX + 22 },
102 { "no-obsolete", no_argument, NULL, CHAR_MAX + 5 },
103 { "no-wrap", no_argument, NULL, CHAR_MAX + 13 },
104 { "obsolete", no_argument, NULL, CHAR_MAX + 12 },
105 { "only-file", required_argument, NULL, CHAR_MAX + 14 },
106 { "only-fuzzy", no_argument, NULL, CHAR_MAX + 4 },
107 { "only-obsolete", no_argument, NULL, CHAR_MAX + 6 },
108 { "output-file", required_argument, NULL, 'o' },
109 { "previous", no_argument, NULL, CHAR_MAX + 21 },
110 { "properties-input", no_argument, NULL, 'P' },
111 { "properties-output", no_argument, NULL, 'p' },
112 { "set-fuzzy", no_argument, NULL, CHAR_MAX + 7 },
113 { "set-obsolete", no_argument, NULL, CHAR_MAX + 9 },
114 { "sort-by-file", no_argument, NULL, 'F' },
115 { "sort-output", no_argument, NULL, 's' },
116 { "stringtable-input", no_argument, NULL, CHAR_MAX + 16 },
117 { "stringtable-output", no_argument, NULL, CHAR_MAX + 17 },
118 { "strict", no_argument, NULL, 'S' },
119 { "style", required_argument, NULL, CHAR_MAX + 20 },
120 { "translated", no_argument, NULL, CHAR_MAX + 1 },
121 { "untranslated", no_argument, NULL, CHAR_MAX + 2 },
122 { "version", no_argument, NULL, 'V' },
123 { "width", required_argument, NULL, 'w' },
124 { NULL, 0, NULL, 0 }
125 };
126
127
128 /* Forward declaration of local functions. */
129 _GL_NORETURN_FUNC static void usage (int status);
130 static msgdomain_list_ty *process_msgdomain_list (msgdomain_list_ty *mdlp,
131 msgdomain_list_ty *only_mdlp,
132 msgdomain_list_ty *ignore_mdlp);
133
134
135 int
136 main (int argc, char **argv)
137 {
138 int optchar;
139 bool do_help;
140 bool do_version;
141 char *output_file;
142 const char *input_file;
143 const char *only_file;
144 const char *ignore_file;
145 msgdomain_list_ty *only_mdlp;
146 msgdomain_list_ty *ignore_mdlp;
147 msgdomain_list_ty *result;
148 catalog_input_format_ty input_syntax = &input_format_po;
149 catalog_output_format_ty output_syntax = &output_format_po;
150 bool sort_by_msgid = false;
151 bool sort_by_filepos = false;
152
153 /* Set program name for messages. */
154 set_program_name (argv[0]);
155 error_print_progname = maybe_print_progname;
156
157 /* Set locale via LC_ALL. */
158 setlocale (LC_ALL, "");
159
160 /* Set the text message domain. */
161 bindtextdomain (PACKAGE, relocate (LOCALEDIR));
162 bindtextdomain ("bison-runtime", relocate (BISON_LOCALEDIR));
163 textdomain (PACKAGE);
164
165 /* Ensure that write errors on stdout are detected. */
166 atexit (close_stdout);
167
168 /* Set default values for variables. */
169 do_help = false;
170 do_version = false;
171 output_file = NULL;
172 input_file = NULL;
173 only_file = NULL;
174 ignore_file = NULL;
175
176 while ((optchar = getopt_long (argc, argv, "D:eEFhino:pPsVw:", long_options,
177 NULL)) != EOF)
178 switch (optchar)
179 {
180 case '\0': /* Long option. */
181 break;
182
183 case 'D':
184 dir_list_append (optarg);
185 break;
186
187 case 'e':
188 message_print_style_escape (false);
189 break;
190
191 case 'E':
192 message_print_style_escape (true);
193 break;
194
195 case 'F':
196 sort_by_filepos = true;
197 break;
198
199 case 'h':
200 do_help = true;
201 break;
202
203 case 'i':
204 message_print_style_indent ();
205 break;
206
207 case 'n':
208 if (handle_filepos_comment_option (optarg))
209 usage (EXIT_FAILURE);
210 break;
211
212 case 'o':
213 output_file = optarg;
214 break;
215
216 case 'p':
217 output_syntax = &output_format_properties;
218 break;
219
220 case 'P':
221 input_syntax = &input_format_properties;
222 break;
223
224 case 's':
225 sort_by_msgid = true;
226 break;
227
228 case 'S':
229 message_print_style_uniforum ();
230 break;
231
232 case 'V':
233 do_version = true;
234 break;
235
236 case 'w':
237 {
238 int value;
239 char *endp;
240 value = strtol (optarg, &endp, 10);
241 if (endp != optarg)
242 message_page_width_set (value);
243 }
244 break;
245
246 case CHAR_MAX + 1: /* --translated */
247 to_remove |= REMOVE_UNTRANSLATED;
248 break;
249
250 case CHAR_MAX + 2: /* --untranslated */
251 to_remove |= REMOVE_TRANSLATED;
252 break;
253
254 case CHAR_MAX + 3: /* --no-fuzzy */
255 to_remove |= REMOVE_FUZZY;
256 break;
257
258 case CHAR_MAX + 4: /* --only-fuzzy */
259 to_remove |= REMOVE_NONFUZZY;
260 break;
261
262 case CHAR_MAX + 5: /* --no-obsolete */
263 to_remove |= REMOVE_OBSOLETE;
264 break;
265
266 case CHAR_MAX + 6: /* --only-obsolete */
267 to_remove |= REMOVE_NONOBSOLETE;
268 break;
269
270 case CHAR_MAX + 7: /* --set-fuzzy */
271 to_change |= SET_FUZZY;
272 break;
273
274 case CHAR_MAX + 8: /* --clear-fuzzy */
275 to_change |= RESET_FUZZY;
276 break;
277
278 case CHAR_MAX + 9: /* --set-obsolete */
279 to_change |= SET_OBSOLETE;
280 break;
281
282 case CHAR_MAX + 10: /* --clear-obsolete */
283 to_change |= RESET_OBSOLETE;
284 break;
285
286 case CHAR_MAX + 11: /* --fuzzy */
287 to_remove |= REMOVE_NONFUZZY;
288 to_change |= RESET_FUZZY;
289 break;
290
291 case CHAR_MAX + 12: /* --obsolete */
292 to_remove |= REMOVE_NONOBSOLETE;
293 to_change |= RESET_OBSOLETE;
294 break;
295
296 case CHAR_MAX + 13: /* --no-wrap */
297 message_page_width_ignore ();
298 break;
299
300 case CHAR_MAX + 14: /* --only-file */
301 only_file = optarg;
302 break;
303
304 case CHAR_MAX + 15: /* --ignore-file */
305 ignore_file = optarg;
306 break;
307
308 case CHAR_MAX + 16: /* --stringtable-input */
309 input_syntax = &input_format_stringtable;
310 break;
311
312 case CHAR_MAX + 17: /* --stringtable-output */
313 output_syntax = &output_format_stringtable;
314 break;
315
316 case CHAR_MAX + 18: /* --clear-previous */
317 to_change |= REMOVE_PREV;
318 break;
319
320 case CHAR_MAX + 19: /* --color */
321 if (handle_color_option (optarg) || color_test_mode)
322 usage (EXIT_FAILURE);
323 break;
324
325 case CHAR_MAX + 20: /* --style */
326 handle_style_option (optarg);
327 break;
328
329 case CHAR_MAX + 21: /* --previous */
330 to_change |= ADD_PREV;
331 break;
332
333 case CHAR_MAX + 22: /* --no-location */
334 message_print_style_filepos (filepos_comment_none);
335 break;
336
337 case CHAR_MAX + 23: /* --empty */
338 to_change |= REMOVE_TRANSLATION;
339 break;
340
341 default:
342 usage (EXIT_FAILURE);
343 /* NOTREACHED */
344 }
345
346 /* Version information requested. */
347 if (do_version)
348 {
349 printf ("%s (GNU %s) %s\n", last_component (program_name),
350 PACKAGE, VERSION);
351 /* xgettext: no-wrap */
352 printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\
353 License GPLv3+: GNU GPL version 3 or later <%s>\n\
354 This is free software: you are free to change and redistribute it.\n\
355 There is NO WARRANTY, to the extent permitted by law.\n\
356 "),
357 "2001-2023", "https://gnu.org/licenses/gpl.html");
358 printf (_("Written by %s.\n"), proper_name ("Bruno Haible"));
359 exit (EXIT_SUCCESS);
360 }
361
362 /* Help is requested. */
363 if (do_help)
364 usage (EXIT_SUCCESS);
365
366 /* Test whether we have an .po file name as argument. */
367 if (optind == argc)
368 input_file = "-";
369 else if (optind + 1 == argc)
370 input_file = argv[optind];
371 else
372 {
373 error (EXIT_SUCCESS, 0, _("at most one input file allowed"));
374 usage (EXIT_FAILURE);
375 }
376
377 /* Verify selected options. */
378 if (sort_by_msgid && sort_by_filepos)
379 error (EXIT_FAILURE, 0, _("%s and %s are mutually exclusive"),
380 "--sort-output", "--sort-by-file");
381
382 /* Read input file. */
383 result = read_catalog_file (input_file, input_syntax);
384
385 /* Read optional files that limit the extent of the attribute changes. */
386 only_mdlp = (only_file != NULL
387 ? read_catalog_file (only_file, input_syntax)
388 : NULL);
389 ignore_mdlp = (ignore_file != NULL
390 ? read_catalog_file (ignore_file, input_syntax)
391 : NULL);
392
393 /* Filter the messages and manipulate the attributes. */
394 result = process_msgdomain_list (result, only_mdlp, ignore_mdlp);
395
396 /* Sorting the list of messages. */
397 if (sort_by_filepos)
398 msgdomain_list_sort_by_filepos (result);
399 else if (sort_by_msgid)
400 msgdomain_list_sort_by_msgid (result);
401
402 /* Write the PO file. */
403 msgdomain_list_print (result, output_file, output_syntax, force_po, false);
404
405 exit (EXIT_SUCCESS);
406 }
407
408
409 /* Display usage information and exit. */
410 static void
411 usage (int status)
412 {
413 if (status != EXIT_SUCCESS)
414 fprintf (stderr, _("Try '%s --help' for more information.\n"),
415 program_name);
416 else
417 {
418 printf (_("\
419 Usage: %s [OPTION] [INPUTFILE]\n\
420 "), program_name);
421 printf ("\n");
422 /* xgettext: no-wrap */
423 printf (_("\
424 Filters the messages of a translation catalog according to their attributes,\n\
425 and manipulates the attributes.\n"));
426 printf ("\n");
427 printf (_("\
428 Mandatory arguments to long options are mandatory for short options too.\n"));
429 printf ("\n");
430 printf (_("\
431 Input file location:\n"));
432 printf (_("\
433 INPUTFILE input PO file\n"));
434 printf (_("\
435 -D, --directory=DIRECTORY add DIRECTORY to list for input files search\n"));
436 printf (_("\
437 If no input file is given or if it is -, standard input is read.\n"));
438 printf ("\n");
439 printf (_("\
440 Output file location:\n"));
441 printf (_("\
442 -o, --output-file=FILE write output to specified file\n"));
443 printf (_("\
444 The results are written to standard output if no output file is specified\n\
445 or if it is -.\n"));
446 printf ("\n");
447 printf (_("\
448 Message selection:\n"));
449 printf (_("\
450 --translated keep translated, remove untranslated messages\n"));
451 printf (_("\
452 --untranslated keep untranslated, remove translated messages\n"));
453 printf (_("\
454 --no-fuzzy remove 'fuzzy' marked messages\n"));
455 printf (_("\
456 --only-fuzzy keep 'fuzzy' marked messages\n"));
457 printf (_("\
458 --no-obsolete remove obsolete #~ messages\n"));
459 printf (_("\
460 --only-obsolete keep obsolete #~ messages\n"));
461 printf ("\n");
462 printf (_("\
463 Attribute manipulation:\n"));
464 printf (_("\
465 --set-fuzzy set all messages 'fuzzy'\n"));
466 printf (_("\
467 --clear-fuzzy set all messages non-'fuzzy'\n"));
468 printf (_("\
469 --set-obsolete set all messages obsolete\n"));
470 printf (_("\
471 --clear-obsolete set all messages non-obsolete\n"));
472 printf (_("\
473 --previous when setting 'fuzzy', keep previous msgids\n\
474 of translated messages.\n"));
475 printf (_("\
476 --clear-previous remove the \"previous msgid\" from all messages\n"));
477 printf (_("\
478 --empty when removing 'fuzzy', also set msgstr empty\n"));
479 printf (_("\
480 --only-file=FILE.po manipulate only entries listed in FILE.po\n"));
481 printf (_("\
482 --ignore-file=FILE.po manipulate only entries not listed in FILE.po\n"));
483 printf (_("\
484 --fuzzy synonym for --only-fuzzy --clear-fuzzy\n"));
485 printf (_("\
486 --obsolete synonym for --only-obsolete --clear-obsolete\n"));
487 printf ("\n");
488 printf (_("\
489 Input file syntax:\n"));
490 printf (_("\
491 -P, --properties-input input file is in Java .properties syntax\n"));
492 printf (_("\
493 --stringtable-input input file is in NeXTstep/GNUstep .strings syntax\n"));
494 printf ("\n");
495 printf (_("\
496 Output details:\n"));
497 printf (_("\
498 --color use colors and other text attributes always\n\
499 --color=WHEN use colors and other text attributes if WHEN.\n\
500 WHEN may be 'always', 'never', 'auto', or 'html'.\n"));
501 printf (_("\
502 --style=STYLEFILE specify CSS style rule file for --color\n"));
503 printf (_("\
504 -e, --no-escape do not use C escapes in output (default)\n"));
505 printf (_("\
506 -E, --escape use C escapes in output, no extended chars\n"));
507 printf (_("\
508 --force-po write PO file even if empty\n"));
509 printf (_("\
510 -i, --indent write the .po file using indented style\n"));
511 printf (_("\
512 --no-location do not write '#: filename:line' lines\n"));
513 printf (_("\
514 -n, --add-location generate '#: filename:line' lines (default)\n"));
515 printf (_("\
516 --strict write out strict Uniforum conforming .po file\n"));
517 printf (_("\
518 -p, --properties-output write out a Java .properties file\n"));
519 printf (_("\
520 --stringtable-output write out a NeXTstep/GNUstep .strings file\n"));
521 printf (_("\
522 -w, --width=NUMBER set output page width\n"));
523 printf (_("\
524 --no-wrap do not break long message lines, longer than\n\
525 the output page width, into several lines\n"));
526 printf (_("\
527 -s, --sort-output generate sorted output\n"));
528 printf (_("\
529 -F, --sort-by-file sort output by file location\n"));
530 printf ("\n");
531 printf (_("\
532 Informative output:\n"));
533 printf (_("\
534 -h, --help display this help and exit\n"));
535 printf (_("\
536 -V, --version output version information and exit\n"));
537 printf ("\n");
538 /* TRANSLATORS: The first placeholder is the web address of the Savannah
539 project of this package. The second placeholder is the bug-reporting
540 email address for this package. Please add _another line_ saying
541 "Report translation bugs to <...>\n" with the address for translation
542 bugs (typically your translation team's web or email address). */
543 printf(_("\
544 Report bugs in the bug tracker at <%s>\n\
545 or by email to <%s>.\n"),
546 "https://savannah.gnu.org/projects/gettext",
547 "bug-gettext@gnu.org");
548 }
549
550 exit (status);
551 }
552
553
554 /* Return true if a message should be kept. */
555 static bool
556 is_message_selected (const message_ty *mp)
557 {
558 /* Always keep the header entry. */
559 if (is_header (mp))
560 return true;
561
562 if ((to_remove & (REMOVE_UNTRANSLATED | REMOVE_TRANSLATED))
563 && (mp->msgstr[0] == '\0'
564 ? to_remove & REMOVE_UNTRANSLATED
565 : to_remove & REMOVE_TRANSLATED))
566 return false;
567
568 if ((to_remove & (REMOVE_FUZZY | REMOVE_NONFUZZY))
569 && (mp->is_fuzzy
570 ? to_remove & REMOVE_FUZZY
571 : to_remove & REMOVE_NONFUZZY))
572 return false;
573
574 if ((to_remove & (REMOVE_OBSOLETE | REMOVE_NONOBSOLETE))
575 && (mp->obsolete
576 ? to_remove & REMOVE_OBSOLETE
577 : to_remove & REMOVE_NONOBSOLETE))
578 return false;
579
580 return true;
581 }
582
583
584 static void
585 process_message_list (message_list_ty *mlp,
586 message_list_ty *only_mlp, message_list_ty *ignore_mlp)
587 {
588 /* Keep only the selected messages. */
589 message_list_remove_if_not (mlp, is_message_selected);
590
591 /* Change the attributes. */
592 if (to_change)
593 {
594 size_t j;
595
596 for (j = 0; j < mlp->nitems; j++)
597 {
598 message_ty *mp = mlp->item[j];
599
600 /* Attribute changes only affect messages listed in --only-file
601 and not listed in --ignore-file. */
602 if ((only_mlp
603 ? message_list_search (only_mlp, mp->msgctxt, mp->msgid) != NULL
604 : true)
605 && (ignore_mlp
606 ? message_list_search (ignore_mlp, mp->msgctxt, mp->msgid) == NULL
607 : true))
608 {
609 if (to_change & SET_FUZZY)
610 {
611 if ((to_change & ADD_PREV) && !is_header (mp)
612 && !mp->is_fuzzy && mp->msgstr[0] != '\0')
613 {
614 mp->prev_msgctxt =
615 (mp->msgctxt != NULL ? xstrdup (mp->msgctxt) : NULL);
616 mp->prev_msgid =
617 (mp->msgid != NULL ? xstrdup (mp->msgid) : NULL);
618 mp->prev_msgid_plural =
619 (mp->msgid_plural != NULL
620 ? xstrdup (mp->msgid_plural)
621 : NULL);
622 }
623 mp->is_fuzzy = true;
624 }
625
626 if (to_change & RESET_FUZZY)
627 {
628 if ((to_change & REMOVE_TRANSLATION)
629 && mp->is_fuzzy && !mp->obsolete)
630 {
631 unsigned long int nplurals = 0;
632 char *msgstr;
633 size_t pos;
634
635 for (pos = 0; pos < mp->msgstr_len; ++pos)
636 if (!mp->msgstr[pos])
637 ++nplurals;
638 free ((char *) mp->msgstr);
639 msgstr = XNMALLOC (nplurals, char);
640 memset (msgstr, '\0', nplurals);
641 mp->msgstr = msgstr;
642 mp->msgstr_len = nplurals;
643 }
644 mp->is_fuzzy = false;
645 }
646 /* Always keep the header entry non-obsolete. */
647 if ((to_change & SET_OBSOLETE) && !is_header (mp))
648 mp->obsolete = true;
649 if (to_change & RESET_OBSOLETE)
650 mp->obsolete = false;
651 if (to_change & REMOVE_PREV)
652 {
653 mp->prev_msgctxt = NULL;
654 mp->prev_msgid = NULL;
655 mp->prev_msgid_plural = NULL;
656 }
657 }
658 }
659 }
660 }
661
662
663 static msgdomain_list_ty *
664 process_msgdomain_list (msgdomain_list_ty *mdlp,
665 msgdomain_list_ty *only_mdlp,
666 msgdomain_list_ty *ignore_mdlp)
667 {
668 size_t k;
669
670 for (k = 0; k < mdlp->nitems; k++)
671 process_message_list (mdlp->item[k]->messages,
672 only_mdlp
673 ? msgdomain_list_sublist (only_mdlp,
674 mdlp->item[k]->domain,
675 true)
676 : NULL,
677 ignore_mdlp
678 ? msgdomain_list_sublist (ignore_mdlp,
679 mdlp->item[k]->domain,
680 false)
681 : NULL);
682
683 return mdlp;
684 }