]> git.ipfire.org Git - thirdparty/gettext.git/commitdiff
msginit: Produce a merged PO file instead of failing.
authorBruno Haible <bruno@clisp.org>
Sun, 12 Oct 2025 02:00:26 +0000 (04:00 +0200)
committerBruno Haible <bruno@clisp.org>
Sun, 12 Oct 2025 11:03:50 +0000 (13:03 +0200)
* gettext-tools/src/msginit.c: Include <omp.h>, msgl-merge.h, backupfile.h,
copy-file.h.
(catalogname): Remove variable.
(main): When the PO file already exists, create a backup file, then merge the
two files.
(usage): Say what happens if the output file already exists.
(struct header_entry_field): New type.
(fresh_fields): Renamed from fields.
(NFIELDS): Remove macro.
(FRESH_FIELDS_LAST_TRANSLATOR): Renamed from FIELD_LAST_TRANSLATOR.
(update_fields): New variable.
(UPDATE_FIELDS_LAST_TRANSLATOR): New macro.
(fill_header): Add 'fresh' parameter. Allocate field_value array on the heap.
* gettext-tools/src/Makefile.am (msginit_SOURCES): Add msgl-fsearch.c,
msgl-merge.c.
(msginit_CFLAGS, msginit_CXXFLAGS): Link with the OpenMP flags.
* gettext-tools/doc/msginit.texi: Say what happens if the output file already
exists.
* gettext-tools/doc/gettext.texi (Creating): Change title. Mention that msginit
can also be used when continuing an existing translation.
* NEWS: Mention the improvement.

NEWS
gettext-tools/doc/gettext.texi
gettext-tools/doc/msginit.texi
gettext-tools/src/Makefile.am
gettext-tools/src/msginit.c

diff --git a/NEWS b/NEWS
index 518b2a957cf5b32e43f1acde3228973150db9778..730c97f8c5cf012b284d051eb48a14fba8ca7146 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,11 @@
 Version 0.27 - October 2025
 
+# Improvements for translators:
+  * msginit:
+    - When the PO file already exists, 'msginit' now updates it w.r.t. the
+      POT file, like 'msgmerge' would do. Previously, 'msginit' failed with
+      an error message in this situation.
+
 # Programming languages support:
   * OCaml:
     - xgettext now supports OCaml.
index 9571c6070bd7719beca51e9ad90d6bad708c088a..7b31eeceb57f437c6c2ca5a8189f6a08295cd62c 100644 (file)
@@ -75,7 +75,7 @@
 * msgfilter: (gettext)msgfilter Invocation.    Pipe a PO file through a filter.
 * msgfmt: (gettext)msgfmt Invocation.          Make MO files out of PO files.
 * msggrep: (gettext)msggrep Invocation.        Select part of a PO file.
-* msginit: (gettext)msginit Invocation.        Create a fresh PO file.
+* msginit: (gettext)msginit Invocation.        Start translating a PO file.
 * msgmerge: (gettext)msgmerge Invocation.      Update a PO file from template.
 * msgunfmt: (gettext)msgunfmt Invocation.      Uncompile MO file into PO file.
 * msguniq: (gettext)msguniq Invocation.        Unify duplicates for PO file.
@@ -3746,15 +3746,23 @@ This is because @code{msgcat} generally is meant to produce PO files that
 are to be reviewed and edited by a translator; this is not desired here.
 
 @node Creating
-@chapter Creating a New PO File
+@chapter Creating or preparing for translating a PO file
 @cindex creating a new PO file
+@cindex preparing for translating a PO file
 
 When starting a new translation, the translator creates a file called
 @file{@var{LANG}.po}, as a copy of the @file{@var{package}.pot} template
 file with modifications in the initial comments (at the beginning of the file)
 and in the header entry (the first entry, near the beginning of the file).
 
-The easiest way to do so is by use of the @samp{msginit} program.
+Before continuing an existing translation,
+after a new release of the package @var{package} was made,
+the translator updates the file called @file{@var{LANG}.po},
+with respect to the new @file{@var{package}.pot} template file,
+and adds her name in the @code{Last-Translator} field of the header entry.
+
+The easiest way to do so, in either case,
+is by use of the @samp{msginit} program.
 For example:
 
 @example
@@ -3763,10 +3771,20 @@ $ cd po
 $ msginit
 @end example
 
-The alternative way is to do the copy and modifications by hand.
-To do so, the translator copies @file{@var{package}.pot} to
-@file{@var{LANG}.po}.  Then she modifies the initial comments and
-the header entry of this file.
+The alternative way, without @code{msginit}, is as follows:
+@itemize @bullet
+@item
+When starting a new translation,
+the translator does the copy and modifications by hand.
+She copies @file{@var{package}.pot} to @file{@var{LANG}.po}.
+Then she modifies the initial comments and the header entry of this file.
+
+@item
+When continuing an existing translation,
+the translator invokes
+@code{msgmerge --previous -o @var{lang}.new.po @var{lang}.po @var{package}.pot}
+and @code{mv @var{lang}.new.po @var{lang}.po}.
+@end itemize
 
 @menu
 * msginit Invocation::          Invoking the @code{msginit} Program
index 74b143bdffa08e2b277dfb2ef8d2804d5871a4d2..419bd41fa90fb390ada47b999523afccfa711403 100644 (file)
@@ -1,5 +1,5 @@
 @c This file is part of the GNU gettext manual.
-@c Copyright (C) 1995-2019 Free Software Foundation, Inc.
+@c Copyright (C) 1995-2025 Free Software Foundation, Inc.
 @c See the file gettext.texi for copying conditions.
 
 @pindex msginit
@@ -77,8 +77,12 @@ Write output to specified PO file.
 @end table
 
 If no output file is given, it depends on the @samp{--locale} option or the
-user's locale setting.  If it is @samp{-}, the results are written to
-standard output.
+user's locale setting.
+
+If the output file already exists, it is merged with the input file,
+as if through @command{msgmerge}.
+
+If it is @samp{-}, the results are written to standard output.
 
 @subsection Input file syntax
 
index 797801e1cdee813b3e1cf2a4756f0511caf1dbea..1bcebf69112b07ad04453c605aecc9f3b5e06b66 100644 (file)
@@ -397,7 +397,7 @@ else
 msggrep_SOURCES = ../woe32dll/c++msggrep.cc
 endif
 msginit_SOURCES = msginit.c
-msginit_SOURCES += lang-table.c plural-count.c
+msginit_SOURCES += msgl-fsearch.c msgl-merge.c lang-table.c plural-count.c
 msginit_SOURCES += ../../gettext-runtime/intl/localealias.c
 if !WOE32DLL
 msguniq_SOURCES = msguniq.c
@@ -491,6 +491,8 @@ endif
 # Compile-time flags for particular source files.
 msgmerge_CFLAGS = $(AM_CFLAGS) $(OPENMP_CFLAGS)
 msgmerge_CXXFLAGS = $(AM_CXXFLAGS) $(OPENMP_CFLAGS)
+msginit_CFLAGS = $(AM_CFLAGS) $(OPENMP_CFLAGS)
+msginit_CXXFLAGS = $(AM_CXXFLAGS) $(OPENMP_CFLAGS)
 # On mingw, the compiler option '-fno-threadsafe-statics' avoids requiring
 # the symbols __cxa_guard_acquire and __cxa_guard_release, which in turn
 # avoids a dependency towards libstdc++.
index 482b110f0313944bc7a33a455f9524e59ef88c23..278b41bb4aa9e2c7a7e6ba31369f0aba8a75908d 100644 (file)
@@ -35,6 +35,9 @@
 #if HAVE_PWD_H
 # include <pwd.h>
 #endif
+#ifdef _OPENMP
+# include <omp.h>
+#endif
 
 #include <textstyle.h>
 
@@ -49,6 +52,7 @@
 #include "c-strstr.h"
 #include "c-strcase.h"
 #include "message.h"
+#include "msgl-merge.h"
 #include "read-catalog-file.h"
 #include "read-po.h"
 #include "read-properties.h"
@@ -74,6 +78,8 @@
 #include "plural-count.h"
 #include "spawn-pipe.h"
 #include "wait-process.h"
+#include "backupfile.h"
+#include "copy-file.h"
 #include "xsetenv.h"
 #include "xstriconv.h"
 #include "str-list.h"
@@ -95,9 +101,6 @@ extern const char * _nl_expand_alias (const char *name);
 /* Locale name.  */
 static const char *locale;
 
-/* Language (ISO-639 code) and optional territory (ISO-3166 code).  */
-static const char *catalogname;
-
 /* Language (ISO-639 code).  */
 static const char *language;
 
@@ -110,7 +113,7 @@ static const char *find_pot (void);
 static const char *catalogname_for_locale (const char *locale);
 static const char *language_of_locale (const char *locale);
 static char *get_field (const char *header, const char *field);
-static msgdomain_list_ty *fill_header (msgdomain_list_ty *mdlp);
+static msgdomain_list_ty *fill_header (msgdomain_list_ty *mdlp, bool fresh);
 static msgdomain_list_ty *update_msgstr_plurals (msgdomain_list_ty *mdlp);
 
 
@@ -306,34 +309,54 @@ This is necessary so you can test your translations.\n"),
 
   /* Default output file name is CATALOGNAME.po.  */
   if (output_file == NULL)
+    output_file = xasprintf ("%s.po", catalogname);
+
+  if (strcmp (output_file, "-") != 0
+      && access (output_file, F_OK) == 0)
     {
-      output_file = xasprintf ("%s.po", catalogname);
+      /* The output PO file already exists.  Assume the translator wants to
+         continue, based on these translations.  */
 
-      /* But don't overwrite existing PO files.  */
-      if (access (output_file, F_OK) == 0)
-        {
-          multiline_error (xstrdup (""),
-                           xasprintf (_("\
-Output file %s already exists.\n\
-Please specify the locale through the --locale option or\n\
-the output .po file through the --output-file option.\n"),
-                                      output_file));
-          exit (EXIT_FAILURE);
-        }
-    }
+      /* First, create a backup file.  */
+      {
+        const char *backup_suffix_string = getenv ("SIMPLE_BACKUP_SUFFIX");
+        if (backup_suffix_string != NULL && backup_suffix_string[0] != '\0')
+          simple_backup_suffix = backup_suffix_string;
+      }
+      {
+        char *backup_file = find_backup_file_name (output_file, simple);
+        xcopy_file_preserving (output_file, backup_file);
+      }
 
-  /* Read input file.  */
-  result = read_catalog_file (input_file, input_syntax);
-  check_pot_charset (result, input_file);
+      /* Initialize OpenMP.  */
+      #ifdef _OPENMP
+      openmp_init ();
+      #endif
 
-  /* Fill the header entry.  */
-  result = fill_header (result);
+      /* Read both files and merge them.  */
+      quiet = true;
+      keep_previous = true;
+      msgdomain_list_ty *def;
+      result = merge (output_file, input_file, input_syntax, &def);
 
-  /* Initialize translations.  */
-  if (strcmp (language, "en") == 0)
-    result = msgdomain_list_english (result);
+      /* Update the header entry.  */
+      result = fill_header (result, false);
+    }
   else
-    result = update_msgstr_plurals (result);
+    {
+      /* Read input file.  */
+      result = read_catalog_file (input_file, input_syntax);
+      check_pot_charset (result, input_file);
+
+      /* Fill the header entry.  */
+      result = fill_header (result, true);
+
+      /* Initialize translations.  */
+      if (strcmp (language, "en") == 0)
+        result = msgdomain_list_english (result);
+      else
+        result = update_msgstr_plurals (result);
+    }
 
   /* Write the modified message list out.  */
   msgdomain_list_print (result, output_file, output_syntax,
@@ -383,7 +406,11 @@ Output file location:\n"));
   -o, --output-file=FILE      write output to specified PO file\n"));
       printf (_("\
 If no output file is given, it depends on the --locale option or the user's\n\
-locale setting.  If it is -, the results are written to standard output.\n"));
+locale setting.\n\
+If the output file already exists, it is merged with the input file,\n\
+as if through '%s'.\n\
+If it is -, the results are written to standard output.\n"),
+              "msgmerge");
       printf ("\n");
       printf (_("\
 Input file syntax:\n"));
@@ -1491,13 +1518,14 @@ plural_forms ()
 }
 
 
-static struct
+struct header_entry_field
 {
   const char *name;
   const char * (*getter0) (void);
   const char * (*getter1) (const char *header);
-}
-fields[] =
+};
+
+static struct header_entry_field fresh_fields[] =
   {
     { "Project-Id-Version", NULL, project_id_version },
     { "PO-Revision-Date", NULL, po_revision_date },
@@ -1509,9 +1537,13 @@ fields[] =
     { "Content-Transfer-Encoding", content_transfer_encoding, NULL },
     { "Plural-Forms", plural_forms, NULL }
   };
+#define FRESH_FIELDS_LAST_TRANSLATOR 2
 
-#define NFIELDS SIZEOF (fields)
-#define FIELD_LAST_TRANSLATOR 2
+static struct header_entry_field update_fields[] =
+  {
+    { "Last-Translator", last_translator, NULL }
+  };
+#define UPDATE_FIELDS_LAST_TRANSLATOR 0
 
 
 /* Retrieve a freshly allocated copy of a field's value.  */
@@ -1763,7 +1795,7 @@ subst_string_list (string_list_ty *slp,
 
 /* Fill the templates in all fields of the header entry.  */
 static msgdomain_list_ty *
-fill_header (msgdomain_list_ty *mdlp)
+fill_header (msgdomain_list_ty *mdlp, bool fresh)
 {
   /* Determine the desired encoding to the PO file.
      If the POT file contains charset=UTF-8, it means that the POT file
@@ -1813,10 +1845,25 @@ fill_header (msgdomain_list_ty *mdlp)
 
   /* Cache the strings filled in, for use when there are multiple domains
      and a header entry for each domain.  */
-  const char *field_value[NFIELDS];
+  struct header_entry_field *fields;
+  size_t nfields;
+  size_t field_last_translator;
+  if (fresh)
+    {
+      fields = fresh_fields;
+      nfields = SIZEOF (fresh_fields);
+      field_last_translator = FRESH_FIELDS_LAST_TRANSLATOR;
+    }
+  else
+    {
+      fields = update_fields;
+      nfields = SIZEOF (update_fields);
+      field_last_translator = UPDATE_FIELDS_LAST_TRANSLATOR;
+    }
+  const char **field_value = XNMALLOC (nfields, const char *);
   size_t i;
 
-  for (i = 0; i < NFIELDS; i++)
+  for (i = 0; i < nfields; i++)
     field_value[i] = NULL;
 
   for (k = 0; k < mdlp->nitems; k++)
@@ -1848,7 +1895,7 @@ fill_header (msgdomain_list_ty *mdlp)
           header = xstrdup (header_mp->msgstr);
 
           /* Fill in the fields.  */
-          for (i = 0; i < NFIELDS; i++)
+          for (i = 0; i < nfields; i++)
             {
               if (field_value[i] == NULL)
                 field_value[i] =
@@ -1881,7 +1928,7 @@ fill_header (msgdomain_list_ty *mdlp)
               subst[1][0] = "PACKAGE";
               subst[1][1] = id;
               subst[2][0] = "FIRST AUTHOR <EMAIL@ADDRESS>";
-              subst[2][1] = field_value[FIELD_LAST_TRANSLATOR];
+              subst[2][1] = field_value[field_last_translator];
               subst[3][0] = "YEAR";
               subst[3][1] =
                 xasprintf ("%d",
@@ -1894,6 +1941,8 @@ fill_header (msgdomain_list_ty *mdlp)
         }
     }
 
+  free (field_value);
+
   return mdlp;
 }