From 092e5329e614c7d9c70673cdafbbd71629b47fc4 Mon Sep 17 00:00:00 2001 From: Bruno Haible Date: Sun, 12 Oct 2025 04:00:26 +0200 Subject: [PATCH] msginit: Produce a merged PO file instead of failing. * gettext-tools/src/msginit.c: Include , 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 | 6 ++ gettext-tools/doc/gettext.texi | 32 +++++++-- gettext-tools/doc/msginit.texi | 10 ++- gettext-tools/src/Makefile.am | 4 +- gettext-tools/src/msginit.c | 123 +++++++++++++++++++++++---------- 5 files changed, 127 insertions(+), 48 deletions(-) diff --git a/NEWS b/NEWS index 518b2a957..730c97f8c 100644 --- 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. diff --git a/gettext-tools/doc/gettext.texi b/gettext-tools/doc/gettext.texi index 9571c6070..7b31eeceb 100644 --- a/gettext-tools/doc/gettext.texi +++ b/gettext-tools/doc/gettext.texi @@ -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 diff --git a/gettext-tools/doc/msginit.texi b/gettext-tools/doc/msginit.texi index 74b143bdf..419bd41fa 100644 --- a/gettext-tools/doc/msginit.texi +++ b/gettext-tools/doc/msginit.texi @@ -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 diff --git a/gettext-tools/src/Makefile.am b/gettext-tools/src/Makefile.am index 797801e1c..1bcebf691 100644 --- a/gettext-tools/src/Makefile.am +++ b/gettext-tools/src/Makefile.am @@ -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++. diff --git a/gettext-tools/src/msginit.c b/gettext-tools/src/msginit.c index 482b110f0..278b41bb4 100644 --- a/gettext-tools/src/msginit.c +++ b/gettext-tools/src/msginit.c @@ -35,6 +35,9 @@ #if HAVE_PWD_H # include #endif +#ifdef _OPENMP +# include +#endif #include @@ -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 "; - 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; } -- 2.47.3