/* Converts Uniforum style .po files to binary .mo files
   Copyright (C) 1995-2025 Free Software Foundation, Inc.
   Written by Ulrich Drepper <drepper@gnu.ai.mit.edu>, April 1995.

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>

#include <error.h>
#include "options.h"
#include "noreturn.h"
#include "closeout.h"
#include "str-list.h"
#include "dir-list.h"
#include "error-progname.h"
#include "progname.h"
#include "relocatable.h"
#include "basename-lgpl.h"
#include "xerror.h"
#include "xerror-handler.h"
#include "xvasprintf.h"
#include "xalloc.h"
#include "msgfmt.h"
#include "write-mo.h"
#include "write-java.h"
#include "write-csharp.h"
#include "write-resources.h"
#include "write-tcl.h"
#include "write-qt.h"
#include "write-desktop.h"
#include "write-xml.h"
#include "propername.h"
#include "message.h"
#include "open-catalog.h"
#include "read-catalog-file.h"
#include "read-po.h"
#include "read-properties.h"
#include "read-stringtable.h"
#include "read-desktop.h"
#include "po-xerror.h"
#include "xerror-handler.h"
#include "po-charset.h"
#include "msgl-check.h"
#include "msgl-iconv.h"
#include "concat-filename.h"
#include "its.h"
#include "locating-rules.h"
#include "search-path.h"
#include "gettext.h"

#define _(str) gettext (str)

/* Contains exit status for case in which no premature exit occurs.  */
static int exit_status;

/* If true include even fuzzy translations in output file.  */
static bool include_fuzzies = false;

/* If true include even untranslated messages in output file.  */
static bool include_untranslated = false;

/* Specifies name of the output file.  */
static const char *output_file_name;

/* Java mode output file specification.  */
static bool java_mode;
static bool assume_java2;
static const char *java_resource_name;
static const char *java_locale_name;
static const char *java_class_directory;
static bool java_output_source;

/* C# mode output file specification.  */
static bool csharp_mode;
static const char *csharp_resource_name;
static const char *csharp_locale_name;
static const char *csharp_base_directory;

/* C# resources mode output file specification.  */
static bool csharp_resources_mode;

/* Tcl mode output file specification.  */
static bool tcl_mode;
static const char *tcl_locale_name;
static const char *tcl_base_directory;

/* Qt mode output file specification.  */
static bool qt_mode;

/* Desktop Entry mode output file specification.  */
static bool desktop_mode;
static const char *desktop_locale_name;
static const char *desktop_template_name;
static const char *desktop_base_directory;
static hash_table desktop_keywords;
static bool desktop_default_keywords = true;

/* XML mode output file specification.  */
static bool xml_mode;
static bool xml_replace_text;
static const char *xml_locale_name;
static const char *xml_template_name;
static const char *xml_base_directory;
static const char *xml_language;
static its_rule_list_ty *xml_its_rules;

/* We may have more than one input file.  Domains with same names in
   different files have to merged.  So we need a list of tables for
   each output file.  */
struct msg_domain
{
  /* List for mapping message IDs to message strings.  */
  message_list_ty *mlp;
  /* Name of domain these ID/String pairs are part of.  */
  const char *domain_name;
  /* Output file name.  */
  const char *file_name;
  /* Link to the next domain.  */
  struct msg_domain *next;
};
static struct msg_domain *domain_list;
static struct msg_domain *current_domain;

/* Be more verbose.  Use only 'fprintf' and 'multiline_warning' but not
   'error' or 'multiline_error' to emit verbosity messages, because 'error'
   and 'multiline_error' during PO file parsing cause the program to exit
   with EXIT_FAILURE.  See function lex_end().  */
int verbose = 0;

/* If true check strings according to format string rules for the
   language.  */
static bool check_format_strings = false;

/* If true check the header entry is present and complete.  */
static bool check_header = false;

/* Check that domain directives can be satisfied.  */
static bool check_domain = false;

/* Check that msgfmt's behaviour is semantically compatible with
   X/Open msgfmt or XView msgfmt.  */
static bool check_compatibility = false;

/* If true, consider that strings containing an '&' are menu items and
   the '&' designates a keyboard accelerator, and verify that the translations
   also have a keyboard accelerator.  */
static bool check_accelerators = false;
static char accelerator_char = '&';

/* Counters for statistics on translations for the processed files.  */
static int msgs_translated;
static int msgs_untranslated;
static int msgs_fuzzy;

/* If not zero print statistics about translation at the end.  */
static int do_statistics;


/* Forward declaration of local functions.  */
_GL_NORETURN_FUNC static void usage (int status);
static const char *add_mo_suffix (const char *);
static struct msg_domain *new_domain (const char *name, const char *file_name);
static bool is_nonobsolete (const message_ty *mp);
static void read_catalog_file_msgfmt (char *filename,
                                      catalog_input_format_ty input_syntax);
static int msgfmt_desktop_bulk (const char *directory,
                                const char *template_file_name,
                                hash_table *keywords,
                                const char *file_name);
static int msgfmt_xml_bulk (const char *directory,
                            const char *template_file_name,
                            its_rule_list_ty *its_rules,
                            const char *file_name);


int
main (int argc, char *argv[])
{
  bool do_help = false;
  bool do_version = false;
  bool strict_uniforum = false;
  catalog_input_format_ty input_syntax = &input_format_po;
  int arg_i;
  const char *canon_encoding;
  struct msg_domain *domain;

  /* Set default value for global variables.  */
  alignment = DEFAULT_OUTPUT_ALIGNMENT;
  byteswap = 0 ^ ENDIANNESS;

  /* Set program name for messages.  */
  set_program_name (argv[0]);
  error_print_progname = maybe_print_progname;
  error_one_per_line = 1;
  exit_status = EXIT_SUCCESS;
  gram_max_allowed_errors = 20;

  /* Set locale via LC_ALL.  */
  setlocale (LC_ALL, "");

  /* Set the text message domain.  */
  bindtextdomain (PACKAGE, relocate (LOCALEDIR));
  bindtextdomain ("gnulib", relocate (GNULIB_LOCALEDIR));
  bindtextdomain ("bison-runtime", relocate (BISON_LOCALEDIR));
  textdomain (PACKAGE);

  /* Ensure that write errors on stdout are detected.  */
  atexit (close_stdout);

  /* Parse command line options.  */
  BEGIN_ALLOW_OMITTING_FIELD_INITIALIZERS
  static const struct program_option options[] =
  {
    { "alignment",           'a',           required_argument },
    { "check",               'c',           no_argument       },
    { "check-accelerators",  CHAR_MAX + 1,  optional_argument },
    { "check-compatibility", 'C',           no_argument       },
    { "check-domain",        CHAR_MAX + 2,  no_argument       },
    { "check-format",        CHAR_MAX + 3,  no_argument       },
    { "check-header",        CHAR_MAX + 4,  no_argument       },
    { "csharp",              CHAR_MAX + 10, no_argument       },
    { "csharp-resources",    CHAR_MAX + 11, no_argument       },
    { "desktop",             CHAR_MAX + 15, no_argument       },
    { "directory",           'D',           required_argument },
    { "endianness",          CHAR_MAX + 13, required_argument },
    { "help",                'h',           no_argument       },
    { "java",                'j',           no_argument       },
    { "java2",               CHAR_MAX + 5,  no_argument       },
    { "keyword",             'k',           optional_argument },
    { "language",            'L',           required_argument },
    { "locale",              'l',           required_argument },
    { "no-convert",          CHAR_MAX + 17, no_argument       },
    { "no-hash",             CHAR_MAX + 6,  no_argument       },
    { "no-redundancy",       CHAR_MAX + 18, no_argument       },
    { "output-file",         'o',           required_argument },
    { "properties-input",    'P',           no_argument       },
    { "qt",                  CHAR_MAX + 9,  no_argument       },
    { "replace-text",        CHAR_MAX + 19, no_argument       },
    { "resource",            'r',           required_argument },
    { "source",              CHAR_MAX + 14, no_argument       },
    { "statistics",          0,             no_argument,      &do_statistics, 1 },
    { "strict",              CHAR_MAX + 20, no_argument       },
    { "stringtable-input",   CHAR_MAX + 8,  no_argument       },
    { "tcl",                 CHAR_MAX + 7,  no_argument       },
    { "template",            CHAR_MAX + 16, required_argument },
    { "use-fuzzy",           'f',           no_argument       },
    { "use-untranslated",    CHAR_MAX + 12, no_argument       },
    { "verbose",             'v',           no_argument       },
    { "version",             'V',           no_argument       },
    { "xml",                 'x',           no_argument       },
    { NULL,                  'd',           required_argument },
  };
  END_ALLOW_OMITTING_FIELD_INITIALIZERS
  start_options (argc, argv, options, MOVE_OPTIONS_FIRST, 0);
  int opt;
  while ((opt = get_next_option ()) != -1)
    switch (opt)
      {
      case '\0':                /* Long option with key == 0.  */
        break;
      case 'a':
        if (isdigit ((unsigned char) optarg[0]))
          {
            char *endp;
            size_t new_align = strtoul (optarg, &endp, 0);

            if (endp != optarg)
              /* Check whether new_align is a power of 2.  */
              if (new_align > 0 && (new_align & (new_align - 1)) == 0)
                alignment = new_align;
          }
        break;
      case 'c':
        check_domain = true;
        check_format_strings = true;
        check_header = true;
        break;
      case 'C':
        check_compatibility = true;
        break;
      case 'd':
        java_class_directory = optarg;
        csharp_base_directory = optarg;
        tcl_base_directory = optarg;
        desktop_base_directory = optarg;
        xml_base_directory = optarg;
        break;
      case 'D':
        dir_list_append (optarg);
        break;
      case 'f':
        include_fuzzies = true;
        break;
      case 'h':
        do_help = true;
        break;
      case 'j':
        java_mode = true;
        break;
      case 'k':
        if (optarg == NULL || *optarg == '\0')
          desktop_default_keywords = false;
        else
          {
            /* Ensure that desktop_keywords is initialized.  */
            if (desktop_keywords.table == NULL)
              hash_init (&desktop_keywords, 100);
            desktop_add_keyword (&desktop_keywords, optarg, false);
          }
        break;
      case 'l':
        java_locale_name = optarg;
        csharp_locale_name = optarg;
        tcl_locale_name = optarg;
        desktop_locale_name = optarg;
        xml_locale_name = optarg;
        break;
      case 'L':
        xml_language = optarg;
        break;
      case 'o':
        output_file_name = optarg;
        break;
      case 'P':
        input_syntax = &input_format_properties;
        break;
      case 'r':
        java_resource_name = optarg;
        csharp_resource_name = optarg;
        break;
      case CHAR_MAX + 20: /* --strict */
        strict_uniforum = true;
        break;
      case 'v':
        verbose++;
        break;
      case 'V':
        do_version = true;
        break;
      case 'x':
        xml_mode = true;
        break;
      case CHAR_MAX + 1: /* --check-accelerators */
        check_accelerators = true;
        if (optarg != NULL)
          {
            if (optarg[0] != '\0' && ispunct ((unsigned char) optarg[0])
                && optarg[1] == '\0')
              accelerator_char = optarg[0];
            else
              error (EXIT_FAILURE, 0,
                     _("the argument to %s should be a single punctuation character"),
                     "--check-accelerators");
          }
        break;
      case CHAR_MAX + 2: /* --check-domain */
        check_domain = true;
        break;
      case CHAR_MAX + 3: /* --check-format */
        check_format_strings = true;
        break;
      case CHAR_MAX + 4: /* --check-header */
        check_header = true;
        break;
      case CHAR_MAX + 5: /* --java2 */
        java_mode = true;
        assume_java2 = true;
        break;
      case CHAR_MAX + 6: /* --no-hash */
        no_hash_table = true;
        break;
      case CHAR_MAX + 7: /* --tcl */
        tcl_mode = true;
        break;
      case CHAR_MAX + 8: /* --stringtable-input */
        input_syntax = &input_format_stringtable;
        break;
      case CHAR_MAX + 9: /* --qt */
        qt_mode = true;
        break;
      case CHAR_MAX + 10: /* --csharp */
        csharp_mode = true;
        break;
      case CHAR_MAX + 11: /* --csharp-resources */
        csharp_resources_mode = true;
        break;
      case CHAR_MAX + 12: /* --use-untranslated (undocumented) */
        include_untranslated = true;
        break;
      case CHAR_MAX + 13: /* --endianness={big|little} */
        {
          int endianness;

          if (strcmp (optarg, "big") == 0)
            endianness = 1;
          else if (strcmp (optarg, "little") == 0)
            endianness = 0;
          else
            error (EXIT_FAILURE, 0, _("invalid endianness: %s"), optarg);

          byteswap = endianness ^ ENDIANNESS;
        }
        break;
      case CHAR_MAX + 14: /* --source */
        java_output_source = true;
        break;
      case CHAR_MAX + 15: /* --desktop */
        desktop_mode = true;
        break;
      case CHAR_MAX + 16: /* --template=TEMPLATE */
        desktop_template_name = optarg;
        xml_template_name = optarg;
        break;
      case CHAR_MAX + 17: /* --no-convert */
        no_convert_to_utf8 = true;
        break;
      case CHAR_MAX + 18: /* --no-redundancy */
        no_redundancy = true;
        break;
      case CHAR_MAX + 19: /* --replace-text */
        xml_replace_text = true;
        break;
      default:
        usage (EXIT_FAILURE);
        break;
      }

  /* Version information is requested.  */
  if (do_version)
    {
      printf ("%s (GNU %s) %s\n", last_component (program_name),
              PACKAGE, VERSION);
      /* xgettext: no-wrap */
      printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\
License GPLv3+: GNU GPL version 3 or later <%s>\n\
This is free software: you are free to change and redistribute it.\n\
There is NO WARRANTY, to the extent permitted by law.\n\
"),
              "1995-2025", "https://gnu.org/licenses/gpl.html");
      printf (_("Written by %s.\n"), proper_name ("Ulrich Drepper"));
      exit (EXIT_SUCCESS);
    }

  /* Help is requested.  */
  if (do_help)
    usage (EXIT_SUCCESS);

  /* Test whether we have a .po file name as argument.  */
  if (optind >= argc
      && !(desktop_mode && desktop_base_directory)
      && !(xml_mode && xml_base_directory))
    {
      error (EXIT_SUCCESS, 0, _("no input file given"));
      usage (EXIT_FAILURE);
    }
  if (optind < argc
      && ((desktop_mode && desktop_base_directory)
          || (xml_mode && xml_base_directory)))
    {
      error (EXIT_SUCCESS, 0,
             _("no input file should be given if %s and %s are specified"),
             desktop_mode ? "--desktop" : "--xml", "-d");
      usage (EXIT_FAILURE);
    }

  /* Check for contradicting options.  */
  {
    unsigned int modes =
      (java_mode ? 1 : 0)
      | (csharp_mode ? 2 : 0)
      | (csharp_resources_mode ? 4 : 0)
      | (tcl_mode ? 8 : 0)
      | (qt_mode ? 16 : 0)
      | (desktop_mode ? 32 : 0)
      | (xml_mode ? 64 : 0);
    static const char *mode_options[] =
      { "--java", "--csharp", "--csharp-resources", "--tcl", "--qt",
        "--desktop", "--xml" };
    /* More than one bit set?  */
    if (modes & (modes - 1))
      {
        const char *first_option;
        const char *second_option;
        unsigned int i;
        for (i = 0; ; i++)
          if (modes & (1 << i))
            break;
        first_option = mode_options[i];
        for (i = i + 1; ; i++)
          if (modes & (1 << i))
            break;
        second_option = mode_options[i];
        error (EXIT_FAILURE, 0, _("%s and %s are mutually exclusive"),
               first_option, second_option);
      }
  }
  if (!xml_mode && xml_replace_text)
    {
      error (EXIT_SUCCESS, 0, _("%s is only valid with %s"),
             "--replace-text", "--xml");
      usage (EXIT_FAILURE);
    }
  if (java_mode)
    {
      if (output_file_name != NULL)
        {
          error (EXIT_FAILURE, 0, _("%s and %s are mutually exclusive"),
                 "--java", "--output-file");
        }
      if (java_class_directory == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"-d directory\" specification"),
                 "--java");
          usage (EXIT_FAILURE);
        }
    }
  else if (csharp_mode)
    {
      if (output_file_name != NULL)
        {
          error (EXIT_FAILURE, 0, _("%s and %s are mutually exclusive"),
                 "--csharp", "--output-file");
        }
      if (csharp_locale_name == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"-l locale\" specification"),
                 "--csharp");
          usage (EXIT_FAILURE);
        }
      if (csharp_base_directory == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"-d directory\" specification"),
                 "--csharp");
          usage (EXIT_FAILURE);
        }
    }
  else if (tcl_mode)
    {
      if (output_file_name != NULL)
        {
          error (EXIT_FAILURE, 0, _("%s and %s are mutually exclusive"),
                 "--tcl", "--output-file");
        }
      if (tcl_locale_name == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"-l locale\" specification"),
                 "--tcl");
          usage (EXIT_FAILURE);
        }
      if (tcl_base_directory == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"-d directory\" specification"),
                 "--tcl");
          usage (EXIT_FAILURE);
        }
    }
  else if (desktop_mode)
    {
      if (desktop_template_name == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"--template template\" specification"),
                 "--desktop");
          usage (EXIT_FAILURE);
        }
      if (output_file_name == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"-o file\" specification"),
                 "--desktop");
          usage (EXIT_FAILURE);
        }
      if (desktop_base_directory != NULL && desktop_locale_name != NULL)
        error (EXIT_FAILURE, 0,
               _("%s and %s are mutually exclusive in %s"),
               "-d", "-l", "--desktop");
      if (desktop_base_directory == NULL && desktop_locale_name == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"-l locale\" specification"),
                 "--desktop");
          usage (EXIT_FAILURE);
        }
    }
  else if (xml_mode)
    {
      if (xml_template_name == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"--template template\" specification"),
                 "--xml");
          usage (EXIT_FAILURE);
        }
      if (output_file_name == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"-o file\" specification"),
                 "--xml");
          usage (EXIT_FAILURE);
        }
      if (xml_replace_text && xml_base_directory != NULL)
        error (EXIT_FAILURE, 0,
               _("%s and %s are mutually exclusive in %s"),
               "--replace-text", "-d", "--xml");
      if (xml_base_directory != NULL && xml_locale_name != NULL)
        error (EXIT_FAILURE, 0,
               _("%s and %s are mutually exclusive in %s"),
               "-d", "-l", "--xml");
      if (xml_base_directory == NULL && xml_locale_name == NULL)
        {
          error (EXIT_SUCCESS, 0,
                 _("%s requires a \"-l locale\" specification"),
                 "--xml");
          usage (EXIT_FAILURE);
        }
    }
  else
    {
      if (java_resource_name != NULL)
        {
          error (EXIT_SUCCESS, 0, _("%s is only valid with %s or %s"),
                 "--resource", "--java", "--csharp");
          usage (EXIT_FAILURE);
        }
      if (java_locale_name != NULL)
        {
          error (EXIT_SUCCESS, 0, _("%s is only valid with %s, %s or %s"),
                 "--locale", "--java", "--csharp", "--tcl");
          usage (EXIT_FAILURE);
        }
      if (java_class_directory != NULL)
        {
          error (EXIT_SUCCESS, 0, _("%s is only valid with %s, %s or %s"),
                 "-d", "--java", "--csharp", "--tcl");
          usage (EXIT_FAILURE);
        }
    }

  if (desktop_mode)
    {
      /* Ensure that desktop_keywords is initialized.  */
      if (desktop_keywords.table == NULL)
        hash_init (&desktop_keywords, 100);
      if (desktop_default_keywords)
        desktop_add_default_keywords (&desktop_keywords);
    }

  /* Bulk processing mode for .desktop files.
     Process all .po files in desktop_base_directory.  */
  if (desktop_mode && desktop_base_directory)
    {
      exit_status = msgfmt_desktop_bulk (desktop_base_directory,
                                         desktop_template_name,
                                         &desktop_keywords,
                                         output_file_name);
      exit (exit_status);
    }

  if (xml_mode)
    {
      char **its_dirs;
      char **dirs;
      locating_rule_list_ty *its_locating_rules;
      const char *its_basename;

      its_dirs = get_search_path ("its");
      its_locating_rules = locating_rule_list_alloc ();
      for (dirs = its_dirs; *dirs != NULL; dirs++)
        locating_rule_list_add_from_directory (its_locating_rules, *dirs);

      its_basename = locating_rule_list_locate (its_locating_rules,
                                                xml_template_name,
                                                xml_language);

      if (its_basename != NULL)
        {
          size_t j;

          xml_its_rules = its_rule_list_alloc ();
          for (j = 0; its_dirs[j] != NULL; j++)
            {
              char *its_filename =
                xconcatenated_filename (its_dirs[j], its_basename, NULL);
              struct stat statbuf;
              bool ok = false;

              if (stat (its_filename, &statbuf) == 0)
                ok = its_rule_list_add_from_file (xml_its_rules, its_filename);
              free (its_filename);
              if (ok)
                break;
            }
          if (its_dirs[j] == NULL)
            {
              its_rule_list_free (xml_its_rules);
              xml_its_rules = NULL;
            }
        }
      locating_rule_list_free (its_locating_rules);

      for (dirs = its_dirs; *dirs != NULL; dirs++)
        free (*dirs);
      free (its_dirs);

      if (xml_its_rules == NULL)
        error (EXIT_FAILURE, 0, _("cannot locate ITS rules for %s"),
               xml_template_name);
    }

  /* Bulk processing mode for XML files.
     Process all .po files in xml_base_directory.  */
  if (xml_mode && xml_base_directory)
    {
      exit_status = msgfmt_xml_bulk (xml_base_directory,
                                     xml_template_name,
                                     xml_its_rules,
                                     output_file_name);
      exit (exit_status);
    }

  /* The -o option determines the name of the domain and therefore
     the output file.  */
  if (output_file_name != NULL)
    current_domain =
      new_domain (output_file_name,
                  strict_uniforum && !csharp_resources_mode && !qt_mode
                  ? add_mo_suffix (output_file_name)
                  : output_file_name);

  /* Process all given .po files.  */
  for (arg_i = optind; arg_i < argc; arg_i++)
    {
      /* Remember that we currently have not specified any domain.  This
         is of course not true when we saw the -o option.  */
      if (output_file_name == NULL)
        current_domain = NULL;

      /* And process the input file.  */
      read_catalog_file_msgfmt (argv[arg_i], input_syntax);
    }

  /* We know a priori that some input_syntax->parse() functions convert
     strings to UTF-8.  */
  canon_encoding = (input_syntax->produces_utf8 ? po_charset_utf8 : NULL);

  /* Remove obsolete messages.  They were only needed for duplicate
     checking.  */
  for (domain = domain_list; domain != NULL; domain = domain->next)
    message_list_remove_if_not (domain->mlp, is_nonobsolete);

  /* Perform all kinds of checks: plural expressions, format strings, ...  */
  {
    int nerrors = 0;

    for (domain = domain_list; domain != NULL; domain = domain->next)
      nerrors +=
        check_message_list (domain->mlp,
                            /* Untranslated and fuzzy messages have already
                               been dealt with during parsing, see below in
                               msgfmt_frob_new_message.  */
                            0, 0,
                            1, check_format_strings, check_header,
                            check_compatibility,
                            check_accelerators, accelerator_char,
                            textmode_xerror_handler);

    /* Exit with status 1 on any error.  */
    if (nerrors > 0)
      {
        error (0, 0,
               ngettext ("found %d fatal error", "found %d fatal errors",
                         nerrors),
               nerrors);
        exit_status = EXIT_FAILURE;
      }
  }

  /* Compose the input file name(s).
     This is used for statistics and error messages.  */
  char *all_input_file_names;
  {
    string_list_ty input_file_names;

    string_list_init (&input_file_names);;
    for (arg_i = optind; arg_i < argc; arg_i++)
      string_list_append (&input_file_names, argv[arg_i]);
    all_input_file_names =
      string_list_join (&input_file_names, ", ", '\0', false);
    string_list_destroy (&input_file_names);
  }

  /* Now write out all domains.  */
  for (domain = domain_list; domain != NULL; domain = domain->next)
    {
      if (java_mode)
        {
          if (msgdomain_write_java (domain->mlp, canon_encoding,
                                    java_resource_name, java_locale_name,
                                    java_class_directory, assume_java2,
                                    java_output_source))
            exit_status = EXIT_FAILURE;
        }
      else if (csharp_mode)
        {
          if (msgdomain_write_csharp (domain->mlp, canon_encoding,
                                      csharp_resource_name, csharp_locale_name,
                                      csharp_base_directory))
            exit_status = EXIT_FAILURE;
        }
      else if (csharp_resources_mode)
        {
          if (msgdomain_write_csharp_resources (domain->mlp, canon_encoding,
                                                domain->domain_name,
                                                domain->file_name))
            exit_status = EXIT_FAILURE;
        }
      else if (tcl_mode)
        {
          if (msgdomain_write_tcl (domain->mlp, canon_encoding,
                                   tcl_locale_name, tcl_base_directory))
            exit_status = EXIT_FAILURE;
        }
      else if (qt_mode)
        {
          if (msgdomain_write_qt (domain->mlp, canon_encoding,
                                  domain->domain_name, domain->file_name))
            exit_status = EXIT_FAILURE;
        }
      else if (desktop_mode)
        {
          if (msgdomain_write_desktop (domain->mlp, canon_encoding,
                                       desktop_locale_name,
                                       desktop_template_name,
                                       &desktop_keywords,
                                       domain->file_name))
            exit_status = EXIT_FAILURE;
        }
      else if (xml_mode)
        {
          if (msgdomain_write_xml (domain->mlp, canon_encoding,
                                   xml_locale_name,
                                   xml_template_name,
                                   xml_its_rules,
                                   xml_replace_text,
                                   domain->file_name))
            exit_status = EXIT_FAILURE;
        }
      else
        {
          if (msgdomain_write_mo (domain->mlp, domain->domain_name,
                                  domain->file_name, all_input_file_names))
            exit_status = EXIT_FAILURE;
        }

      /* List is not used anymore.  */
      message_list_free (domain->mlp, 0);
    }

  /* Print statistics if requested.  */
  if (verbose || do_statistics)
    {
      if (do_statistics + verbose >= 2 && optind < argc)
        {
          /* Print the input file name(s) in front of the statistics line.  */
          /* TRANSLATORS: The prefix before a statistics message.  The argument
             is a file name or a comma separated list of file names.  */
          fprintf (stderr, _("%s: "), all_input_file_names);
        }
      fprintf (stderr,
               ngettext ("%d translated message", "%d translated messages",
                         msgs_translated),
               msgs_translated);
      if (msgs_fuzzy > 0)
        fprintf (stderr,
                 ngettext (", %d fuzzy translation", ", %d fuzzy translations",
                           msgs_fuzzy),
                 msgs_fuzzy);
      if (msgs_untranslated > 0)
        fprintf (stderr,
                 ngettext (", %d untranslated message",
                           ", %d untranslated messages",
                           msgs_untranslated),
                 msgs_untranslated);
      fputs (".\n", stderr);
    }

  exit (exit_status);
}


/* Display usage information and exit.  */
static void
usage (int status)
{
  if (status != EXIT_SUCCESS)
    fprintf (stderr, _("Try '%s --help' for more information.\n"),
             program_name);
  else
    {
      printf (_("\
Usage: %s [OPTION] filename.po ...\n\
"), program_name);
      printf ("\n");
      printf (_("\
Generate binary message catalog from textual translation description.\n\
"));
      printf ("\n");
      /* xgettext: no-wrap */
      printf (_("\
Mandatory arguments to long options are mandatory for short options too.\n\
Similarly for optional arguments.\n\
"));
      printf ("\n");
      printf (_("\
Input file location:\n"));
      printf (_("\
  filename.po ...             input files\n"));
      printf (_("\
  -D, --directory=DIRECTORY   add DIRECTORY to list for input files search\n"));
      printf (_("\
If input file is -, standard input is read.\n"));
      printf ("\n");
      printf (_("\
Operation mode:\n"));
      printf (_("\
  -j, --java                  Java mode: generate a Java ResourceBundle class\n"));
      printf (_("\
      --java2                 like --java, and assume Java2 (JDK 1.2 or higher)\n"));
      printf (_("\
      --csharp                C# mode: generate a .NET .dll file\n"));
      printf (_("\
      --csharp-resources      C# resources mode: generate a .NET .resources file\n"));
      printf (_("\
      --tcl                   Tcl mode: generate a tcl/msgcat .msg file\n"));
      printf (_("\
      --qt                    Qt mode: generate a Qt .qm file\n"));
      printf (_("\
      --desktop               Desktop Entry mode: generate a .desktop file\n"));
      printf (_("\
      --xml                   XML mode: generate XML file\n"));
      printf ("\n");
      printf (_("\
Output file location:\n"));
      printf (_("\
  -o, --output-file=FILE      write output to specified file\n"));
      printf (_("\
      --strict                enable strict Uniforum mode\n"));
      printf (_("\
If output file is -, output is written to standard output.\n"));
      printf ("\n");
      printf (_("\
Output file location in Java mode:\n"));
      printf (_("\
  -r, --resource=RESOURCE     resource name\n"));
      printf (_("\
  -l, --locale=LOCALE         locale name, either language or language_COUNTRY\n"));
      printf (_("\
      --source                produce a .java file, instead of a .class file\n"));
      printf (_("\
  -d DIRECTORY                base directory of classes directory hierarchy\n"));
      printf (_("\
The class name is determined by appending the locale name to the resource name,\n\
separated with an underscore.  The -d option is mandatory.  The class is\n\
written under the specified directory.\n\
"));
      printf ("\n");
      printf (_("\
Output file location in C# mode:\n"));
      printf (_("\
  -r, --resource=RESOURCE     resource name\n"));
      printf (_("\
  -l, --locale=LOCALE         locale name, either language or language_COUNTRY\n"));
      printf (_("\
  -d DIRECTORY                base directory for locale dependent .dll files\n"));
      printf (_("\
The -l and -d options are mandatory.  The .dll file is written in a\n\
subdirectory of the specified directory whose name depends on the locale.\n"));
      printf ("\n");
      printf (_("\
Output file location in Tcl mode:\n"));
      printf (_("\
  -l, --locale=LOCALE         locale name, either language or language_COUNTRY\n"));
      printf (_("\
  -d DIRECTORY                base directory of .msg message catalogs\n"));
      printf (_("\
The -l and -d options are mandatory.  The .msg file is written in the\n\
specified directory.\n"));
      printf ("\n");
      printf (_("\
Desktop Entry mode options:\n"));
      printf (_("\
  -l, --locale=LOCALE         locale name, either language or language_COUNTRY\n"));
      printf (_("\
  -o, --output-file=FILE      write output to specified file\n"));
      printf (_("\
  --template=TEMPLATE         a .desktop file used as a template\n"));
      printf (_("\
  -d DIRECTORY                base directory of .po files\n"));
      printf (_("\
  -kWORD, --keyword=WORD      look for WORD as an additional keyword\n\
  -k, --keyword               do not to use default keywords\n"));
      printf (_("\
The -l, -o, and --template options are mandatory.  If -D is specified, input\n\
files are read from the directory instead of the command line arguments.\n"));
      printf ("\n");
      printf (_("\
XML mode options:\n"));
      printf (_("\
  -l, --locale=LOCALE         locale name, either language or language_COUNTRY\n"));
      printf (_("\
  -L, --language=NAME         recognise the specified XML language\n"));
      printf (_("\
  -o, --output-file=FILE      write output to specified file\n"));
      printf (_("\
  --template=TEMPLATE         an XML file used as a template\n"));
      printf (_("\
  -d DIRECTORY                base directory of .po files\n"));
      printf (_("\
  --replace-text              output XML with translated text replacing the\n\
                              original text, not augmenting the original text\n"));
      printf (_("\
The -l, -o, and --template options are mandatory.  If -D is specified, input\n\
files are read from the directory instead of the command line arguments.\n"));
      printf ("\n");
      printf (_("\
Input file syntax:\n"));
      printf (_("\
  -P, --properties-input      input files are in Java .properties syntax\n"));
      printf (_("\
      --stringtable-input     input files are in NeXTstep/GNUstep .strings\n\
                              syntax\n"));
      printf ("\n");
      printf (_("\
Input file interpretation:\n"));
      printf (_("\
  -c, --check                 perform all the checks implied by\n\
                                --check-format, --check-header, --check-domain\n"));
      printf (_("\
      --check-format          check language dependent format strings\n"));
      printf (_("\
      --check-header          verify presence and contents of the header entry\n"));
      printf (_("\
      --check-domain          check for conflicts between domain directives\n\
                                and the --output-file option\n"));
      printf (_("\
  -C, --check-compatibility   check that GNU msgfmt behaves like X/Open msgfmt\n"));
      printf (_("\
      --check-accelerators[=CHAR]  check presence of keyboard accelerators for\n\
                                menu items\n"));
      printf (_("\
  -f, --use-fuzzy             use fuzzy entries in output\n"));
      printf ("\n");
      printf (_("\
Output details:\n"));
      printf (_("\
      --no-convert            don't convert the messages to UTF-8 encoding\n"));
      printf (_("\
      --no-redundancy         don't pre-expand ISO C 99 <inttypes.h>\n\
                                format string directive macros\n"));
      printf (_("\
  -a, --alignment=NUMBER      align strings to NUMBER bytes (default: %d)\n"), DEFAULT_OUTPUT_ALIGNMENT);
      printf (_("\
      --endianness=BYTEORDER  write out 32-bit numbers in the given byte order\n\
                                (big or little, default depends on platform)\n"));
      printf (_("\
      --no-hash               binary file will not include the hash table\n"));
      printf ("\n");
      printf (_("\
Informative output:\n"));
      printf (_("\
  -h, --help                  display this help and exit\n"));
      printf (_("\
  -V, --version               output version information and exit\n"));
      printf (_("\
      --statistics            print statistics about translations\n"));
      printf (_("\
  -v, --verbose               increase verbosity level\n"));
      printf ("\n");
      /* TRANSLATORS: The first placeholder is the web address of the Savannah
         project of this package.  The second placeholder is the bug-reporting
         email address for this package.  Please add _another line_ saying
         "Report translation bugs to <...>\n" with the address for translation
         bugs (typically your translation team's web or email address).  */
      printf(_("\
Report bugs in the bug tracker at <%s>\n\
or by email to <%s>.\n"),
             "https://savannah.gnu.org/projects/gettext",
             "bug-gettext@gnu.org");
    }

  exit (status);
}


static const char *
add_mo_suffix (const char *fname)
{
  size_t len;
  char *result;

  len = strlen (fname);
  if (len > 3 && memcmp (fname + len - 3, ".mo", 3) == 0)
    return fname;
  if (len > 4 && memcmp (fname + len - 4, ".gmo", 4) == 0)
    return fname;
  result = XNMALLOC (len + 4, char);
  stpcpy (stpcpy (result, fname), ".mo");
  return result;
}


static struct msg_domain *
new_domain (const char *name, const char *file_name)
{
  struct msg_domain **p_dom = &domain_list;

  while (*p_dom != NULL && strcmp (name, (*p_dom)->domain_name) != 0)
    p_dom = &(*p_dom)->next;

  if (*p_dom == NULL)
    {
      struct msg_domain *domain;

      domain = XMALLOC (struct msg_domain);
      domain->mlp = message_list_alloc (true);
      domain->domain_name = name;
      domain->file_name = file_name;
      domain->next = NULL;
      *p_dom = domain;
    }

  return *p_dom;
}


static bool
is_nonobsolete (const message_ty *mp)
{
  return !mp->obsolete;
}


/* The rest of the file defines a subclass msgfmt_catalog_reader_ty of
   default_catalog_reader_ty.  Its particularities are:
   - The header entry check is performed on-the-fly.
   - Comments are not stored, they are discarded right away.
     (This is achieved by setting handle_comments = false.)
   - The multi-domain handling is adapted to our domain_list.
 */


/* This structure defines a derived class of the default_catalog_reader_ty
   class.  (See read-catalog-abstract.h for an explanation.)  */
typedef struct msgfmt_catalog_reader_ty msgfmt_catalog_reader_ty;
struct msgfmt_catalog_reader_ty
{
  /* inherited instance variables, etc */
  DEFAULT_CATALOG_READER_TY

  bool has_header_entry;
};


/* Prepare for first message.  */
static void
msgfmt_constructor (abstract_catalog_reader_ty *catr)
{
  msgfmt_catalog_reader_ty *mcatr = (msgfmt_catalog_reader_ty *) catr;

  /* Invoke superclass constructor.  */
  default_constructor (catr);

  mcatr->has_header_entry = false;
}


/* Some checks after whole file is read.  */
static void
msgfmt_parse_debrief (abstract_catalog_reader_ty *catr)
{
  msgfmt_catalog_reader_ty *mcatr = (msgfmt_catalog_reader_ty *) catr;

  /* Invoke superclass method.  */
  default_parse_debrief (catr);

  /* Test whether header entry was found.  */
  if (check_header)
    {
      if (!mcatr->has_header_entry)
        {
          size_t prefix_width =
            multiline_error (xasprintf ("%s: ", mcatr->file_name),
                             xasprintf (_("warning: PO file header missing or invalid\n")));
          multiline_append (prefix_width,
                            xasprintf (_("warning: charset conversion will not work\n")));
        }
    }
}


/* Set 'domain' directive when seen in .po file.  */
static void
msgfmt_set_domain (default_catalog_reader_ty *dcatr,
                   char *name, lex_pos_ty *name_pos)
{
  /* If no output file was given, we change it with each 'domain'
     directive.  */
  if (!java_mode && !csharp_mode && !csharp_resources_mode && !tcl_mode
      && !qt_mode && !desktop_mode && !xml_mode && output_file_name == NULL)
    {
      size_t correct;

      correct = strcspn (name, INVALID_PATH_CHAR);
      if (name[correct] != '\0')
        {
          exit_status = EXIT_FAILURE;
          if (correct == 0)
            {
              error (0, 0,
                     _("domain name \"%s\" not suitable as file name"), name);
              return;
            }
          else
            error (0, 0,
                   _("domain name \"%s\" not suitable as file name: will use prefix"),
                   name);
          name[correct] = '\0';
        }

      /* Set new domain.  */
      current_domain = new_domain (name, add_mo_suffix (name));
      dcatr->domain = current_domain->domain_name;
      dcatr->mlp = current_domain->mlp;
    }
  else
    {
      if (check_domain)
        po_xerror (PO_SEVERITY_ERROR, NULL,
                   name_pos->file_name, name_pos->line_number, (size_t)(-1),
                   false,
                   xasprintf (_("'domain %s' directive ignored"), name));

      /* NAME was allocated in read-po-gram.y but is not used anywhere.  */
      free (name);
    }
}


static void
msgfmt_add_message (default_catalog_reader_ty *dcatr,
                    char *msgctxt,
                    char *msgid,
                    lex_pos_ty *msgid_pos,
                    char *msgid_plural,
                    char *msgstr, size_t msgstr_len,
                    lex_pos_ty *msgstr_pos,
                    char *prev_msgctxt,
                    char *prev_msgid,
                    char *prev_msgid_plural,
                    bool force_fuzzy, bool obsolete)
{
  /* Check whether already a domain is specified.  If not, use default
     domain.  */
  if (current_domain == NULL)
    {
      current_domain = new_domain (MESSAGE_DOMAIN_DEFAULT,
                                   add_mo_suffix (MESSAGE_DOMAIN_DEFAULT));
      /* Keep current_domain and dcatr->domain synchronized.  */
      dcatr->domain = current_domain->domain_name;
      dcatr->mlp = current_domain->mlp;
    }

  /* Invoke superclass method.  */
  default_add_message (dcatr, msgctxt, msgid, msgid_pos, msgid_plural,
                       msgstr, msgstr_len, msgstr_pos,
                       prev_msgctxt, prev_msgid, prev_msgid_plural,
                       force_fuzzy, obsolete);
}


static void
msgfmt_frob_new_message (default_catalog_reader_ty *dcatr, message_ty *mp,
                         const lex_pos_ty *msgid_pos,
                         const lex_pos_ty *msgstr_pos)
{
  msgfmt_catalog_reader_ty *mcatr = (msgfmt_catalog_reader_ty *) dcatr;

  if (!mp->obsolete)
    {
      /* Don't emit untranslated entries.
         Also don't emit fuzzy entries, unless --use-fuzzy was specified.
         But ignore fuzziness of the header entry.  */
      if ((!include_untranslated && mp->msgstr[0] == '\0')
          || (!include_fuzzies && mp->is_fuzzy && !is_header (mp)))
        {
          if (check_compatibility)
            {
              error_with_progname = false;
              error_at_line (0, 0, mp->pos.file_name, mp->pos.line_number,
                             (mp->msgstr[0] == '\0'
                              ? _("empty 'msgstr' entry ignored")
                              : _("fuzzy 'msgstr' entry ignored")));
              error_with_progname = true;
            }

          /* Increment counter for fuzzy/untranslated messages.  */
          if (mp->msgstr[0] == '\0')
            ++msgs_untranslated;
          else
            ++msgs_fuzzy;

          mp->obsolete = true;
        }
      else
        {
          /* Test for header entry.  */
          if (is_header (mp))
            {
              mcatr->has_header_entry = true;
            }
          else
            /* We don't count the header entry in the statistic so place
               the counter incrementation here.  */
            if (mp->is_fuzzy)
              ++msgs_fuzzy;
            else
              ++msgs_translated;
        }
    }
}


/* Test for '#, fuzzy' comments and warn.  */
static void
msgfmt_comment_special (abstract_catalog_reader_ty *catr, const char *s)
{
  msgfmt_catalog_reader_ty *mcatr = (msgfmt_catalog_reader_ty *) catr;

  /* Invoke superclass method.  */
  default_comment_special (catr, s);

  if (mcatr->is_fuzzy)
    {
      static bool warned = false;

      if (!include_fuzzies && check_compatibility && !warned)
        {
          warned = true;
          error (0, 0,
                 _("%s: warning: source file contains fuzzy translation"),
                 mcatr->file_name);
        }
    }
}


/* So that the one parser can be used for multiple programs, and also
   use good data hiding and encapsulation practices, an object
   oriented approach has been taken.  An object instance is allocated,
   and all actions resulting from the parse will be through
   invocations of method functions of that object.  */

static default_catalog_reader_class_ty msgfmt_methods =
{
  {
    sizeof (msgfmt_catalog_reader_ty),
    msgfmt_constructor,
    default_destructor,
    default_parse_brief,
    msgfmt_parse_debrief,
    default_directive_domain,
    default_directive_message,
    default_comment,
    default_comment_dot,
    default_comment_filepos,
    msgfmt_comment_special
  },
  msgfmt_set_domain, /* set_domain */
  msgfmt_add_message, /* add_message */
  msgfmt_frob_new_message /* frob_new_message */
};


/* Read .po file FILENAME and store translation pairs.  */
static void
read_catalog_file_msgfmt (char *filename, catalog_input_format_ty input_syntax)
{
  char *real_filename;
  FILE *fp = open_catalog_file (filename, &real_filename, true);
  default_catalog_reader_ty *dcatr;

  dcatr = default_catalog_reader_alloc (&msgfmt_methods,
                                        textmode_xerror_handler);
  dcatr->pass_obsolete_entries = true;
  dcatr->handle_comments = false;
  dcatr->allow_domain_directives = true;
  dcatr->allow_duplicates = false;
  dcatr->allow_duplicates_if_same_msgstr = false;
  dcatr->file_name = real_filename;
  dcatr->mdlp = NULL;
  dcatr->mlp = NULL;
  if (current_domain != NULL)
    {
      /* Keep current_domain and dcatr->domain synchronized.  */
      dcatr->domain = current_domain->domain_name;
      dcatr->mlp = current_domain->mlp;
    }
  catalog_reader_parse ((abstract_catalog_reader_ty *) dcatr, fp, real_filename,
                        filename, false, input_syntax);
  catalog_reader_free ((abstract_catalog_reader_ty *) dcatr);

  if (fp != stdin)
    fclose (fp);
}

static void
add_languages (string_list_ty *languages, string_list_ty *desired_languages,
               const char *line, size_t length)
{
  const char *start;

  /* Split the line by whitespace and build the languages list.  */
  for (start = line; start - line < length; )
    {
      const char *p;

      /* Skip whitespace before the string.  */
      while (*start == ' ' || *start == '\t')
        start++;

      p = start;
      while (*p != '\0' && *p != ' ' && *p != '\t')
        p++;

      if (desired_languages == NULL
          || string_list_member_desc (desired_languages, start, p - start))
        string_list_append_unique_desc (languages, start, p - start);
      start = p + 1;
    }
}

/* Compute the languages list by reading the "LINGUAS" envvar or the
   LINGUAS file under DIRECTORY.  */
static void
get_languages (string_list_ty *languages, const char *directory)
{
  char *envval;
  string_list_ty real_desired_languages, *desired_languages = NULL;
  char *linguas_file_name = NULL;
  struct stat statbuf;
  FILE *fp;
  size_t line_len = 0;
  char *line_buf = NULL;

  envval = getenv ("LINGUAS");
  if (envval)
    {
      string_list_init (&real_desired_languages);
      add_languages (&real_desired_languages, NULL, envval, strlen (envval));
      desired_languages = &real_desired_languages;
    }

  linguas_file_name = xconcatenated_filename (directory, "LINGUAS", NULL);
  if (stat (linguas_file_name, &statbuf) < 0)
    {
      error (EXIT_SUCCESS, 0, _("%s does not exist"), linguas_file_name);
      goto out;
    }

  fp = fopen (linguas_file_name, "r");
  if (fp == NULL)
    {
      error (EXIT_SUCCESS, 0, _("%s exists but cannot read"),
             linguas_file_name);
      goto out;
    }

  while (!feof (fp))
    {
      /* Read next line from file.  */
      int len = getline (&line_buf, &line_len, fp);

      /* In case of an error leave loop.  */
      if (len < 0)
        break;

      /* Remove trailing '\n' and trailing whitespace.  */
      if (len > 0 && line_buf[len - 1] == '\n')
        line_buf[--len] = '\0';
      while (len > 0
             && (line_buf[len - 1] == ' '
                 || line_buf[len - 1] == '\t'
                 || line_buf[len - 1] == '\r'))
        line_buf[--len] = '\0';

      /* Test if we have to ignore the line.  */
      if (!(*line_buf == '\0' || *line_buf == '#'))
        /* Include the line among the languages.  */
        add_languages (languages, desired_languages, line_buf, len);
    }

  free (line_buf);
  fclose (fp);

 out:
  if (desired_languages != NULL)
    string_list_destroy (desired_languages);
  free (linguas_file_name);
}

static void
msgfmt_operand_list_init (msgfmt_operand_list_ty *operands)
{
  operands->items = NULL;
  operands->nitems = 0;
  operands->nitems_max = 0;
}

static void
msgfmt_operand_list_destroy (msgfmt_operand_list_ty *operands)
{
  size_t i;

  for (i = 0; i < operands->nitems; i++)
    {
      free (operands->items[i].language);
      message_list_free (operands->items[i].mlp, 0);
    }
  free (operands->items);
}

static void
msgfmt_operand_list_append (msgfmt_operand_list_ty *operands,
                            const char *language,
                            message_list_ty *messages)
{
  msgfmt_operand_ty *operand;

  if (operands->nitems == operands->nitems_max)
    {
      operands->nitems_max = operands->nitems_max * 2 + 1;
      operands->items = xrealloc (operands->items,
                                  sizeof (msgfmt_operand_ty)
                                  * operands->nitems_max);
    }

  operand = &operands->items[operands->nitems++];
  operand->language = xstrdup (language);
  operand->mlp = messages;
}

static int
msgfmt_operand_list_add_from_directory (msgfmt_operand_list_ty *operands,
                                        const char *directory)
{
  string_list_ty languages;
  void *saved_dir_list;
  int retval = 0;
  size_t i;

  string_list_init (&languages);
  get_languages (&languages, directory);

  if (languages.nitems == 0)
    return 0;

  /* Reset the directory search list so only .po files under DIRECTORY
     will be read.  */
  saved_dir_list = dir_list_save_reset ();
  dir_list_append (directory);

  /* Read all .po files.  */
  for (i = 0; i < languages.nitems; i++)
    {
      const char *language = languages.item[i];
      message_list_ty *mlp;
      char *input_file_name;
      int nerrors;

      current_domain = new_domain (MESSAGE_DOMAIN_DEFAULT,
                                   add_mo_suffix (MESSAGE_DOMAIN_DEFAULT));

      input_file_name = xconcatenated_filename ("", language, ".po");
      read_catalog_file_msgfmt (input_file_name, &input_format_po);
      free (input_file_name);

      /* The domain directive is not supported in the bulk execution mode.
         Thus, domain_list should always contain a single domain.  */
      assert (current_domain == domain_list && domain_list->next == NULL);
      mlp = current_domain->mlp;
      free (current_domain);
      current_domain = domain_list = NULL;

      /* Remove obsolete messages.  They were only needed for duplicate
         checking.  */
      message_list_remove_if_not (mlp, is_nonobsolete);

      /* Perform all kinds of checks: plural expressions, format
         strings, ...  */
      nerrors =
        check_message_list (mlp,
                            /* Untranslated and fuzzy messages have already
                               been dealt with during parsing, see below in
                               msgfmt_frob_new_message.  */
                            0, 0,
                            1, check_format_strings, check_header,
                            check_compatibility,
                            check_accelerators, accelerator_char,
                            textmode_xerror_handler);

      retval += nerrors;
      if (nerrors > 0)
        {
          error (0, 0,
                 ngettext ("found %d fatal error", "found %d fatal errors",
                           nerrors),
                 nerrors);
          continue;
        }

      /* Convert the messages to Unicode.  */
      iconv_message_list (mlp, NULL, po_charset_utf8, NULL,
                          textmode_xerror_handler);

      msgfmt_operand_list_append (operands, language, mlp);
    }

  string_list_destroy (&languages);
  dir_list_restore (saved_dir_list);

  return retval;
}

/* Helper function to support 'bulk' operation mode of --desktop.
   This reads all .po files in DIRECTORY and merges them into a
   .desktop file FILE_NAME.  Currently it does not support some
   options available in 'iterative' mode, such as --statistics.  */
static int
msgfmt_desktop_bulk (const char *directory,
                     const char *template_file_name,
                     hash_table *keywords,
                     const char *file_name)
{
  msgfmt_operand_list_ty operands;
  int nerrors, status;

  msgfmt_operand_list_init (&operands);

  /* Read all .po files.  */
  nerrors = msgfmt_operand_list_add_from_directory (&operands, directory);
  if (nerrors > 0)
    status = 1;
  else
    /* Write the messages into .desktop file.  */
    status = msgdomain_write_desktop_bulk (&operands,
                                           template_file_name,
                                           keywords,
                                           file_name);

  msgfmt_operand_list_destroy (&operands);

  return status;
}

/* Helper function to support 'bulk' operation mode of --xml.
   This reads all .po files in DIRECTORY and merges them into an
   XML file FILE_NAME.  Currently it does not support some
   options available in 'iterative' mode, such as --statistics.  */
static int
msgfmt_xml_bulk (const char *directory,
                 const char *template_file_name,
                 its_rule_list_ty *its_rules,
                 const char *file_name)
{
  msgfmt_operand_list_ty operands;
  int nerrors, status;

  msgfmt_operand_list_init (&operands);

  /* Read all .po files.  */
  nerrors = msgfmt_operand_list_add_from_directory (&operands, directory);
  if (nerrors > 0)
    {
      msgfmt_operand_list_destroy (&operands);
      return 1;
    }

  /* Write the messages into .xml file.  */
  status = msgdomain_write_xml_bulk (&operands,
                                     template_file_name,
                                     its_rules,
                                     false,
                                     file_name);

  msgfmt_operand_list_destroy (&operands);

  return status;
}
