[PATCH v17 3/9] argp: document translated names in --help and --usage

Vivien Kraus vivien@planete-kraus.eu
Tue Nov 18 21:22:31 GMT 2025


They are displayed in the --usage and --help output as the main option
name, and the untranslated name is also displayed in parenthesis, so
that someone reading a script (presumably with untranslated option
names) can then figure out which translated option name it relates
to.

This help message processing uses dynamic memory allocation.  If
malloc fails, the translated option name will not be displayed.

A translation context is required for translated options processing.
However, its configuration would be most likely expected to be in the
struct argp definition, but it would change this struct type.  The
easiest solution is to use a fixed context.

Since the configuration of the translation domain is on a per-parser
basis, and getopt_long is called with the union of all
options (including options from children parsers), then all option
names in all parsers must be in the same domain.  However, it makes
sense to use documentation from different domains.  Thus, the
translations for option names will all be searched in the current
textdomain, while documentation will respect the parser’s domain.
---
 argp/Makefile                  |  14 +++++
 argp/argp-help.c               |  63 +++++++++++++++++---
 argp/argp-parse.c              |   1 +
 argp/tst-argphelp-localized.c  | 102 +++++++++++++++++++++++++++++++++
 argp/tst-argphelp-localized.po |  18 ++++++
 argp/tst-argpusage-localized.c |  82 ++++++++++++++++++++++++++
 manual/argp.texi               |  22 ++++---
 7 files changed, 286 insertions(+), 16 deletions(-)
 create mode 100644 argp/tst-argphelp-localized.c
 create mode 100644 argp/tst-argphelp-localized.po
 create mode 100644 argp/tst-argpusage-localized.c

diff --git a/argp/Makefile b/argp/Makefile
index 65d917ad14..ff4b6d9593 100644
--- a/argp/Makefile
+++ b/argp/Makefile
@@ -44,6 +44,8 @@ tests = \
   bug-argp2 \
   tst-argp1 \
   tst-argp2 \
+  tst-argphelp-localized \
+  tst-argpusage-localized \
   tst-ldbl-argp \
   # tests
 
@@ -51,6 +53,18 @@ CFLAGS-argp-help.c += $(uses-callbacks) -fexceptions
 CFLAGS-argp-parse.c += $(uses-callbacks)
 CFLAGS-argp-fmtstream.c += -fexceptions
 
+tst_argphelp_localized_mo = $(objpfx)domaindir/en_GB/LC_MESSAGES/tst-argphelp-localized.mo
+
+$(tst_argphelp_localized_mo): tst-argphelp-localized.po
+	$(make-target-directory)
+	msgfmt -o $@T $<
+	mv -f $@T $@
+
+$(objpfx)tst-argphelp-localized.out: $(tst_argphelp_localized_mo)
+$(objpfx)tst-argpusage-localized.out: $(tst_argphelp_localized_mo)
+CFLAGS-tst-argphelp-localized.c += -DOBJPFX=\"$(objpfx)\"
+CFLAGS-tst-argpusage-localized.c += -DOBJPFX=\"$(objpfx)\"
+
 bug-argp1-ARGS = -- --help
 bug-argp2-ARGS = -- -d 111 --dstaddr 222 -p 333 --peer 444
 
diff --git a/argp/argp-help.c b/argp/argp-help.c
index 3fe3a244ce..672e733fc8 100644
--- a/argp/argp-help.c
+++ b/argp/argp-help.c
@@ -1213,6 +1213,8 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,
   unsigned num;
   const struct argp_option *real = entry->opt, *opt;
   char *so = entry->short_options;
+  char *option_msgid;
+  const char *translated_option_name;
   int have_long_opt = 0;	/* We have any long options.  */
   /* Saved margins.  */
   int old_lm = __argp_fmtstream_set_lmargin (stream, 0);
@@ -1276,9 +1278,25 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,
 	if (opt->name && ovisible (opt))
 	  {
 	    comma (uparams.long_opt_col, &pest);
-	    __argp_fmtstream_printf (stream, "--%s", opt->name);
+            option_msgid =
+              malloc (strlen ("command-line option\004")
+                      + strlen (opt->name) + 1);
+            if (option_msgid)
+              {
+                strcpy (option_msgid, "command-line option\004");
+                strcat (option_msgid, opt->name);
+                translated_option_name = gettext (option_msgid);
+                if (!strcmp (translated_option_name, option_msgid))
+                  translated_option_name = opt->name;
+              }
+            else
+              translated_option_name = opt->name;
+	    __argp_fmtstream_printf (stream, "--%s", translated_option_name);
 	    arg (real, "=%s", "[=%s]",
 		 state == NULL ? NULL : state->root_argp->argp_domain, stream);
+            if (strcmp (translated_option_name, opt->name))
+              __argp_fmtstream_printf (stream, " (--%s)", opt->name);
+            free (option_msgid);
 	  }
     }
 
@@ -1420,6 +1438,8 @@ usage_long_opt (const struct argp_option *opt,
 {
   argp_fmtstream_t stream = cookie;
   const char *arg = opt->arg;
+  char *option_msgid;
+  const char *translated_opt_name = opt->name;
   int flags = opt->flags | real->flags;
 
   if (! arg)
@@ -1427,16 +1447,41 @@ usage_long_opt (const struct argp_option *opt,
 
   if (! (flags & OPTION_NO_USAGE))
     {
+      /* Since we cannot customize the translation context, we will
+         use a default one.  FIXME: use pgettext_expr(). */
+      static const char *default_context = "command-line option\004";
+      option_msgid = malloc (strlen (default_context) + strlen (opt->name) + 1);
+      translated_opt_name = NULL;
+      if (option_msgid)
+        {
+          strcpy (option_msgid, default_context);
+          strcat (option_msgid, opt->name);
+          translated_opt_name = gettext (option_msgid);
+          if (!strcmp (translated_opt_name, option_msgid))
+            translated_opt_name = opt->name;
+        }
+      if (!strcmp (translated_opt_name, opt->name))
+        translated_opt_name = NULL;
       if (arg)
-	{
-	  arg = dgettext (domain, arg);
-	  if (flags & OPTION_ARG_OPTIONAL)
-	    __argp_fmtstream_printf (stream, " [--%s[=%s]]", opt->name, arg);
-	  else
-	    __argp_fmtstream_printf (stream, " [--%s=%s]", opt->name, arg);
-	}
+        {
+          arg = dgettext (domain, arg);
+          if ((flags & OPTION_ARG_OPTIONAL) && translated_opt_name)
+            __argp_fmtstream_printf (stream, " [--%s[=%s] (--%s)]",
+                                     translated_opt_name, arg, opt->name);
+          else if (flags & OPTION_ARG_OPTIONAL)
+            __argp_fmtstream_printf (stream, " [--%s[=%s]]", opt->name, arg);
+          else if (translated_opt_name)
+            __argp_fmtstream_printf (stream, " [--%s=%s (--%s)]",
+                                     translated_opt_name, arg, opt->name);
+          else
+            __argp_fmtstream_printf (stream, " [--%s=%s]", opt->name, arg);
+        }
+      else if (translated_opt_name)
+        __argp_fmtstream_printf (stream, " [--%s (--%s)]",
+                                 translated_opt_name, opt->name);
       else
-	__argp_fmtstream_printf (stream, " [--%s]", opt->name);
+        __argp_fmtstream_printf (stream, " [--%s]", opt->name);
+      free (option_msgid);
     }
 
   return 0;
diff --git a/argp/argp-parse.c b/argp/argp-parse.c
index 99f8d9ecd4..90525cdf61 100644
--- a/argp/argp-parse.c
+++ b/argp/argp-parse.c
@@ -472,6 +472,7 @@ parser_init (struct parser *parser, const struct argp *argp,
   struct parser_sizes szs;
   struct _getopt_data opt_data = _GETOPT_DATA_INITIALIZER;
 
+  opt_data.optctxt = "command-line option";
   szs.short_len = (flags & ARGP_NO_ARGS) ? 0 : 1;
   szs.long_len = 0;
   szs.num_groups = 0;
diff --git a/argp/tst-argphelp-localized.c b/argp/tst-argphelp-localized.c
new file mode 100644
index 0000000000..abc3325827
--- /dev/null
+++ b/argp/tst-argphelp-localized.c
@@ -0,0 +1,102 @@
+/* Test program for argp argument parser
+   Copyright (C) 2025 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library 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
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <time.h>
+#include <string.h>
+#include <argp.h>
+#include <libintl.h>
+#include <locale.h>
+#include <unistd.h>
+
+#include <support/support.h>
+
+#define PN_(ctxt, str) (str)
+
+const char *argp_program_version = "argphelp-test 1.0";
+
+struct argp_option options[] =
+{
+  {PN_ ("command-line option", "color"), 'c', 0, 0, "Rainbow!"},
+  {0}
+};
+
+static int color_set = 0;
+
+static error_t
+parse_opt (int key, char *arg, struct argp_state *state)
+{
+  (void) state;
+  if (key == 'c' && color_set)
+    {
+      fprintf (stderr, "%s:%d: color already set.\n", __FILE__, __LINE__);
+      abort ();
+    }
+  else if (key == 'c')
+    {
+      color_set = 1;
+    }
+  return 0;
+}
+
+static struct argp argp = { options, parse_opt };
+

+int
+main (int argc, char *argv[])
+{
+  char *test1_argv[3] =
+    { (char *) "/bin/tst-argphelp-localized", (char *) "--colour", NULL };
+  char *test2_argv[3] =
+    { (char *) "/bin/tst-argphelp-localized", (char *) "--help", NULL };
+
+  unsetenv ("LANGUAGE");
+  setlocale (LC_ALL, "en_GB.UTF-8");
+  if (bindtextdomain ("tst-argphelp-localized", OBJPFX "domaindir") == NULL)
+    {
+      fprintf (stderr, "%s:%d: cannot call bindtextdomain.\n",
+               __FILE__, __LINE__);
+      abort ();
+    }
+  if (textdomain ("tst-argphelp-localized") == NULL)
+    {
+      fprintf (stderr, "%s:%d: cannot call textdomain.\n",
+               __FILE__, __LINE__);
+      abort ();
+    }
+  /* Check that the catalog is OK: */
+  if (strcmp (gettext ("command-line option\004color"), "colour") != 0)
+    {
+      fprintf (stderr, "%s:%d: the mo file does not work.\n",
+               __FILE__, __LINE__);
+      abort ();
+    }
+  argp_parse (&argp, 2, test1_argv, 0, 0, NULL);
+  if (!color_set)
+    {
+      fprintf (stderr, "%s:%d: color not set.\n", __FILE__, __LINE__);
+      abort ();
+    }
+  argp_parse (&argp, 2, test2_argv, 0, 0, NULL);
+  fprintf (stderr, "%s:%d: --help did not exit the program.\n", __FILE__, __LINE__);
+  abort ();
+  return 0;
+}
diff --git a/argp/tst-argphelp-localized.po b/argp/tst-argphelp-localized.po
new file mode 100644
index 0000000000..4e301bf278
--- /dev/null
+++ b/argp/tst-argphelp-localized.po
@@ -0,0 +1,18 @@
+# English translations for a GNU C Library test.
+# This file is distributed under the same license as the GNU C Library.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: tst-argphelp-localized\n"
+"Report-Msgid-Bugs-To: \n"
+"Language-Team: English (British) <(nothing)>\n"
+"Language: en_GB\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: tst-argphelp-localized.c:73
+msgctxt "command-line option"
+msgid "color"
+msgstr "colour"
\ No newline at end of file
diff --git a/argp/tst-argpusage-localized.c b/argp/tst-argpusage-localized.c
new file mode 100644
index 0000000000..4061609fc3
--- /dev/null
+++ b/argp/tst-argpusage-localized.c
@@ -0,0 +1,82 @@
+/* Test program for argp argument parser
+   Copyright (C) 2025 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library 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
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <time.h>
+#include <string.h>
+#include <argp.h>
+#include <libintl.h>
+#include <locale.h>
+#include <unistd.h>
+
+#include <support/support.h>
+
+#define PN_(ctxt, str) (str)
+
+const char *argp_program_version = "argpusage-test 1.0";
+
+struct argp_option options[] =
+{
+  {PN_ ("command-line option", "color"), 'c', 0, 0, "Rainbow!"},
+  {0}
+};
+
+static error_t
+parse_opt (int key, char *arg, struct argp_state *state)
+{
+  return 0;
+}
+
+static struct argp argp = { options, parse_opt };
+

+int
+main (int argc, char *argv[])
+{
+  char *test_argv[3] =
+    { (char *) "/bin/tst-argpusage-localized", (char *) "--usage", NULL };
+
+  unsetenv ("LANGUAGE");
+  setlocale (LC_ALL, "en_GB.UTF-8");
+  if (bindtextdomain ("tst-argphelp-localized", OBJPFX "domaindir") == NULL)
+    {
+      fprintf (stderr, "%s:%d: cannot call bindtextdomain.\n",
+               __FILE__, __LINE__);
+      abort ();
+    }
+  if (textdomain ("tst-argphelp-localized") == NULL)
+    {
+      fprintf (stderr, "%s:%d: cannot call textdomain.\n",
+               __FILE__, __LINE__);
+      abort ();
+    }
+  /* Check that the catalog is OK: */
+  if (strcmp (gettext ("command-line option\004color"), "colour") != 0)
+    {
+      fprintf (stderr, "%s:%d: the mo file does not work.\n",
+               __FILE__, __LINE__);
+      abort ();
+    }
+  argp_parse (&argp, 2, test_argv, 0, 0, NULL);
+  fprintf (stderr, "%s:%d: --usage did not exit the program.\n", __FILE__, __LINE__);
+  abort ();
+  return 0;
+}
diff --git a/manual/argp.texi b/manual/argp.texi
index 0023441812..97456ef20e 100644
--- a/manual/argp.texi
+++ b/manual/argp.texi
@@ -206,8 +206,10 @@ messages.  @xref{Argp Help Filtering}.
 
 @item const char *argp_domain
 If non-zero, the strings used in the argp library are translated using
-the domain described by this string.  If zero, the current default domain
-is used.
+the domain described by this string.  If zero, the current default
+domain is used.  The long option names are always translated with the
+current default domain, and with the @samp{"command-line option"}
+disambiguation string.
 
 @end table
 @end deftp
@@ -233,7 +235,9 @@ beginning, the unused fields left unspecified.
 The @code{options} field in a @code{struct argp} points to a vector of
 @code{struct argp_option} structures, each of which specifies an option
 that the argp parser supports.  Multiple entries may be used for a single
-option provided it has multiple names.  This should be terminated by an
+option provided it has multiple names.  In any case, option names are
+translated, so either the translated or untranslated form is
+recognized for each option.  This should be terminated by an
 entry with zero in all fields.  Note that when using an initialized C
 array for options, writing @code{@{ 0 @}} is enough to achieve this.
 
@@ -247,9 +251,12 @@ the following fields:
 @item const char *name
 The long name for this option, corresponding to the long option
 @samp{--@var{name}}; this field may be zero if this option @emph{only}
-has a short name.  To specify multiple names for an option, additional
-entries may follow this one, with the @code{OPTION_ALIAS} flag
-set.  @xref{Argp Option Flags}.
+has a short name.  You should mark this string for translation with
+the fixed @samp{"command-line option"} context.  To specify multiple
+names for an option, additional entries may follow this one, with the
+@code{OPTION_ALIAS} flag set.  @xref{Argp Option Flags}.  Translations
+are added automatically, it is not necessary to use an alias for
+translations.
 
 @item int key
 The integer key provided by the current option to the option parser.  If
@@ -323,7 +330,8 @@ This option isn't displayed in any help messages.
 This option is an alias for the closest previous non-alias option.  This
 means that it will be displayed in the same help entry, and will inherit
 fields other than @code{name} and @code{key} from the option being
-aliased.
+aliased.  It is not necessary to list the translation of an option
+name as an alias.
 
 
 @item OPTION_DOC
-- 
2.34.1



More information about the Libc-alpha mailing list