msginit: Produce a merged PO file instead of failing.

* 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.
This commit is contained in:
Bruno Haible 2025-10-12 04:00:26 +02:00
parent e6bff68d9d
commit 092e5329e6
5 changed files with 129 additions and 50 deletions

6
NEWS
View File

@ -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.

View 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

View 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

View 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++.

View 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);
}
/* Initialize OpenMP. */
#ifdef _OPENMP
openmp_init ();
#endif
/* Read both files and merge them. */
quiet = true;
keep_previous = true;
msgdomain_list_ty *def;
result = merge (output_file, input_file, input_syntax, &def);
/* Update the header entry. */
result = fill_header (result, false);
}
/* 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);
/* Initialize translations. */
if (strcmp (language, "en") == 0)
result = msgdomain_list_english (result);
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;
}