get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/1.1/patches/2227403/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 2227403,
    "url": "http://patchwork.ozlabs.org/api/1.1/patches/2227403/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/glibc/patch/450db6bd8f93d16aa277d771bfebd4118baf3df8.1776957778.git.vivien@planete-kraus.eu/",
    "project": {
        "id": 41,
        "url": "http://patchwork.ozlabs.org/api/1.1/projects/41/?format=api",
        "name": "GNU C Library",
        "link_name": "glibc",
        "list_id": "libc-alpha.sourceware.org",
        "list_email": "libc-alpha@sourceware.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<450db6bd8f93d16aa277d771bfebd4118baf3df8.1776957778.git.vivien@planete-kraus.eu>",
    "date": "2026-04-23T16:04:07",
    "name": "[v22,9/9] posix, argp: Support multiple long option name translations",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "87d55de7f0621c60ffcb0502cd3b10bd80c8952e",
    "submitter": {
        "id": 90948,
        "url": "http://patchwork.ozlabs.org/api/1.1/people/90948/?format=api",
        "name": "Vivien Kraus",
        "email": "vivien@planete-kraus.eu"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/glibc/patch/450db6bd8f93d16aa277d771bfebd4118baf3df8.1776957778.git.vivien@planete-kraus.eu/mbox/",
    "series": [
        {
            "id": 501215,
            "url": "http://patchwork.ozlabs.org/api/1.1/series/501215/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/glibc/list/?series=501215",
            "date": "2026-04-23T16:03:58",
            "name": "Support translated long option names in getopt and argp",
            "version": 22,
            "mbox": "http://patchwork.ozlabs.org/series/501215/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/2227403/comments/",
    "check": "pending",
    "checks": "http://patchwork.ozlabs.org/api/patches/2227403/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<libc-alpha-bounces~incoming=patchwork.ozlabs.org@sourceware.org>",
        "X-Original-To": [
            "incoming@patchwork.ozlabs.org",
            "libc-alpha@sourceware.org"
        ],
        "Delivered-To": [
            "patchwork-incoming@legolas.ozlabs.org",
            "libc-alpha@sourceware.org"
        ],
        "Authentication-Results": [
            "legolas.ozlabs.org;\n\tdkim=pass (2048-bit key;\n secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu\n header.a=rsa-sha1 header.s=albinoniA header.b=QIhMstwb;\n\tdkim-atps=neutral",
            "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=sourceware.org\n (client-ip=2620:52:6:3111::32; helo=vm01.sourceware.org;\n envelope-from=libc-alpha-bounces~incoming=patchwork.ozlabs.org@sourceware.org;\n receiver=patchwork.ozlabs.org)",
            "sourceware.org;\n\tdkim=pass (2048-bit key,\n secure) header.d=planete-kraus.eu header.i=@planete-kraus.eu\n header.a=rsa-sha1 header.s=albinoniA header.b=QIhMstwb",
            "sourceware.org; dmarc=pass (p=reject dis=none)\n header.from=planete-kraus.eu",
            "sourceware.org;\n spf=pass smtp.mailfrom=planete-kraus.eu",
            "server2.sourceware.org;\n arc=none smtp.remote-ip=89.234.140.182"
        ],
        "Received": [
            "from vm01.sourceware.org (vm01.sourceware.org\n [IPv6:2620:52:6:3111::32])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4g1gwR6Nmfz1y2d\n\tfor <incoming@patchwork.ozlabs.org>; Fri, 24 Apr 2026 02:07:35 +1000 (AEST)",
            "from vm01.sourceware.org (localhost [127.0.0.1])\n\tby sourceware.org (Postfix) with ESMTP id DA5E44BB3BD0\n\tfor <incoming@patchwork.ozlabs.org>; Thu, 23 Apr 2026 16:07:33 +0000 (GMT)",
            "from planete-kraus.eu (planete-kraus.eu [89.234.140.182])\n by sourceware.org (Postfix) with ESMTPS id D11DB4B87BAB\n for <libc-alpha@sourceware.org>; Thu, 23 Apr 2026 16:05:59 +0000 (GMT)",
            "from planete-kraus.eu (localhost [127.0.0.1])\n by planete-kraus.eu (OpenSMTPD) with ESMTP id c0ab0975;\n Thu, 23 Apr 2026 16:05:43 +0000 (UTC)",
            "by planete-kraus.eu (OpenSMTPD) with ESMTPSA id fdbe342a\n (TLSv1.3:TLS_CHACHA20_POLY1305_SHA256:256:NO);\n Thu, 23 Apr 2026 16:05:42 +0000 (UTC)"
        ],
        "DKIM-Filter": [
            "OpenDKIM Filter v2.11.0 sourceware.org DA5E44BB3BD0",
            "OpenDKIM Filter v2.11.0 sourceware.org D11DB4B87BAB"
        ],
        "DMARC-Filter": "OpenDMARC Filter v1.4.2 sourceware.org D11DB4B87BAB",
        "ARC-Filter": "OpenARC Filter v1.0.0 sourceware.org D11DB4B87BAB",
        "ARC-Seal": "i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1776960360; cv=none;\n b=UKPJtkYs/f+29v7zlrXScn2aOU1IMa2CXpqKAJ+mi2vg/PUdItda7Rcpl+svn1V0sq2HAfKuwNzT0ovGCM8fYwvuuFYOX/s8Ri32F7ZCgyquYAe7soE8yH6lGj6Q9PVAyGbiVvHNYNCOGuAmOCjoJUrFgYIwhUAHO3RYQmWxcI0=",
        "ARC-Message-Signature": "i=1; a=rsa-sha256; d=sourceware.org; s=key;\n t=1776960360; c=relaxed/simple;\n bh=6ymLAM/23KYyfPgDZWCFrAMCfsdGFieFnkh9RN/ocOk=;\n h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version;\n b=xifGoYwVEcoOMQWp8SDtQdXNorsH7VCqtXMh6VDHCOAKIM1shAP8Ycb7Ol4VlxjpOLFVrBoTSlBPLS5sK6MV9KBhCPozBfyc/WQ1Xe4C6opofGq4bj38sL4TKv5x83IwyQERbL1doEMGZk9wzY/qEeUcYraMouWBvRoKWwDeEQ4=",
        "ARC-Authentication-Results": "i=1; server2.sourceware.org",
        "DKIM-Signature": "v=1; a=rsa-sha1; c=relaxed; d=planete-kraus.eu; h=from\n :to:cc:subject:date:message-id:in-reply-to:references\n :mime-version:content-type:content-transfer-encoding; s=\n albinoniA; bh=adNwM/4Gy4Mf6b5oCxAs88zBKo4=; b=QIhMstwboOcMxJXqJ5\n t+FPUV3WBPNbhaDFlThBY/JG13YCkqopNWr8kzgD3zhZhIlEyFIXM+BieXp0Oa1j\n BIN0T6CtaE2YOcmJmR4901SpOGHzhwiMGSmPwaX8r0VkiYlNc6qE3kLIBEYCNN+x\n KeKPDP5WJcz9tuWiuNPHDRyGtlckMlRhSlgCXaSjdUf6BoECXm3cgs8cS1GN614T\n Cm5yNIKMYrSovkhbx4xD1WYFRM968dE1xmk7Mek6Ge7VCLh3HTS5sWn622EG9GgM\n ArX9bHh5Q0VhBaFncAKY7WAuRZotVfjILgPbi+LLofyRsrl5Fs4ym24tNWAHb7yn\n kKFg==",
        "From": "Vivien Kraus <vivien@planete-kraus.eu>",
        "To": "adhemerval.zanella@linaro.org,\n\tlibc-alpha@sourceware.org",
        "Cc": "Vivien Kraus <vivien@planete-kraus.eu>",
        "Subject": "[PATCH v22 9/9] posix,\n argp: Support multiple long option name translations",
        "Date": "Thu, 23 Apr 2026 18:04:07 +0200",
        "Message-ID": "\n <450db6bd8f93d16aa277d771bfebd4118baf3df8.1776957778.git.vivien@planete-kraus.eu>",
        "X-Mailer": "git-send-email 2.52.0",
        "In-Reply-To": "<cover.1776957777.git.vivien@planete-kraus.eu>",
        "References": "<cover.1776957777.git.vivien@planete-kraus.eu>",
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=UTF-8",
        "Content-Transfer-Encoding": "8bit",
        "X-BeenThere": "libc-alpha@sourceware.org",
        "X-Mailman-Version": "2.1.30",
        "Precedence": "list",
        "List-Id": "Libc-alpha mailing list <libc-alpha.sourceware.org>",
        "List-Unsubscribe": "<https://sourceware.org/mailman/options/libc-alpha>,\n <mailto:libc-alpha-request@sourceware.org?subject=unsubscribe>",
        "List-Archive": "<https://sourceware.org/pipermail/libc-alpha/>",
        "List-Post": "<mailto:libc-alpha@sourceware.org>",
        "List-Help": "<mailto:libc-alpha-request@sourceware.org?subject=help>",
        "List-Subscribe": "<https://sourceware.org/mailman/listinfo/libc-alpha>,\n <mailto:libc-alpha-request@sourceware.org?subject=subscribe>",
        "Errors-To": "libc-alpha-bounces~incoming=patchwork.ozlabs.org@sourceware.org"
    },
    "content": "There are programs that have different options having the same\nbehavior.  For instance, emacs: the --eval and --execute options have\nthe same behavior.  So, if Emacs were written in a different language\nthan English where “foo” translates both to “eval” and “execute”, then\nthe English translator would have a hard time deciding between “eval”\nand “execute” for the translation of the only option --foo.\n\nOther example I’ve found: bash --init-file / --rcfile.\n\nIn conclusion: the translator lacks the liberty to name equivalent\nbehaviors differently, and this liberty is sometimes useful.\n\nIn this patch, the translation for an option name is expected to be a\nspace-separated list of equally accepted translations of a\ncommand-line option.  I chose a space because it would be a bad choice\nto have a space character in an option name anyway.\n\nThe translation type is extended to have a list of translations.  We\nchange the following behaviors:\n1. Argument matching tries any translation;\n2. Collision detection searches collisions with all\ntranslations;\n3. Argp --help / --usage now lists all translations, but the argument\nplaceholder is not repeated.\n---\n argp/argp-help.c                              | 170 ++++++++++++------\n argp/tst-argphelp-localized.c                 |  55 +++++-\n argp/tst-argphelp-localized.po                |   3 +-\n argp/tst-argpusage-localized.c                |   3 +-\n manual/getopt.texi                            |   5 +\n posix/check-getopt-translations.pl            |  26 +--\n posix/getopt.c                                | 136 ++++++++++++--\n .../standalone-multiple-getopt-collisions.po  |   6 +-\n posix/tst-getopt_long_collision.c             |  10 +-\n posix/tst-getopt_long_collision.po            |   8 +-\n posix/tstgetoptl.c                            |  34 +++-\n posix/tstgetoptl.po                           |   4 +-\n 12 files changed, 355 insertions(+), 105 deletions(-)",
    "diff": "diff --git a/argp/argp-help.c b/argp/argp-help.c\nindex aad5d7be13..e748e34258 100644\n--- a/argp/argp-help.c\n+++ b/argp/argp-help.c\n@@ -1205,36 +1205,81 @@ comma (unsigned col, struct pentry_state *pest)\n   indent_to (pest->stream, col);\n }\n \f\n-/* Help and usage output show the translated option name.  *allocated\n-   holds a pointer that should be freed by the caller, or a NULL\n-   pointer.  */\n-static const char *\n-translate_option_name (const char *name, char **allocated)\n+/* Help and usage output show the translated option name.  Since an\n+   option name may have multiple translations, we return all of them.\n+   The result is to be freed by the caller, each element of the array\n+   first, and then the array itself.  */\n+static char **\n+translate_option_name (const char *name)\n {\n   /* Argp does not have a configuration for the context, so a default\n      one is used.  */\n+  char *msgid = NULL;\n+  const char *full_translation = NULL;\n+  size_t n_translations;\n+  char **results = NULL;\n   /* FIXME: use pgettext_expr.  */\n-  *allocated = NULL;\n   if (__libc_enable_secure)\n     /* Translations are disabled.  */\n-    return name;\n-  if (__asprintf (allocated, \"command-line option\\004%s\", name) == -1)\n     {\n-      /* *allocated is NULL */\n-      return name;\n+      results = calloc (2, sizeof (char *));\n+      if (results != NULL)\n+\t{\n+\t  results[0] = __strdup (name);\n+\t  if (results[0] == NULL)\n+\t    {\n+\t      free (results);\n+\t      results = NULL;\n+\t    }\n+\t}\n+      return results;\n     }\n-  const char *translated = gettext (*allocated);\n-  if (strcmp (translated, *allocated) == 0)\n+  if (__asprintf (&msgid, \"command-line option\\004%s\", name) == -1)\n+    /* Do not bother trying to strdup name.  */\n+    return NULL;\n+  full_translation = gettext (msgid);\n+  if (strcmp (full_translation, msgid) == 0)\n+    full_translation = name;\n+  /* Split full_translation into results.  Do it in 2 passes: first\n+     count, then copy.  */\n+  for (int pass = 0; pass < 2; pass++)\n     {\n-      /* No translation performed.  */\n-      free (*allocated);\n-      *allocated = NULL;\n-      return name;\n+      n_translations = 0;\n+      const char *start = full_translation;\n+      const char *end = NULL;\n+      while (start != NULL)\n+\t{\n+\t  end = strchr (start, ' ');\n+\t  if (pass == 1)\n+\t    {\n+\t      if (end == NULL)\n+\t\tresults[n_translations] = __strdup (start);\n+\t      else\n+\t\tresults[n_translations] = __strndup (start, end - start);\n+\t      if (results[n_translations] == NULL)\n+\t\t{\n+\t\t  /* Abort.  */\n+\t\t  for (size_t i = 0; i < n_translations; i++)\n+\t\t    free (results[i]);\n+\t\t  free (results);\n+\t\t  return NULL;\n+\t\t}\n+\t    }\n+\t  start = end;\n+\t  if (start != NULL)\n+\t    /* Skip ' ' */\n+\t    start++;\n+\t  n_translations++;\n+\t}\n+      if (pass == 0)\n+\tresults = calloc (n_translations + 1, sizeof (char *));\n+      if (results == NULL)\n+\treturn NULL;\n     }\n-  /* FIXME: is it safe to discard *allocated early here?  Won’t the\n-     return value alias it? */\n-  /* *allocated is to be freed by the caller.  */\n-  return translated;\n+  /* We did not touch that index, and allocated the array with\n+     calloc.  */\n+  assert (results[n_translations] == NULL);\n+  return results;\n }\n \f\n /* Print help for ENTRY to STREAM.  */\n@@ -1245,7 +1290,7 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,\n   unsigned num;\n   const struct argp_option *real = entry->opt, *opt;\n   char *so = entry->short_options;\n-  const char *translated_option_name;\n+  char **translated_option_names;\n   int have_long_opt = 0;\t/* We have any long options.  */\n   /* Saved margins.  */\n   int old_lm = __argp_fmtstream_set_lmargin (stream, 0);\n@@ -1304,19 +1349,35 @@ hol_entry_help (struct hol_entry *entry, const struct argp_state *state,\n   else\n     /* A real long option.  */\n     {\n+      bool needs_untranslated = true;\n       __argp_fmtstream_set_wmargin (stream, uparams.long_opt_col);\n       for (opt = real, num = entry->num; num > 0; opt++, num--)\n \tif (opt->name && ovisible (opt))\n \t  {\n \t    comma (uparams.long_opt_col, &pest);\n-\t    char *name_allocated = NULL;\n-\t    translated_option_name = translate_option_name (opt->name, &name_allocated);\n-\t    __argp_fmtstream_printf (stream, \"--%s\", translated_option_name);\n-\t    arg (real, \"=%s\", \"[=%s]\",\n-\t\t state == NULL ? NULL : state->root_argp->argp_domain, stream);\n-\t    if (strcmp (translated_option_name, opt->name))\n+\t    translated_option_names = translate_option_name (opt->name);\n+\t    for (size_t i = 0;\n+\t\t (translated_option_names != NULL\n+\t\t  && translated_option_names[i] != NULL);\n+\t\t i++)\n+\t      {\n+\t\tif (i != 0)\n+\t\t  __argp_fmtstream_printf (stream, \", \");\n+\t\t__argp_fmtstream_printf (stream, \"--%s\", translated_option_names[i]);\n+\t\t/* Only display the argument for the first translation.  */\n+\t\tif (i == 0)\n+\t\t  arg (real, \"=%s\", \"[=%s]\",\n+\t\t       state == NULL ? NULL : state->root_argp->argp_domain, stream);\n+\t\t/* If we see the untranslated name, we won’t repeat it.  */\n+\t\tif (strcmp (translated_option_names[i], opt->name) == 0)\n+\t\t  needs_untranslated = false;\n+\t\tfree (translated_option_names[i]);\n+\t      }\n+\t    free (translated_option_names);\n+\t    if (needs_untranslated)\n \t      __argp_fmtstream_printf (stream, \" (--%s)\", opt->name);\n-\t    free (name_allocated);\n+\t    /* If memory allocation failed, the --help output will\n+\t       just display the untranslated name in parenthesis.  */\n \t  }\n     }\n \n@@ -1458,39 +1519,46 @@ usage_long_opt (const struct argp_option *opt,\n {\n   argp_fmtstream_t stream = cookie;\n   const char *arg = opt->arg;\n-  const char *translated_option_name = opt->name;\n+  char **translated_option_names = NULL;\n   int flags = opt->flags | real->flags;\n+  bool needs_untranslated = true;\n \n   if (! arg)\n     arg = real->arg;\n \n   if (! (flags & OPTION_NO_USAGE))\n     {\n-      char *name_allocated = NULL;\n-      translated_option_name =\n-\ttranslate_option_name (opt->name, &name_allocated);\n-      int translation_differs =\n-\t(strcmp (translated_option_name, opt->name) != 0);\n+      translated_option_names =\n+\ttranslate_option_name (opt->name);\n+      __argp_fmtstream_printf (stream, \" [\");\n       if (arg)\n+\targ = dgettext (domain, arg);\n+      for (size_t i = 0;\n+\t   (translated_option_names != NULL\n+\t    && translated_option_names[i] != NULL);\n+\t   i++)\n \t{\n-\t  arg = dgettext (domain, arg);\n-\t  if ((flags & OPTION_ARG_OPTIONAL) && translation_differs)\n-\t    __argp_fmtstream_printf (stream, \" [--%s[=%s] (--%s)]\",\n-\t\t\t\t     translated_option_name, arg, opt->name);\n-\t  else if (flags & OPTION_ARG_OPTIONAL)\n-\t    __argp_fmtstream_printf (stream, \" [--%s[=%s]]\", opt->name, arg);\n-\t  else if (translation_differs)\n-\t    __argp_fmtstream_printf (stream, \" [--%s=%s (--%s)]\",\n-\t\t\t\t     translated_option_name, arg, opt->name);\n-\t  else\n-\t    __argp_fmtstream_printf (stream, \" [--%s=%s]\", opt->name, arg);\n+\t  if (i != 0)\n+\t    __argp_fmtstream_printf (stream, \" / \");\n+\t  __argp_fmtstream_printf (stream, \"--%s\", translated_option_names[i]);\n+\t  if (arg && i == 0)\n+\t    {\n+\t      if (flags & OPTION_ARG_OPTIONAL)\n+\t\t__argp_fmtstream_printf (stream, \"[=%s]\", arg);\n+\t      else\n+\t\t__argp_fmtstream_printf (stream, \"=%s\", arg);\n+\t    }\n+\t  if (strcmp (translated_option_names[i], opt->name) == 0)\n+\t    needs_untranslated = false;\n+\t  free (translated_option_names[i]);\n \t}\n-      else if (translation_differs)\n-\t__argp_fmtstream_printf (stream, \" [--%s (--%s)]\",\n-\t\t\t\t translated_option_name, opt->name);\n-      else\n-\t__argp_fmtstream_printf (stream, \" [--%s]\", opt->name);\n-      free (name_allocated);\n+      free (translated_option_names);\n+      if (needs_untranslated)\n+\t__argp_fmtstream_printf (stream, \" (--%s)\", opt->name);\n+      __argp_fmtstream_printf (stream, \"]\");\n+      /* If memory allocation failed, the output will be like\n+\t [ (--option)]\n+      */\n     }\n \n   return 0;\ndiff --git a/argp/tst-argphelp-localized.c b/argp/tst-argphelp-localized.c\nindex 8703742b8f..6c36eb9cf8 100644\n--- a/argp/tst-argphelp-localized.c\n+++ b/argp/tst-argphelp-localized.c\n@@ -44,6 +44,8 @@ const struct argp_option options[] =\n };\n \n static bool color_set = false;\n+static bool flavor_set = false;\n+static bool texture_set = false;\n \n static error_t\n parse_opt (int key, char *arg, struct argp_state *state)\n@@ -53,6 +55,14 @@ parse_opt (int key, char *arg, struct argp_state *state)\n     FAIL (\"color already set.\\n\");\n   else if (key == 'c')\n     color_set = true;\n+  else if (key == 'f' && flavor_set)\n+    FAIL (\"flavor already set.\\n\");\n+  else if (key == 'f')\n+    flavor_set = true;\n+  else if (key == 't' && texture_set)\n+    FAIL (\"texture already set.\\n\");\n+  else if (key == 't')\n+    texture_set = true;\n   return 0;\n }\n \n@@ -64,6 +74,16 @@ do_test (void)\n   char *test1_argv[3] =\n     { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--colour=yellow\", NULL };\n   char *test2_argv[3] =\n+    { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--color=yellow\", NULL };\n+  char *test3_argv[3] =\n+    { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--coolur=yellow\", NULL };\n+  char *test4_argv[3] =\n+    { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--flavour\", NULL };\n+  char *test5_argv[3] =\n+    { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--flavor\", NULL };\n+  char *test6_argv[3] =\n+    { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--texture\", NULL };\n+  char *test7_argv[3] =\n     { (char *) \"/bin/tst-argphelp-localized\", (char *) \"--help\", NULL };\n \n   unsetenv (\"LANGUAGE\");\n@@ -72,18 +92,47 @@ do_test (void)\n \t\t\t\t    OBJPFX \"domaindir\") != NULL);\n   TEST_VERIFY_EXIT (textdomain (\"tst-argphelp-localized\") != NULL);\n   /* Check that the catalog is OK: */\n-  TEST_COMPARE_STRING (gettext (\"command-line option\\004color\"), \"colour\");\n+  TEST_COMPARE_STRING (gettext (\"command-line option\\004color\"),\n+\t\t       \"colour coolur\");\n   TEST_COMPARE_STRING (gettext (\"COOKIE\"), \"BISCUIT\");\n   argp_parse (&argp, 2, test1_argv, 0, 0, NULL);\n   TEST_VERIFY (color_set);\n+  TEST_VERIFY (!flavor_set);\n+  TEST_VERIFY (!texture_set);\n   color_set = false;\n+  argp_parse (&argp, 2, test2_argv, 0, 0, NULL);\n+  TEST_VERIFY (color_set);\n+  TEST_VERIFY (!flavor_set);\n+  TEST_VERIFY (!texture_set);\n+  color_set = false;\n+  argp_parse (&argp, 2, test3_argv, 0, 0, NULL);\n+  TEST_VERIFY (color_set);\n+  TEST_VERIFY (!flavor_set);\n+  TEST_VERIFY (!texture_set);\n+  color_set = false;\n+  argp_parse (&argp, 2, test4_argv, 0, 0, NULL);\n+  TEST_VERIFY (!color_set);\n+  TEST_VERIFY (flavor_set);\n+  TEST_VERIFY (!texture_set);\n+  flavor_set = false;\n+  argp_parse (&argp, 2, test5_argv, 0, 0, NULL);\n+  TEST_VERIFY (!color_set);\n+  TEST_VERIFY (flavor_set);\n+  TEST_VERIFY (!texture_set);\n+  flavor_set = false;\n+  argp_parse (&argp, 2, test6_argv, 0, 0, NULL);\n+  TEST_VERIFY (!color_set);\n+  TEST_VERIFY (!flavor_set);\n+  TEST_VERIFY (texture_set);\n+  texture_set = false;\n \n   /* This is the last chance to fail.  */\n   if (support_record_failure_is_failed ())\n-    FAIL_EXIT1 (\"There were test failures before the final invocation of --help\");\n+    FAIL_EXIT1 (\n+\t\"There were test failures before the final invocation of --help\");\n   /* This last test will exit the program with code 0 and ignore\n      previous failures.  */\n-  argp_parse (&argp, 2, test2_argv, 0, 0, NULL);\n+  argp_parse (&argp, 2, test7_argv, 0, 0, NULL);\n   FAIL_EXIT1 (\"--help did not exit the program\");\n   return 0;\n }\ndiff --git a/argp/tst-argphelp-localized.po b/argp/tst-argphelp-localized.po\nindex 718cb39ab7..5b3a672a6d 100644\n--- a/argp/tst-argphelp-localized.po\n+++ b/argp/tst-argphelp-localized.po\n@@ -15,7 +15,7 @@ msgstr \"\"\n #: tst-argphelp-localized.c:73\n msgctxt \"command-line option\"\n msgid \"color\"\n-msgstr \"colour\"\n+msgstr \"colour coolur\"\n \n msgctxt \"command-line option\"\n msgid \"flavor\"\n@@ -23,3 +23,4 @@ msgstr \"flavour\"\n \n msgid \"COOKIE\"\n msgstr \"BISCUIT\"\n+\ndiff --git a/argp/tst-argpusage-localized.c b/argp/tst-argpusage-localized.c\nindex f93c2a156e..5d1f568679 100644\n--- a/argp/tst-argpusage-localized.c\n+++ b/argp/tst-argpusage-localized.c\n@@ -64,7 +64,8 @@ do_test (void)\n \t\t\t\t    OBJPFX \"domaindir\") != NULL);\n   TEST_VERIFY_EXIT (textdomain (\"tst-argphelp-localized\") != NULL);\n   /* Check that the catalog is OK: */\n-  TEST_COMPARE_STRING (gettext (\"command-line option\\004color\"), \"colour\");\n+  TEST_COMPARE_STRING (gettext (\"command-line option\\004color\"),\n+\t\t       \"colour coolur\");\n   TEST_COMPARE_STRING (gettext (\"COOKIE\"), \"BISCUIT\");\n   /* This is the last chance to fail.  */\n   if (support_record_failure_is_failed ())\ndiff --git a/manual/getopt.texi b/manual/getopt.texi\nindex e39f3e3f85..e7216c2baf 100644\n--- a/manual/getopt.texi\n+++ b/manual/getopt.texi\n@@ -257,6 +257,11 @@ invocation of your program, the program users should be encouraged to\n use untranslated option names or publish the locale used for this\n invocation.\n \n+If the translation of an option name contains a space character, then\n+it means multiple translations recognize the same option name.  This\n+is useful to upgrade a translation without disrupting the user's\n+workflow.\n+\n Since option names may be short words instead of long sentences, they\n may have different translations in different contexts within the same\n program.  @xref{Contexts, , Using contexts for solving ambiguities,\ndiff --git a/posix/check-getopt-translations.pl b/posix/check-getopt-translations.pl\nindex c3c3cff1eb..5cabd9be31 100644\n--- a/posix/check-getopt-translations.pl\n+++ b/posix/check-getopt-translations.pl\n@@ -146,7 +146,8 @@ while (my $line = <$pofile>) {\n     } elsif ($parser_state == 3 && $line eq \"\") {\n         $parser_state = 1;\n     } elsif ($parser_state == 3 && $line =~ /^msgstr\\s*\"([^\"]*)\"$/) {\n-        $translations{$entry_msgid} = $1;\n+        my @translations_for_this = split(/\\s+/, $1);\n+        $translations{$entry_msgid} = \\@translations_for_this;\n         $parser_state = 1;\n     }\n }\n@@ -156,13 +157,14 @@ my $number_of_errors = 0;\n # Verify that every option name is unique.\n my %untranslated_name;\n for my $option_name (sort(keys %translations)) {\n-    my $translation = $translations{$option_name};\n-    my @existing;\n-    if (exists $untranslated_name{$translation}) {\n-\t@existing = @{$untranslated_name{$translation}};\n+    for my $translation (@{$translations{$option_name}}) {\n+        my @existing;\n+        if (exists $untranslated_name{$translation}) {\n+            @existing = @{$untranslated_name{$translation}};\n+        }\n+        push(@existing, $option_name);\n+        $untranslated_name{$translation} = \\@existing;\n     }\n-    push(@existing, $option_name);\n-    $untranslated_name{$translation} = \\@existing;\n }\n for my $translation (sort(keys %untranslated_name)) {\n     my $names = $untranslated_name{$translation};\n@@ -180,10 +182,12 @@ for my $translation (sort(keys %untranslated_name)) {\n for my $option_name (sort(keys %translations)) {\n     for my $other_option_name (sort(keys %translations)) {\n         if ($option_name ne $other_option_name) {\n-\t    if ($translations{$option_name} eq $other_option_name) {\n-\t\tprint STDERR \"${translations{$option_name}} is a translation of ${option_name}, but it is also a different option.\\n\";\n-\t\t++$number_of_errors;\n-\t    }\n+            for my $translation (@{$translations{$option_name}}) {\n+                if ($translation eq $other_option_name) {\n+                    print STDERR \"${translation} is a translation of ${option_name}, but it is also a different option.\\n\";\n+                    ++$number_of_errors;\n+                }\n+            }\n         }\n     }\n }\ndiff --git a/posix/getopt.c b/posix/getopt.c\nindex 2983fe6ea7..32cd4cf120 100644\n--- a/posix/getopt.c\n+++ b/posix/getopt.c\n@@ -182,6 +182,50 @@ exchange (char **argv, struct _getopt_data *d)\n   d->__last_nonopt = d->optind;\n }\n \n+/* Return TRUE if other is equal to reference.  */\n+static bool\n+complete_match (const char *reference,\n+\t\tsize_t reference_length,\n+\t\tconst char *other)\n+{\n+  return (strncmp (reference, other, reference_length) == 0\n+\t  /* So, reference is a prefix of other */\n+\t  && other[reference_length] == '\\0');\n+}\n+\n+/* Match a string against a space-separated string list.  We save an\n+   allocated copy of the exact match for the collision checker.  */\n+static bool\n+match_any_translation (const char *reference,\n+\t\t       size_t reference_length,\n+\t\t       const char *translation,\n+\t\t       char **match)\n+{\n+  const char *start = translation;\n+  const char *end;\n+\n+  if (match)\n+    *match = NULL;\n+  while (start != NULL)\n+    {\n+      end = strchr (start, ' ');\n+      if ((end == NULL && complete_match (reference, reference_length, start))\n+\t  || (end != NULL\n+\t      && end - start == reference_length\n+\t      && strncmp (reference, start, reference_length) == 0))\n+\t{\n+\t  if (match)\n+\t    *match = __strndup (reference, reference_length);\n+\t  return true;\n+\t}\n+      start = end;\n+      if (start != NULL)\n+\t/* Skip the space character.  */\n+\tstart++;\n+    }\n+  return false;\n+}\n+\n /* Return true iff translation_context is not NULL, a translation for\n    opt_name has been found and it matches the substring from argument,\n    length argument_length.\n@@ -202,16 +246,42 @@ match_translated_option_name (char *(*translate) (const char *, const char *,\n   if (translate != NULL && !__libc_enable_secure)\n     translated = translate (opt_textdomain, translation_context,\n \t\t\t    opt_name, &translation_buffer);\n-\n-  if (strncmp (translated, argument, argument_length) != 0)\n-    matches = false;\n-  else\n-    /* We know that argument is a prefix of translated.  */\n-    matches = translated[argument_length] == '\\0';\n+  matches = match_any_translation (argument, argument_length, translated, NULL);\n   free (translation_buffer);\n   return matches;\n }\n \n+/* Translate opt_name, but only keep the first item of the list.  This\n+   is used for error messages.  */\n+static const char *\n+first_translation (char *(*translate) (const char *, const char *,\n+\t\t\t\t       const char *, char **),\n+\t\t   const char *translation_context,\n+\t\t   const char *opt_textdomain,\n+\t\t   const char *opt_name,\n+\t\t   char **allocated)\n+{\n+  char *translation_buffer = NULL;\n+  const char *all_translations =\n+    translate (opt_textdomain, translation_context, opt_name,\n+\t       &translation_buffer);\n+  const char *end = strchr (all_translations, ' ');\n+  if (end == NULL)\n+    {\n+      /* There is only 1 translation for opt_name.  No extra\n+\t processing is needed.  */\n+      *allocated = translation_buffer;\n+      return all_translations;\n+    }\n+  *allocated = __strndup (all_translations, end - all_translations);\n+  free (translation_buffer);\n+  if (*allocated == NULL)\n+    /* Memory allocation failed; return opt_name.  No extra memory\n+       needs to be kept around.  */\n+    return opt_name;\n+  return *allocated;\n+}\n+\n /* Process the argument starting with d->__nextchar as a long option.\n    d->optind should *not* have been advanced over this argument.\n \n@@ -393,9 +463,9 @@ process_long_option (int argc, char **argv, const char *optstring,\n \t{\n \t  if (print_errors)\n \t    {\n-\t      translated_option_name = translate (d->opttextdomain, d->optctxt,\n-\t\t\t\t\t\t  pfound->name,\n-\t\t\t\t\t\t  &translation_buffer);\n+\t      translated_option_name =\n+\t\tfirst_translation (translate, d->optctxt, d->opttextdomain,\n+\t\t\t\t   pfound->name, &translation_buffer);\n \t      if (strcmp (translated_option_name, pfound->name) != 0)\n \t\t/* Print both names of the option.  */\n \t\tfprintf (stderr,\n@@ -423,9 +493,9 @@ process_long_option (int argc, char **argv, const char *optstring,\n \t    {\n \t      /* Same dichotomy as when the option does not allow an\n \t\t argument.  */\n-\t      translated_option_name = translate (d->opttextdomain, d->optctxt,\n-\t\t\t\t\t\t  pfound->name,\n-\t\t\t\t\t\t  &translation_buffer);\n+\t      translated_option_name =\n+\t\tfirst_translation (translate, d->optctxt, d->opttextdomain,\n+\t\t\t\t   pfound->name, &translation_buffer);\n \t      if (strcmp (translated_option_name, pfound->name) != 0)\n \t\tfprintf (stderr,\n \t\t\t _(\"%s: option '%s%s' / '%s%s' requires an argument\\n\"),\n@@ -488,6 +558,32 @@ _getopt_initialize (_GL_UNUSED int argc,\n }\n \f\n \n+/* Match any item of a string list against any item of another.  This\n+   is used by the collision checker.  */\n+static bool\n+match_any_translation_pair (const char *list_a,\n+\t\t\t    const char *list_b,\n+\t\t\t    char **match)\n+{\n+  const char *start = list_a;\n+  const char *end;\n+\n+  while (start != NULL)\n+    {\n+      end = strchr (start, ' ');\n+      if ((end == NULL\n+\t   && match_any_translation (start, strlen (start), list_b, match))\n+\t  || (end != NULL\n+\t      && match_any_translation (start, end - start, list_b, match)))\n+\treturn true;\n+      start = end;\n+      if (start != NULL)\n+\t/* Skip the space character.  */\n+\tstart++;\n+    }\n+  return false;\n+}\n+\n static bool\n has_translation_collisions (const char *domain,\n \t\t\t    const char *context,\n@@ -508,6 +604,7 @@ has_translation_collisions (const char *domain,\n   char *b_buffer = NULL;\n   const char *b_name = NULL;\n   const struct option *option_b;\n+  char *collision = NULL;\n   bool has_collision = false;\n \n   if (do_translate == NULL || context == NULL)\n@@ -532,7 +629,9 @@ has_translation_collisions (const char *domain,\n \t  {\n \t    option_b = &(long_options[option_index_b]);\n \t    b_name = do_translate (domain, context, option_b->name, &b_buffer);\n-\t    if (strcmp (option_a->name, b_name) == 0)\n+\t    collision = NULL;\n+\t    if (match_any_translation (option_a->name, strlen (option_a->name), b_name,\n+\t\t\t\t       &collision))\n \t      {\n \t\tif (print_errors)\n \t\t  /* Since we do not consider a particular use of an\n@@ -546,13 +645,15 @@ has_translation_collisions (const char *domain,\n \t\t\t   argv0,\n \t\t\t   domain, context,\n \t\t\t   option_a->name,\n-\t\t\t   option_b->name, b_name);\n+\t\t\t   option_b->name, collision);\n \t\thas_collision = true;\n \t      }\n-\t    if (strcmp (a_name, b_name) == 0\n+\t    free (collision);\n+\t    collision = NULL;\n+\t    if (option_index_a < option_index_b\n \t\t&& strcmp (option_a->name, a_name) != 0\n \t\t&& strcmp (option_b->name, b_name) != 0\n-\t\t&& option_index_a < option_index_b)\n+\t\t&& match_any_translation_pair (a_name, b_name, &collision))\n \t      {\n \t\tif (print_errors)\n \t\t  fprintf (stderr,\n@@ -560,9 +661,10 @@ has_translation_collisions (const char *domain,\n \t\t\t     \"domain '%s', context '%s': \"\n \t\t\t     \"both '%s' and '%s' translate to '%s'\\n\"),\n \t\t\t   argv0, domain, context,\n-\t\t\t   option_a->name, option_b->name, a_name);\n+\t\t\t   option_a->name, option_b->name, collision);\n \t\thas_collision = true;\n \t      }\n+\t    free (collision);\n \t    free (b_buffer);\n \t  }\n       free (a_buffer);\ndiff --git a/posix/standalone-multiple-getopt-collisions.po b/posix/standalone-multiple-getopt-collisions.po\nindex 14b876a2a3..edd2231d8f 100644\n--- a/posix/standalone-multiple-getopt-collisions.po\n+++ b/posix/standalone-multiple-getopt-collisions.po\n@@ -27,17 +27,17 @@ msgstr \"bar\"\n # This is the --foo option.\n msgctxt \"command-line option\"\n msgid \"foo\"\n-msgstr \"toto\"\n+msgstr \"tata toto\"\n \n # This is the --bar option.  Oops, I translated with toto here too.\n msgctxt \"command-line option\"\n msgid \"bar\"\n-msgstr \"toto\"\n+msgstr \"titi toto\"\n \n # Let’s go to the --pub!\n msgctxt \"command-line option\"\n msgid \"pub\"\n-msgstr \"bar\"\n+msgstr \"bar club\"\n \n # Wait, it’s OK if baz is translated to baz though.\n msgctxt \"command-line option\"\ndiff --git a/posix/tst-getopt_long_collision.c b/posix/tst-getopt_long_collision.c\nindex 2e603ec9f8..ff90ec1131 100644\n--- a/posix/tst-getopt_long_collision.c\n+++ b/posix/tst-getopt_long_collision.c\n@@ -42,6 +42,8 @@\n    In the third, we don’t translate anything:\n    foo -> foo\n    bar -> bar\n+\n+   For added fun, we add some noise to the translations.\n   */\n \n static const struct option options[] =\n@@ -62,13 +64,13 @@ setup_catalog (void)\n   TEST_VERIFY_EXIT (textdomain (\"tst-getopt_long_collision\") != NULL);\n   /* Check that the catalog is OK: */\n   TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 1\\004foo\"),\n-\t\t       \"bar\");\n+\t\t       \"bar noise1\");\n   TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 1\\004bar\"),\n-\t\t       \"baz\");\n+\t\t       \"noise2 baz noise3\");\n   TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 2\\004foo\"),\n-\t\t       \"same\");\n+\t\t       \"same noise4\");\n   TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 2\\004bar\"),\n-\t\t       \"same\");\n+\t\t       \"noise5 same\");\n   TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 3\\004foo\"),\n \t\t       \"kind 3\\004foo\");\n   TEST_COMPARE_STRING (dgettext (\"tst-getopt_long_collision\", \"kind 3\\004bar\"),\ndiff --git a/posix/tst-getopt_long_collision.po b/posix/tst-getopt_long_collision.po\nindex 2f39001c6d..a196e81f38 100644\n--- a/posix/tst-getopt_long_collision.po\n+++ b/posix/tst-getopt_long_collision.po\n@@ -17,16 +17,16 @@ msgstr \"\"\n \n msgctxt \"kind 1\"\n msgid \"foo\"\n-msgstr \"bar\"\n+msgstr \"bar noise1\"\n \n msgctxt \"kind 1\"\n msgid \"bar\"\n-msgstr \"baz\"\n+msgstr \"noise2 baz noise3\"\n \n msgctxt \"kind 2\"\n msgid \"foo\"\n-msgstr \"same\"\n+msgstr \"same noise4\"\n \n msgctxt \"kind 2\"\n msgid \"bar\"\n-msgstr \"same\"\n+msgstr \"noise5 same\"\ndiff --git a/posix/tstgetoptl.c b/posix/tstgetoptl.c\nindex bdc20b7e3d..0374219dc5 100644\n--- a/posix/tstgetoptl.c\n+++ b/posix/tstgetoptl.c\n@@ -31,10 +31,27 @@\n    This echoes tstgetopt.c, where --colour was an option name alias\n    for --color, so it had to be listed twice.  */\n \n-/* This uses the en_GB locale so that colour means color.  We also\n-   check that getopt only matches translations for actual options, by\n-   having the user pass --flavour (which is a known translation of\n-   flavor) without the program recognizing a --flavor option.  */\n+/* This uses the en_GB locale so that colour means color.\n+\n+   Oh no! The translator made a mistake and translated color as\n+   “coolur”.  A bug-fix has been released, but in the mean time, a\n+   popular British influencer made a blog post about how you can use\n+   “coolur” and it turned into a meme.  Lots of people have\n+   copy-pasted a custom script to check if the program supports\n+   British values, and it does so by checking whether --coolur prints\n+   “as red as a sunburnt tourist”.  To the point where ChatGPT and\n+   other LLMs now consider this the pinnacle of British\n+   exceptionalism.  The UK MPs have voted to make the script a\n+   mandatory part of any operating systems used by British people\n+   anywhere in the world, with severe fines for anyone not using the\n+   exact version prescribed by the law.  It has thus been decided to\n+   support both “colour” and “coolur”, without creating a new option\n+   (only “color” exists for the developers).\n+\n+   We also check that getopt only matches\n+   translations for actual options, by having the user pass --flavour\n+   (which is a known translation of flavor) without the program\n+   recognizing a --flavor option.  */\n \n #define TRANSLATION_CONTEXT \"command-line option\"\n \n@@ -48,7 +65,7 @@ prepare_localedir (void)\n   /* Check that the catalog is OK: */\n   TEST_COMPARE_STRING (dgettext (\"tstgetoptl\",\n \t\t\t\t TRANSLATION_CONTEXT \"\\004\" \"color\"),\n-\t\t       \"colour\");\n+\t\t       \"colour coolur\");\n   TEST_COMPARE_STRING (dgettext (\"tstgetoptl\",\n \t\t\t\t TRANSLATION_CONTEXT \"\\004\" \"flavor\"),\n \t\t       \"flavour\");\n@@ -61,7 +78,7 @@ prepare_argv (int *argc)\n     {\n       (char *) \"tstgetoptl\", (char *) \"--required\", (char *) \"foobar\",\n       (char *) \"--optional=bazbug\", (char *) \"--col\", (char *) \"--color\",\n-      (char *) \"--colour\", (char *) \"--flavour\", NULL\n+      (char *) \"--colour\", (char *) \"--coolur\", (char *) \"--flavour\", NULL\n     };\n   *argc = array_length (argv) - 1;\n   return argv;\n@@ -79,7 +96,8 @@ do_my_test (bool with_optctxt)\n       {\"required\", required_argument, NULL, 'r'},\n       {\"optional\", optional_argument, NULL, 'o'},\n       {\"color\",\t   no_argument,\t      NULL, 'C'},\n-      /* Now colour is handled as a translation of color.  */\n+      /* Now colour (and coolur) are handled as a translation of\n+\t color.  */\n       /* Note that there’s no \"--flavor\" option, so the \"flavor\" ->\n \t \"flavour\" translation is useless.  */\n       {NULL, 0, NULL, 0 }\n@@ -139,7 +157,7 @@ do_my_test (bool with_optctxt)\n   printf (\"Cflags = %d\\n\", Cflag);\n \n   if (with_optctxt)\n-    TEST_COMPARE (Cflag, 3);\n+    TEST_COMPARE (Cflag, 4);\n   else\n     TEST_COMPARE (Cflag, 2);\n \ndiff --git a/posix/tstgetoptl.po b/posix/tstgetoptl.po\nindex b1dc11c468..341ac9ea33 100644\n--- a/posix/tstgetoptl.po\n+++ b/posix/tstgetoptl.po\n@@ -7,7 +7,7 @@ msgstr \"\"\n \"Project-Id-Version: tstgetoptl 0.0.0\\n\"\n \"Report-Msgid-Bugs-To: \\n\"\n \"POT-Creation-Date: 2025-05-27 19:29+0200\\n\"\n-\"PO-Revision-Date: 2025-05-27 19:30+0200\\n\"\n+\"PO-Revision-Date: 2025-06-06 21:22+0200\\n\"\n \"Language-Team: English (British) <(nothing)>\\n\"\n \"Language: en_GB\\n\"\n \"MIME-Version: 1.0\\n\"\n@@ -18,7 +18,7 @@ msgstr \"\"\n #: xxx.c:yy\n msgctxt \"command-line option\"\n msgid \"color\"\n-msgstr \"colour\"\n+msgstr \"colour coolur\"\n \n #: xxx.c:yy\n msgctxt \"command-line option\"\n",
    "prefixes": [
        "v22",
        "9/9"
    ]
}