From e125dba1c5c39f47940421737e1700ed55b01f3b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?K=C3=A9vin=20Commaille?= <zecakeh@tedomum.fr>
Date: Wed, 5 Jan 2022 12:03:28 +0100
Subject: [PATCH 2/2] xgettext: Add Rust support.

* gettext-tools/src/x-rust.h: New file.
* gettext-tools/src/x-rust.c: New file.
* gettext-tools/src/xgettext.c: Include x-rust.h.
(flag_table_rust): New variable.
(main): Update for Rust.
(usage): Document the '-L Rust' option.
(xgettext_record_flag, language_to_extractor, extension_to_language): Update for
Rust.
* gettext-tools/src/Makefile.am (noinst_HEADERS): Add x-rust.h.
(xgettext_SOURCES): Add x-rust.c.
* gettext-tools/src/FILES: Mention x-rust.h, x-rust.c.
* gettext-tools/po/POTFILES.in: Add x-rust.c
* gettext-tools/tests/xgettext-rust-1: New file.
* gettext-tools/tests/xgettext-rust-2: New file.
* gettext-tools/tests/xgettext-rust-3: New file.
* gettext-tools/tests/xgettext-rust-4: New file.
* gettext-tools/tests/xgettext-rust-5: New file.
* gettext-tools/tests/format-rust-1: New file.
* gettext-tools/tests/format-rust-2: New file.
* gettext-tools/tests/Makefile.am (TESTS): Add them.
* gettext-tools/doc/lang-rust.texi: New file.
* gettext-tools/doc/gettext.texi (Rust): New section.
* gettext-tools/doc/xgettext.texi: Document the '-L Rust' option.
* NEWS: Mention the Rust support.
---
 NEWS                                |    4 +
 gettext-tools/doc/gettext.texi      |    3 +
 gettext-tools/doc/lang-rust.texi    |   85 ++
 gettext-tools/doc/xgettext.texi     |    2 +-
 gettext-tools/po/POTFILES.in        |    1 +
 gettext-tools/src/FILES             |    3 +
 gettext-tools/src/Makefile.am       |    6 +-
 gettext-tools/src/x-rust.c          | 1512 +++++++++++++++++++++++++++
 gettext-tools/src/x-rust.h          |   45 +
 gettext-tools/src/xgettext.c        |   14 +-
 gettext-tools/tests/Makefile.am     |    3 +
 gettext-tools/tests/format-rust-1   |  103 ++
 gettext-tools/tests/format-rust-2   |   67 ++
 gettext-tools/tests/xgettext-rust-1 |   75 ++
 gettext-tools/tests/xgettext-rust-2 |   82 ++
 gettext-tools/tests/xgettext-rust-3 |  165 +++
 gettext-tools/tests/xgettext-rust-4 |   36 +
 gettext-tools/tests/xgettext-rust-5 |   28 +
 18 files changed, 2230 insertions(+), 4 deletions(-)
 create mode 100644 gettext-tools/doc/lang-rust.texi
 create mode 100644 gettext-tools/src/x-rust.c
 create mode 100644 gettext-tools/src/x-rust.h
 create mode 100755 gettext-tools/tests/format-rust-1
 create mode 100755 gettext-tools/tests/format-rust-2
 create mode 100755 gettext-tools/tests/xgettext-rust-1
 create mode 100755 gettext-tools/tests/xgettext-rust-2
 create mode 100755 gettext-tools/tests/xgettext-rust-3
 create mode 100755 gettext-tools/tests/xgettext-rust-4
 create mode 100755 gettext-tools/tests/xgettext-rust-5

diff --git a/NEWS b/NEWS
index 7684c8699..931927a6e 100644
--- a/NEWS
+++ b/NEWS
@@ -9,6 +9,10 @@ Version 0.21.1 - February 2022
   - C, C++: xgettext now supports gettext-like functions that take wide strings
     (of type 'const wchar_t *', 'const char16_t *', or 'const char32_t *') as
     arguments.
+  - Rust:
+    o xgettext now supports Rust.
+    o 'msgfmt -c' now verifies the syntax of translations of Rust format
+      strings.
 
 * Runtime behaviour:
   - On AIX, locale names with a script or with an uppercase language are now
diff --git a/gettext-tools/doc/gettext.texi b/gettext-tools/doc/gettext.texi
index 635e2c37e..76d934228 100644
--- a/gettext-tools/doc/gettext.texi
+++ b/gettext-tools/doc/gettext.texi
@@ -444,6 +444,7 @@ Individual Programming Languages
 * Pike::                        Pike
 * GCC-source::                  GNU Compiler Collection sources
 * YCP::                         YCP - YaST2 scripting language
+* Rust::                        Rust
 
 sh - Shell Script
 
@@ -9930,6 +9931,7 @@ that language, and to combine the resulting files using @code{msgcat}.
 * Pike::                        Pike
 * GCC-source::                  GNU Compiler Collection sources
 * YCP::                         YCP - YaST2 scripting language
+* Rust::                        Rust
 @end menu
 
 @include lang-c.texi
@@ -9957,6 +9959,7 @@ that language, and to combine the resulting files using @code{msgcat}.
 @include lang-pike.texi
 @include lang-gcc-source.texi
 @include lang-ycp.texi
+@include lang-rust.texi
 
 @c This is the template for new languages.
 @ignore
diff --git a/gettext-tools/doc/lang-rust.texi b/gettext-tools/doc/lang-rust.texi
new file mode 100644
index 000000000..fd3fb20ec
--- /dev/null
+++ b/gettext-tools/doc/lang-rust.texi
@@ -0,0 +1,85 @@
+@c This file is part of the GNU gettext manual.
+@c Copyright (C) 1995-2020, 2022 Free Software Foundation, Inc.
+@c See the file gettext.texi for copying conditions.
+
+@node Rust
+@subsection Rust
+@cindex Rust
+
+@table @asis
+@item RPMs
+rust, cargo
+
+@item Ubuntu packages
+cargo
+
+@item File extension
+@code{rs}
+
+@item String syntax
+@code{"abc"}, @code{b"abc"}, @code{r"abc"}
+
+@item gettext shorthand
+---
+
+@item gettext/ngettext functions
+@code{gettext}, @code{ngettext}, @code{pgettext},
+@code{npgettext}, @code{dgettext}, @code{dngettext},
+@code{dcgettext}, @code{dcngettext},
+also macros that allow formatting @code{gettext!},
+@code{ngettext!}, @code{pgettext!}, @code{npgettext!},
+@code{dgettext!}, @code{dngettext!}, @code{dcgettext!},
+@code{dcngettext!}
+
+@item textdomain
+@code{textdomain} function, or
+@code{TextDomain::new} method
+
+@item bindtextdomain
+@code{bindtextdomain} function, or
+automatic with @code{TextDomain::init} method
+
+@item setlocale
+@code{setlocale} function, or
+automatic with @code{TextDomain::init} method, or
+@code{TextDomain::locale} method
+
+@item Prerequisite
+@code{use gettext_rs::gettext;}
+
+@item Use or emulate GNU gettext
+use
+
+@item Extractor
+@code{xgettext}
+
+@item Formatting with positions
+@code{gettext!("@{1@} is before @{0@}", x, y);}
+@*@code{gettext!("@{new@} replaces @{old@}", new, old);}
+
+@item Portability
+fully portable
+
+@item po-mode marking
+---
+@end table
+
+A note about format strings: Rust supports format strings with unnamed
+arguments, such as @code{"...@{@}..."}, format strings with positional
+arguments, such as @code{"...@{0@}..."} and format strings with named arguments,
+such as @code{"...@{ident@}..."}. It is preferable to avoid the former for
+internationalized programs. When a format string takes more than one argument,
+the translator can provide a translation that uses the arguments in a different
+order, if the format string uses positional or named arguments.  For example,
+the translator can reformulate
+@smallexample
+"@{volume@} has only @{freespace@} bytes free."
+@end smallexample
+@noindent
+to
+@smallexample
+"Only @{freespace@} bytes free on @{volume@}."
+@end smallexample
+@noindent
+Additionally, the identifiers of named arguments also provide some context to
+the translator.
diff --git a/gettext-tools/doc/xgettext.texi b/gettext-tools/doc/xgettext.texi
index 7cd9ec8ff..51cd8aa89 100644
--- a/gettext-tools/doc/xgettext.texi
+++ b/gettext-tools/doc/xgettext.texi
@@ -76,7 +76,7 @@ Specifies the language of the input files.  The supported languages
 are @code{C}, @code{C++}, @code{ObjectiveC}, @code{PO}, @code{Shell},
 @code{Python}, @code{Lisp}, @code{EmacsLisp}, @code{librep}, @code{Scheme},
 @code{Smalltalk}, @code{Java}, @code{JavaProperties}, @code{C#}, @code{awk},
-@code{YCP}, @code{Tcl}, @code{Perl}, @code{PHP}, @code{Ruby},
+@code{YCP}, @code{Tcl}, @code{Perl}, @code{PHP}, @code{Ruby}, @code{Rust},
 @code{GCC-source}, @code{NXStringTable}, @code{RST}, @code{RSJ}, @code{Glade},
 @code{Lua}, @code{JavaScript}, @code{Vala}, @code{GSettings}, @code{Desktop}.
 
diff --git a/gettext-tools/po/POTFILES.in b/gettext-tools/po/POTFILES.in
index 120548182..68daa3294 100644
--- a/gettext-tools/po/POTFILES.in
+++ b/gettext-tools/po/POTFILES.in
@@ -132,6 +132,7 @@ src/x-po.c
 src/x-python.c
 src/x-rst.c
 src/x-ruby.c
+src/x-rust.c
 src/x-scheme.c
 src/x-sh.c
 src/x-smalltalk.c
diff --git a/gettext-tools/src/FILES b/gettext-tools/src/FILES
index cde4be905..1af29b440 100644
--- a/gettext-tools/src/FILES
+++ b/gettext-tools/src/FILES
@@ -396,6 +396,9 @@ msgl-check.c
 | x-appdata.h
 | x-appdata.c
 |               String extractor for AppData files.
+| x-rust.h
+| x-rust.c
+|               String extractor for Rust.
 | xgettext.c
 |               Main source for the 'xgettext' program.
 |
diff --git a/gettext-tools/src/Makefile.am b/gettext-tools/src/Makefile.am
index 5f96f8635..a80e6f893 100644
--- a/gettext-tools/src/Makefile.am
+++ b/gettext-tools/src/Makefile.am
@@ -80,7 +80,8 @@ noinst_HEADERS = \
   x-ycp.h \
   x-rst.h \
   x-desktop.h \
-  x-glade.h x-gsettings.h x-appdata.h
+  x-glade.h x-gsettings.h x-appdata.h \
+  x-rust.h
 
 EXTRA_DIST += FILES project-id
 
@@ -247,7 +248,8 @@ xgettext_SOURCES += \
   x-php.c \
   x-ycp.c \
   x-rst.c \
-  x-desktop.c
+  x-desktop.c \
+  x-rust.c
 if !WOE32DLL
 msgattrib_SOURCES = msgattrib.c
 else
diff --git a/gettext-tools/src/x-rust.c b/gettext-tools/src/x-rust.c
new file mode 100644
index 000000000..64d95c49e
--- /dev/null
+++ b/gettext-tools/src/x-rust.c
@@ -0,0 +1,1512 @@
+/* xgettext Rust backend.
+   Copyright (C) 2022 Free Software Foundation, Inc.
+   Written by Kévin Commaille <zecakeh@tedomum.fr>, 2022.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+/* Specification.  */
+#include "x-rust.h"
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "attribute.h"
+#include "c-ctype.h"
+#include "error-progname.h"
+#include "error.h"
+#include "gettext.h"
+#include "mem-hash-map.h"
+#include "message.h"
+#include "po-charset.h"
+#include "rc-str-list.h"
+#include "unictype.h"
+#include "unistr.h"
+#include "xalloc.h"
+#include "xerror.h"
+#include "xg-arglist-callshape.h"
+#include "xg-arglist-context.h"
+#include "xg-arglist-parser.h"
+#include "xg-encoding.h"
+#include "xg-message.h"
+#include "xg-mixed-string.h"
+#include "xg-pos.h"
+#include "xgettext.h"
+#include "xvasprintf.h"
+
+#define _(str) gettext (str)
+
+#define SIZEOF(a) (sizeof (a) / sizeof (a[0]))
+
+/* The Rust syntax is defined in the Rust Reference:
+   <https://doc.rust-lang.org/reference/>
+  */
+
+/* ====================== Keyword set customization.  ====================== */
+
+/* If true extract all strings.  */
+static bool extract_all = false;
+
+static hash_table keywords;
+static bool default_keywords = true;
+
+void
+x_rust_extract_all ()
+{
+  extract_all = true;
+}
+
+void
+x_rust_keyword (const char *name)
+{
+  if (name == NULL)
+    default_keywords = false;
+  else
+    {
+      const char *end;
+      struct callshape shape;
+      const char *colon;
+
+      if (keywords.table == NULL)
+        hash_init (&keywords, 100);
+
+      split_keywordspec (name, &end, &shape);
+
+      /* The characters between name and end should form a valid Rust
+         identifier. A colon means an invalid parse in split_keywordspec().  */
+      colon = strchr (name, ':');
+      if (colon == NULL || colon >= end)
+        insert_keyword_callshape (&keywords, name, end - name, &shape);
+    }
+}
+
+/* Finish initializing the keywords hash table.
+   Called after argument processing, before each file is processed.  */
+static void
+init_keywords ()
+{
+  if (default_keywords)
+    {
+      /* When adding new keywords here, also update the documentation in
+         xgettext.texi!  */
+      x_rust_keyword ("gettext");
+      x_rust_keyword ("dgettext:2");
+      x_rust_keyword ("dcgettext:2");
+      x_rust_keyword ("ngettext:1,2");
+      x_rust_keyword ("dngettext:2,3");
+      x_rust_keyword ("dcngettext:2,3");
+      x_rust_keyword ("pgettext:1c,2");
+      x_rust_keyword ("npgettext:2c,3");
+      x_rust_keyword ("gettext!");
+      x_rust_keyword ("dgettext!:2");
+      x_rust_keyword ("dcgettext!:2");
+      x_rust_keyword ("ngettext!:1,2");
+      x_rust_keyword ("dngettext!:2,3");
+      x_rust_keyword ("dcngettext!:2,3");
+      x_rust_keyword ("pgettext!:1c,2");
+      x_rust_keyword ("npgettext!:2c,3");
+      default_keywords = false;
+    }
+}
+
+void
+init_flag_table_rust ()
+{
+  xgettext_record_flag ("gettext:1:pass-rust-format");
+  xgettext_record_flag ("dgettext:2:pass-rust-format");
+  xgettext_record_flag ("dcgettext:2:pass-rust-format");
+  xgettext_record_flag ("ngettext:1:pass-rust-format");
+  xgettext_record_flag ("ngettext:2:pass-rust-format");
+  xgettext_record_flag ("dngettext:2:pass-rust-format");
+  xgettext_record_flag ("dngettext:3:pass-rust-format");
+  xgettext_record_flag ("dcngettext:2:pass-rust-format");
+  xgettext_record_flag ("dcngettext:3:pass-rust-format");
+  xgettext_record_flag ("pgettext:2:pass-rust-format");
+  xgettext_record_flag ("npgettext:3:pass-rust-format");
+  xgettext_record_flag ("gettext!:1:pass-rust-format");
+  xgettext_record_flag ("dgettext!:2:pass-rust-format");
+  xgettext_record_flag ("dcgettext!:2:pass-rust-format");
+  xgettext_record_flag ("ngettext!:1:pass-rust-format");
+  xgettext_record_flag ("ngettext!:2:pass-rust-format");
+  xgettext_record_flag ("dngettext!:2:pass-rust-format");
+  xgettext_record_flag ("dngettext!:3:pass-rust-format");
+  xgettext_record_flag ("dcngettext!:2:pass-rust-format");
+  xgettext_record_flag ("dcngettext!:3:pass-rust-format");
+  xgettext_record_flag ("pgettext!:2:pass-rust-format");
+  xgettext_record_flag ("npgettext!:3:pass-rust-format");
+}
+
+/* ======================== Reading of characters.  ======================== */
+
+/* The input file stream.  */
+static FILE *fp;
+
+/* 1. line_number handling.  */
+
+/* Maximum used, roughly a safer MB_LEN_MAX.  */
+#define MAX_PHASE1_PUSHBACK 16
+static unsigned char phase1_pushback[MAX_PHASE1_PUSHBACK];
+static int phase1_pushback_length;
+
+/* Read the next single byte from the input file.  */
+static int
+phase1_getc ()
+{
+  int c;
+
+  if (phase1_pushback_length)
+    c = phase1_pushback[--phase1_pushback_length];
+  else
+    {
+      c = getc (fp);
+
+      if (c == EOF)
+        {
+          if (ferror (fp))
+            error (EXIT_FAILURE, errno, _ ("error while reading \"%s\""),
+                   real_file_name);
+          return EOF;
+        }
+    }
+
+  if (c == '\n')
+    ++line_number;
+  return c;
+}
+
+/* Supports MAX_PHASE1_PUSHBACK characters of pushback.  */
+static void
+phase1_ungetc (int c)
+{
+  if (c != EOF)
+    {
+      if (c == '\n')
+        --line_number;
+
+      if (phase1_pushback_length == SIZEOF (phase1_pushback))
+        abort ();
+      phase1_pushback[phase1_pushback_length++] = c;
+    }
+}
+
+/* Phase 2: Conversion to Unicode.
+   This is done early because the Rust Reference section 2.1. says that the
+   source is "a sequence of Unicode code points encoded in UTF-8".  */
+
+/* End-of-file indicator for functions returning an UCS-4 character.  */
+#define UEOF -1
+
+/* Newline Unicode character.  */
+#define UNL 0x000a
+
+static lexical_context_ty lexical_context;
+
+static int phase2_pushback[3];
+static int phase2_pushback_length;
+
+/* Read the next Unicode UCS-4 character from the input file.  */
+static int
+phase2_getc ()
+{
+  if (phase2_pushback_length)
+    return phase2_pushback[--phase2_pushback_length];
+
+  if (xgettext_current_source_encoding == po_charset_ascii)
+    {
+      int c = phase1_getc ();
+      if (c == EOF)
+        return UEOF;
+      if (!c_isascii (c))
+        {
+          multiline_error (
+              xstrdup (""),
+              xasprintf ("%s\n%s\n",
+                         non_ascii_error_message (lexical_context,
+                                                  real_file_name, line_number),
+                         _ ("Please specify the source encoding through "
+                            "--from-code\n")));
+          exit (EXIT_FAILURE);
+        }
+      return c;
+    }
+  else if (xgettext_current_source_encoding != po_charset_utf8)
+    {
+#if HAVE_ICONV
+      /* Use iconv on an increasing number of bytes.  Read only as many bytes
+         through phase1_getc as needed.  This is needed to give reasonable
+         interactive behaviour when fp is connected to an interactive tty.  */
+      unsigned char buf[MAX_PHASE1_PUSHBACK];
+      size_t bufcount;
+      int c = phase1_getc ();
+      if (c == EOF)
+        return UEOF;
+      buf[0] = (unsigned char)c;
+      bufcount = 1;
+
+      for (;;)
+        {
+          unsigned char scratchbuf[6];
+          const char *inptr = (const char *)&buf[0];
+          size_t insize = bufcount;
+          char *outptr = (char *)&scratchbuf[0];
+          size_t outsize = sizeof (scratchbuf);
+
+          size_t res = iconv (xgettext_current_source_iconv,
+                              (ICONV_CONST char **)&inptr, &insize, &outptr,
+                              &outsize);
+          /* We expect that a character has been produced if and only if
+             some input bytes have been consumed.  */
+          if ((insize < bufcount) != (outsize < sizeof (scratchbuf)))
+            abort ();
+          if (outsize == sizeof (scratchbuf))
+            {
+              /* No character has been produced.  Must be an error.  */
+              if (res != (size_t)(-1))
+                abort ();
+
+              if (errno == EILSEQ)
+                {
+                  /* An invalid multibyte sequence was encountered.  */
+                  multiline_error (xstrdup (""),
+                                   xasprintf (_ ("\
+%s:%d: Invalid multibyte sequence.\n\
+Please specify the correct source encoding through --from-code\n"),
+                                              real_file_name, line_number));
+                  exit (EXIT_FAILURE);
+                }
+              else if (errno == EINVAL)
+                {
+                  /* An incomplete multibyte character.  */
+                  int c;
+
+                  if (bufcount == MAX_PHASE1_PUSHBACK)
+                    {
+                      /* An overlong incomplete multibyte sequence was
+                         encountered.  */
+                      multiline_error (xstrdup (""), xasprintf (_ ("\
+%s:%d: Long incomplete multibyte sequence.\n\
+Please specify the correct source encoding through --from-code\n"),
+                                                                real_file_name,
+                                                                line_number));
+                      exit (EXIT_FAILURE);
+                    }
+
+                  /* Read one more byte and retry iconv.  */
+                  c = phase1_getc ();
+                  if (c == EOF)
+                    {
+                      multiline_error (xstrdup (""), xasprintf (_ ("\
+%s:%d: Incomplete multibyte sequence at end of file.\n\
+Please specify the correct source encoding through --from-code\n"),
+                                                                real_file_name,
+                                                                line_number));
+                      exit (EXIT_FAILURE);
+                    }
+                  if (c == '\n')
+                    {
+                      multiline_error (xstrdup (""),
+                                       xasprintf (_ ("\
+%s:%d: Incomplete multibyte sequence at end of line.\n\
+Please specify the correct source encoding through --from-code\n"),
+                                                  real_file_name,
+                                                  line_number - 1));
+                      exit (EXIT_FAILURE);
+                    }
+                  buf[bufcount++] = (unsigned char)c;
+                }
+              else
+                error (EXIT_FAILURE, errno, _ ("%s:%d: iconv failure"),
+                       real_file_name, line_number);
+            }
+          else
+            {
+              size_t outbytes = sizeof (scratchbuf) - outsize;
+              size_t bytes = bufcount - insize;
+              ucs4_t uc;
+
+              /* We expect that one character has been produced.  */
+              if (bytes == 0)
+                abort ();
+              if (outbytes == 0)
+                abort ();
+              /* Push back the unused bytes.  */
+              while (insize > 0)
+                phase1_ungetc (buf[--insize]);
+              /* Convert the character from UTF-8 to UCS-4.  */
+              if (u8_mbtoucr (&uc, scratchbuf, outbytes) < (int)outbytes)
+                {
+                  /* scratchbuf contains an out-of-range Unicode character
+                     (> 0x10ffff).  */
+                  multiline_error (xstrdup (""),
+                                   xasprintf (_ ("\
+%s:%d: Invalid multibyte sequence.\n\
+Please specify the source encoding through --from-code\n"),
+                                              real_file_name, line_number));
+                  exit (EXIT_FAILURE);
+                }
+              return uc;
+            }
+        }
+#else
+      /* If we don't have iconv(), the only supported values for
+         xgettext_global_source_encoding and thus also for
+         xgettext_current_source_encoding are ASCII and UTF-8.  */
+      abort ();
+#endif
+    }
+  else
+    {
+      /* Read an UTF-8 encoded character.  */
+      unsigned char buf[6];
+      unsigned int count;
+      int c;
+      ucs4_t uc;
+
+      c = phase1_getc ();
+      if (c == EOF)
+        return UEOF;
+      buf[0] = c;
+      count = 1;
+
+      if (buf[0] >= 0xc0)
+        {
+          c = phase1_getc ();
+          if (c == EOF)
+            return UEOF;
+          buf[1] = c;
+          count = 2;
+        }
+
+      if (buf[0] >= 0xe0 && ((buf[1] ^ 0x80) < 0x40))
+        {
+          c = phase1_getc ();
+          if (c == EOF)
+            return UEOF;
+          buf[2] = c;
+          count = 3;
+        }
+
+      if (buf[0] >= 0xf0 && ((buf[1] ^ 0x80) < 0x40)
+          && ((buf[2] ^ 0x80) < 0x40))
+        {
+          c = phase1_getc ();
+          if (c == EOF)
+            return UEOF;
+          buf[3] = c;
+          count = 4;
+        }
+
+      if (buf[0] >= 0xf8 && ((buf[1] ^ 0x80) < 0x40)
+          && ((buf[2] ^ 0x80) < 0x40) && ((buf[3] ^ 0x80) < 0x40))
+        {
+          c = phase1_getc ();
+          if (c == EOF)
+            return UEOF;
+          buf[4] = c;
+          count = 5;
+        }
+
+      if (buf[0] >= 0xfc && ((buf[1] ^ 0x80) < 0x40)
+          && ((buf[2] ^ 0x80) < 0x40) && ((buf[3] ^ 0x80) < 0x40)
+          && ((buf[4] ^ 0x80) < 0x40))
+        {
+          c = phase1_getc ();
+          if (c == EOF)
+            return UEOF;
+          buf[5] = c;
+          count = 6;
+        }
+
+      u8_mbtouc (&uc, buf, count);
+      return uc;
+    }
+}
+
+/* Supports three characters of pushback.  */
+static void
+phase2_ungetc (int c)
+{
+  if (c != UEOF)
+    {
+      if (phase2_pushback_length == SIZEOF (phase2_pushback))
+        abort ();
+      phase2_pushback[phase2_pushback_length++] = c;
+    }
+}
+
+/* ========================= Accumulating strings.  ======================== */
+
+/* See xg-mixed-string.h for the API.  */
+
+/* ======================== Accumulating comments.  ======================== */
+
+/* Accumulating a single comment line.  */
+
+static struct mixed_string_buffer comment_buffer;
+
+static inline void
+comment_start ()
+{
+  mixed_string_buffer_init (&comment_buffer, lc_comment, logical_file_name,
+                            line_number);
+}
+
+static inline bool
+comment_at_start ()
+{
+  return mixed_string_buffer_is_empty (&comment_buffer);
+}
+
+static inline void
+comment_add (int c)
+{
+  mixed_string_buffer_append_unicode (&comment_buffer, c);
+}
+
+static inline const char *
+comment_line_end (size_t chars_to_remove)
+{
+  char *buffer = mixed_string_contents_free1 (
+      mixed_string_buffer_result (&comment_buffer));
+  size_t buflen = strlen (buffer) - chars_to_remove;
+
+  while (buflen >= 1
+         && (buffer[buflen - 1] == ' ' || buffer[buflen - 1] == '\t'))
+    --buflen;
+  buffer[buflen] = '\0';
+  savable_comment_add (buffer);
+  lexical_context = lc_outside;
+  return buffer;
+}
+
+/* These are for tracking whether comments count as immediately before
+   keyword.  */
+static int last_comment_line;
+static int last_non_comment_line;
+
+/* 3. Replace each comment that is not inside a character constant or
+   string literal with a space or newline character.  */
+
+static int
+phase3_getc ()
+{
+  int c;
+  bool last_was_star;
+  bool is_doc;
+
+  c = phase2_getc ();
+  if (c != '/')
+    return c;
+  c = phase2_getc ();
+  switch (c)
+    {
+    default:
+      phase2_ungetc (c);
+      return '/';
+
+    case '*':
+      /* C style comment.  */
+      last_was_star = false;
+      c = phase2_getc ();
+      if (c == '!' || c == '*')
+        {
+          /* Doc comment. Ignore. */
+          for (;;)
+            {
+              c = phase2_getc ();
+              if (c == UEOF)
+                break;
+              switch (c)
+                {
+                case UNL:
+                  last_was_star = false;
+                  continue;
+
+                case '*':
+                  last_was_star = true;
+                  continue;
+
+                case '/':
+                  if (last_was_star)
+                    {
+                      break;
+                    }
+                  FALLTHROUGH;
+
+                default:
+                  last_was_star = false;
+                  continue;
+                }
+              break;
+            }
+        }
+      else
+        {
+          phase2_ungetc (c);
+          comment_start ();
+          for (;;)
+            {
+              c = phase2_getc ();
+              if (c == UEOF)
+                break;
+              /* We skip all leading white space, but not EOLs.  */
+              if (!(comment_at_start () && (c == ' ' || c == '\t')))
+                comment_add (c);
+              switch (c)
+                {
+                case UNL:
+                  comment_line_end (1);
+                  comment_start ();
+                  last_was_star = false;
+                  continue;
+
+                case '*':
+                  last_was_star = true;
+                  continue;
+
+                case '/':
+                  if (last_was_star)
+                    {
+                      comment_line_end (2);
+                      break;
+                    }
+                  FALLTHROUGH;
+
+                default:
+                  last_was_star = false;
+                  continue;
+                }
+              break;
+            }
+          last_comment_line = line_number;
+        }
+      return ' ';
+
+    case '/':
+      /* C++ style comment.  */
+      c = phase2_getc ();
+      if (c == '!' || c == '/')
+        {
+          /* Doc comment. Ignore. */
+          for (;;)
+            {
+              c = phase2_getc ();
+              if (c == UNL || c == UEOF)
+                break;
+            }
+        }
+      else
+        {
+          phase2_ungetc (c);
+          last_comment_line = line_number;
+          comment_start ();
+          for (;;)
+            {
+              c = phase2_getc ();
+              if (c == UNL || c == UEOF)
+                break;
+              /* We skip all leading white space, but not EOLs.  */
+              if (!(comment_at_start () && (c == ' ' || c == '\t')))
+                comment_add (c);
+            }
+          phase2_ungetc (
+              c); /* push back the newline, to decrement logical_line_number */
+          comment_line_end (0);
+          phase2_getc (); /* read the newline again */
+        }
+      return UNL;
+    }
+}
+
+/* Supports only one pushback character.  */
+static void
+phase3_ungetc (int c)
+{
+  phase2_ungetc (c);
+}
+
+/* ========================== Reading of tokens.  ========================== */
+
+enum token_type_ty
+{
+  token_type_eof,
+  token_type_lparen,         /* ( */
+  token_type_rparen,         /* ) */
+  token_type_comma,          /* , */
+  token_type_dot,            /* . */
+  token_type_string_literal, /* "abc", r"abc", b"abc" */
+  token_type_number,         /* 1.23 */
+  token_type_symbol,         /* identifier, keyword, null */
+  token_type_plus,           /* + */
+  token_type_other           /* character literal, misc. operator */
+};
+typedef enum token_type_ty token_type_ty;
+
+typedef struct token_ty token_ty;
+struct token_ty
+{
+  token_type_ty type;
+  char *string;                       /* for token_type_symbol */
+  mixed_string_ty *mixed_string;      /* for token_type_string_literal */
+  refcounted_string_list_ty *comment; /* for token_type_string_literal */
+  int line_number;
+};
+
+/* Free the memory pointed to by a 'struct token_ty'.  */
+static inline void
+free_token (token_ty *tp)
+{
+  if (tp->type == token_type_symbol)
+    free (tp->string);
+  if (tp->type == token_type_string_literal)
+    {
+      mixed_string_free (tp->mixed_string);
+      drop_reference (tp->comment);
+    }
+}
+
+/* Read a Unicode escape sequence.
+   See the Rust Reference section 2.6.  */
+static int
+do_getc_unicode_escaped ()
+{
+  int c;
+
+  /* Use phase 2, because phase 3 elides comments.  */
+  c = phase2_getc ();
+  if (c == UEOF)
+    return '\\';
+  if (c == 'u')
+    {
+      int c1 = phase2_getc ();
+      if (c1 == '{')
+        {
+          unsigned char buf[6];
+          unsigned int n;
+          int i;
+
+          n = 0;
+          for (i = 0; i < 6; i++)
+            {
+              int c2 = phase2_getc ();
+
+              if (c2 >= '0' && c2 <= '9')
+                n = (n << 4) + (c2 - '0');
+              else if (c2 >= 'A' && c2 <= 'F')
+                n = (n << 4) + (c2 - 'A' + 10);
+              else if (c2 >= 'a' && c2 <= 'f')
+                n = (n << 4) + (c2 - 'a' + 10);
+              else if (i >= 1 && c2 == '}')
+                {
+                  return n;
+                }
+              else
+                {
+                  phase2_ungetc (c2);
+                  while (--i >= 0)
+                    phase2_ungetc (buf[i]);
+                  phase2_ungetc (c1);
+                  phase2_ungetc (c);
+                  return '\\';
+                }
+              buf[i] = c2;
+            }
+        }
+      phase2_ungetc (c1);
+    }
+  phase2_ungetc (c);
+  return '\\';
+}
+
+/* Read an escape sequence inside a string literal or character literal.
+   See the Rust Reference section 2.6.  */
+static int
+do_getc_escaped ()
+{
+  int c;
+  int n;
+  int i;
+
+  /* Use phase 2, because phase 3 elides comments.  */
+  c = phase2_getc ();
+  if (c == UEOF)
+    return '\\';
+  switch (c)
+    {
+    case 't':
+      return 0x0009;
+    case 'n':
+      return 0x000a;
+    case 'r':
+      return 0x000d;
+    case '"':
+      return '"';
+    case '\'':
+      return '\'';
+    case '\\':
+      return '\\';
+    case '0':
+      return 0x0000;
+    case 'x':
+      c = phase2_getc ();
+      switch (c)
+        {
+        default:
+          phase2_ungetc (c);
+          phase2_ungetc ('x');
+          return '\\';
+
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+        case 'A':
+        case 'B':
+        case 'C':
+        case 'D':
+        case 'E':
+        case 'F':
+        case 'a':
+        case 'b':
+        case 'c':
+        case 'd':
+        case 'e':
+        case 'f':
+          break;
+        }
+      n = 0;
+      for (i = 0;; i++)
+        {
+          switch (c)
+            {
+            default:
+              phase2_ungetc (c);
+              return n;
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+              n = n * 16 + c - '0';
+              break;
+            case 'A':
+            case 'B':
+            case 'C':
+            case 'D':
+            case 'E':
+            case 'F':
+              n = n * 16 + 10 + c - 'A';
+              break;
+            case 'a':
+            case 'b':
+            case 'c':
+            case 'd':
+            case 'e':
+            case 'f':
+              n = n * 16 + 10 + c - 'a';
+              break;
+            }
+          if (i == 1)
+            break;
+          c = phase2_getc ();
+        }
+      return n;
+    case 'u':
+      phase2_ungetc (c);
+      return do_getc_unicode_escaped ();
+    default:
+      /* Invalid escape sequence.  */
+      phase2_ungetc (c);
+      return '\\';
+    }
+}
+
+/* Read a regular string literal or character literal.
+   See the Rust Reference section 2.6.  */
+static void
+accumulate_escaped (struct mixed_string_buffer *literal, int delimiter)
+{
+  int c;
+  int count = 0;
+
+  for (;;)
+    {
+      /* Use phase 2, because phase 3 elides comments.  */
+      c = phase2_getc ();
+      if (c == UEOF || c == delimiter)
+        break;
+      if (delimiter == '\'' && count >= 1
+          && (c == ' ' || c == '\t' || c == '\f' || c == UNL))
+        {
+          /* Lifetime */
+          break;
+        }
+      if (c == UNL)
+        {
+          phase2_ungetc (c);
+          error_with_progname = false;
+          if (delimiter == '\'')
+            error (0, 0,
+                   _ ("%s:%d: warning: unterminated character constant or "
+                      "unnamed lifetime"),
+                   logical_file_name, line_number);
+          else
+            error (0, 0, _ ("%s:%d: warning: unterminated string constant"),
+                   logical_file_name, line_number);
+          error_with_progname = true;
+          break;
+        }
+      if (c == '\\')
+        c = do_getc_escaped ();
+      if (literal)
+        mixed_string_buffer_append_unicode (literal, c);
+      count++;
+    }
+}
+
+/* Combine characters into tokens.  Discard whitespace.  */
+
+/* Maximum used guaranteed to be < 4.  */
+static token_ty phase4_pushback[4];
+static int phase4_pushback_length;
+
+static void
+phase4_get (token_ty *tp)
+{
+  int c;
+
+  if (phase4_pushback_length)
+    {
+      *tp = phase4_pushback[--phase4_pushback_length];
+      return;
+    }
+  tp->string = NULL;
+
+  for (;;)
+    {
+      tp->line_number = line_number;
+      c = phase3_getc ();
+
+      if (c == UEOF)
+        {
+          tp->type = token_type_eof;
+          return;
+        }
+
+      switch (c)
+        {
+        case UNL:
+          if (last_non_comment_line > last_comment_line)
+            savable_comment_reset ();
+          FALLTHROUGH;
+        case ' ':
+        case '\t':
+        case '\f':
+          /* Ignore whitespace and comments.  */
+          continue;
+        }
+
+      last_non_comment_line = tp->line_number;
+
+      switch (c)
+        {
+        case '(':
+          tp->type = token_type_lparen;
+          return;
+
+        case ')':
+          tp->type = token_type_rparen;
+          return;
+
+        case ',':
+          tp->type = token_type_comma;
+          return;
+
+        case '.':
+          c = phase3_getc ();
+          if (!(c >= '0' && c <= '9'))
+            {
+              phase3_ungetc (c);
+              tp->type = token_type_dot;
+              return;
+            }
+          FALLTHROUGH;
+
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+          {
+            /* Don't need to verify the complicated syntax of integers and
+               floating-point numbers.  We assume a valid C# input.
+               The simplified syntax that we recognize as number is: any
+               sequence of alphanumeric characters, additionally '+' and '-'
+               immediately after 'e' or 'E' except in non-float numbers.  */
+            bool non_float = false;
+
+            for (;;)
+              {
+                c = phase3_getc ();
+                if (c >= '0' && c <= '9')
+                  continue;
+                if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
+                  {
+                    if (c == 'x' || c == 'b' || c == 'o')
+                      non_float = true;
+                    if ((c == 'E' || c == 'e') && !non_float)
+                      {
+                        c = phase3_getc ();
+                        if (!(c == '+' || c == '-'))
+                          phase3_ungetc (c);
+                      }
+                    continue;
+                  }
+                if (c == '.')
+                  continue;
+                break;
+              }
+            phase3_ungetc (c);
+            tp->type = token_type_number;
+            return;
+          }
+
+        case '"':
+          /* Regular string literal.  */
+          {
+            struct mixed_string_buffer literal;
+
+            lexical_context = lc_string;
+            mixed_string_buffer_init (&literal, lexical_context,
+                                      logical_file_name, line_number);
+            accumulate_escaped (&literal, '"');
+            tp->mixed_string = mixed_string_buffer_result (&literal);
+            tp->comment = add_reference (savable_comment);
+            lexical_context = lc_outside;
+            tp->type = token_type_string_literal;
+            return;
+          }
+
+        case '\'':
+          /* Character literal or lifetime.  */
+          {
+            accumulate_escaped (NULL, '\'');
+            tp->type = token_type_other;
+            return;
+          }
+
+        case '+':
+          c = phase3_getc ();
+          if (c == '+')
+            /* Operator ++ */
+            tp->type = token_type_other;
+          else if (c == '=')
+            /* Operator += */
+            tp->type = token_type_other;
+          else
+            {
+              /* Operator + */
+              phase3_ungetc (c);
+              tp->type = token_type_plus;
+            }
+          return;
+
+        case 'b':
+          {
+            int c1 = phase3_getc ();
+            if (c1 == '"')
+              {
+                /* Byte string literal */
+                struct mixed_string_buffer literal;
+
+                lexical_context = lc_string;
+                mixed_string_buffer_init (&literal, lexical_context,
+                                          logical_file_name, line_number);
+                accumulate_escaped (&literal, '"');
+                tp->mixed_string = mixed_string_buffer_result (&literal);
+                tp->comment = add_reference (savable_comment);
+                lexical_context = lc_outside;
+                tp->type = token_type_string_literal;
+                return;
+              }
+            phase3_ungetc (c1);
+            FALLTHROUGH; /* So that identifiers starting with b are recognized.
+                          */
+          }
+        case 'r':
+          {
+            int hash_count = 0;
+            int c1 = phase3_getc ();
+            int c2 = c1;
+            if (c1 == '#')
+              {
+                do
+                  {
+                    hash_count++;
+                    c2 = phase3_getc ();
+                  }
+                while (c2 == '#');
+              }
+            if (c2 == '"')
+              {
+                /* Raw string literal.  */
+                struct mixed_string_buffer literal;
+
+                lexical_context = lc_string;
+                mixed_string_buffer_init (&literal, lexical_context,
+                                          logical_file_name, line_number);
+                for (;;)
+                  {
+                    /* Use phase 2, because phase 3 elides comments.  */
+                    c = phase2_getc ();
+                    if (c == UEOF)
+                      break;
+                    if (c == '"')
+                      {
+                        int end_hash_count = 0;
+                        c = phase2_getc ();
+                        if (hash_count == 0)
+                          {
+                            phase2_ungetc (c);
+                            break;
+                          }
+                        else if (c == '#')
+                          {
+                            do
+                              {
+                                end_hash_count++;
+                                c = phase2_getc ();
+                              }
+                            while (c == '#');
+                            if (end_hash_count == hash_count)
+                              {
+                                phase2_ungetc (c);
+                                break;
+                              }
+                          }
+                      }
+                    /* No special treatment of newline and backslash here.  */
+                    mixed_string_buffer_append_unicode (&literal, c);
+                  }
+                tp->mixed_string = mixed_string_buffer_result (&literal);
+                tp->comment = add_reference (savable_comment);
+                lexical_context = lc_outside;
+                tp->type = token_type_string_literal;
+                return;
+              }
+            if (hash_count == 0)
+              {
+                phase3_ungetc (c2);
+              }
+            phase3_ungetc (c1);
+            FALLTHROUGH; /* so that identifiers starting with r and
+                            r#identifier are recognized.  */
+          }
+        default:
+          if (uc_is_property_xid_start (c) || c == '_')
+            {
+              struct mixed_string_buffer buffer;
+              mixed_string_ty *mixed_string;
+
+              mixed_string_buffer_init (&buffer, lexical_context,
+                                        logical_file_name, line_number);
+              for (;;)
+                {
+                  mixed_string_buffer_append_unicode (&buffer, c);
+                  c = phase3_getc ();
+                  if (!uc_is_property_xid_continue (c))
+                    break;
+                }
+              if (c != '!') /* Macro invocation operator */
+                phase3_ungetc (c);
+              mixed_string = mixed_string_buffer_result (&buffer);
+              tp->string = mixed_string_contents (mixed_string);
+              mixed_string_free (mixed_string);
+              tp->type = token_type_symbol;
+              return;
+            }
+          else
+            {
+              /* Misc. operator.  */
+              tp->type = token_type_other;
+              return;
+            }
+        }
+    }
+}
+
+/* Supports 3 tokens of pushback.  */
+static void
+phase4_unget (token_ty *tp)
+{
+  if (tp->type != token_type_eof)
+    {
+      if (phase4_pushback_length == SIZEOF (phase4_pushback))
+        abort ();
+      phase4_pushback[phase4_pushback_length++] = *tp;
+    }
+}
+
+/* Compile-time optimization of string literal concatenation.
+   Combine "string1" + ... + "stringN" to the concatenated string if
+     - the token after this expression is not '.' (because then the last
+       string could be part of a method call expression).  */
+
+static token_ty phase5_pushback[2];
+static int phase5_pushback_length;
+
+static void
+phase5_get (token_ty *tp)
+{
+  if (phase5_pushback_length)
+    {
+      *tp = phase5_pushback[--phase5_pushback_length];
+      return;
+    }
+
+  phase4_get (tp);
+  if (tp->type == token_type_string_literal)
+    {
+      mixed_string_ty *sum = tp->mixed_string;
+
+      for (;;)
+        {
+          token_ty token2;
+
+          phase4_get (&token2);
+          if (token2.type == token_type_plus)
+            {
+              token_ty token3;
+
+              phase4_get (&token3);
+              if (token3.type == token_type_string_literal)
+                {
+                  token_ty token_after;
+
+                  phase4_get (&token_after);
+                  if (token_after.type != token_type_dot)
+                    {
+                      sum = mixed_string_concat_free1 (sum,
+                                                       token3.mixed_string);
+
+                      phase4_unget (&token_after);
+                      free_token (&token3);
+                      free_token (&token2);
+                      continue;
+                    }
+                  phase4_unget (&token_after);
+                }
+              phase4_unget (&token3);
+            }
+          phase4_unget (&token2);
+          break;
+        }
+      tp->mixed_string = sum;
+    }
+}
+
+/* Supports 2 tokens of pushback.  */
+static void
+phase5_unget (token_ty *tp)
+{
+  if (tp->type != token_type_eof)
+    {
+      if (phase5_pushback_length == SIZEOF (phase5_pushback))
+        abort ();
+      phase5_pushback[phase5_pushback_length++] = *tp;
+    }
+}
+
+static void
+x_rust_lex (token_ty *tp)
+{
+  phase5_get (tp);
+}
+
+/* Supports 2 tokens of pushback.  */
+static void
+x_rust_unlex (token_ty *tp)
+{
+  phase5_unget (tp);
+}
+
+/* ========================= Extracting strings.  ========================== */
+
+/* Context lookup table.  */
+static flag_context_list_table_ty *flag_context_list_table;
+
+/* The file is broken into tokens.  Scan the token stream, looking for
+   a keyword, followed by a left paren, followed by a string.  When we
+   see this sequence, we have something to remember.  We assume we are
+   looking at a valid Rust program, and leave the complaints about
+   the grammar to the compiler.
+
+     Normal handling: Look for
+       keyword ( ... msgid ... )
+     Plural handling: Look for
+       keyword ( ... msgid ... msgid_plural ... )
+
+   We use recursion because the arguments before msgid or between msgid
+   and msgid_plural can contain subexpressions of the same form.  */
+
+/* Extract messages until the next balanced closing parenthesis.
+   Extracted messages are added to MLP.
+   Return true upon eof, false upon closing parenthesis.  */
+static bool
+extract_parenthesized (message_list_ty *mlp, flag_context_ty outer_context,
+                       flag_context_list_iterator_ty context_iter,
+                       struct arglist_parser *argparser)
+{
+  /* Current argument number.  */
+  int arg = 1;
+  /* 0 when no keyword has been seen.  1 right after a keyword is seen.  */
+  int state;
+  /* Parameters of the keyword just seen.  Defined only in state 1.  */
+  const struct callshapes *next_shapes = NULL;
+  /* Context iterator that will be used if the next token is a '('.  */
+  flag_context_list_iterator_ty next_context_iter
+      = passthrough_context_list_iterator;
+  /* Current context.  */
+  flag_context_ty inner_context = inherited_context (
+      outer_context, flag_context_list_iterator_advance (&context_iter));
+
+  /* Start state is 0.  */
+  state = 0;
+
+  for (;;)
+    {
+      token_ty token;
+
+      x_rust_lex (&token);
+      switch (token.type)
+        {
+        case token_type_symbol:
+          {
+            /* Combine symbol1 . ... . symbolN to a single strings, so that
+                  we can recognize static function calls like
+                  Catalog.gettext.  The information present for
+                  symbolI.....symbolN has precedence over the information for
+                  symbolJ.....symbolN with J > I.  */
+            char *sum = token.string;
+            size_t sum_len = strlen (sum);
+            const char *dottedname;
+            flag_context_list_ty *context_list;
+
+            for (;;)
+              {
+                token_ty token2;
+
+                x_rust_lex (&token2);
+                if (token2.type == token_type_dot)
+                  {
+                    token_ty token3;
+
+                    x_rust_lex (&token3);
+                    if (token3.type == token_type_symbol)
+                      {
+                        char *addend = token3.string;
+                        size_t addend_len = strlen (addend);
+
+                        sum = (char *)xrealloc (sum,
+                                                sum_len + 1 + addend_len + 1);
+                        sum[sum_len] = '.';
+                        memcpy (sum + sum_len + 1, addend, addend_len + 1);
+                        sum_len += 1 + addend_len;
+
+                        free_token (&token3);
+                        free_token (&token2);
+                        continue;
+                      }
+                    x_rust_unlex (&token3);
+                  }
+                x_rust_unlex (&token2);
+                break;
+              }
+
+            for (dottedname = sum;;)
+              {
+                void *keyword_value;
+
+                if (hash_find_entry (&keywords, dottedname,
+                                     strlen (dottedname), &keyword_value)
+                    == 0)
+                  {
+                    next_shapes = (const struct callshapes *)keyword_value;
+                    state = 1;
+                    break;
+                  }
+
+                dottedname = strchr (dottedname, '.');
+                if (dottedname == NULL)
+                  {
+                    state = 0;
+                    break;
+                  }
+                dottedname++;
+              }
+
+            for (dottedname = sum;;)
+              {
+                context_list = flag_context_list_table_lookup (
+                    flag_context_list_table, dottedname, strlen (dottedname));
+                if (context_list != NULL)
+                  break;
+
+                dottedname = strchr (dottedname, '.');
+                if (dottedname == NULL)
+                  break;
+                dottedname++;
+              }
+            next_context_iter = flag_context_list_iterator (context_list);
+
+            free (sum);
+            continue;
+          }
+
+        case token_type_lparen:
+          if (extract_parenthesized (
+                  mlp, inner_context, next_context_iter,
+                  arglist_parser_alloc (mlp, state ? next_shapes : NULL)))
+            {
+              arglist_parser_done (argparser, arg);
+              return true;
+            }
+          next_context_iter = null_context_list_iterator;
+          state = 0;
+          continue;
+
+        case token_type_rparen:
+          arglist_parser_done (argparser, arg);
+          return false;
+          next_context_iter = null_context_list_iterator;
+          state = 0;
+          continue;
+
+        case token_type_comma:
+          arg++;
+          inner_context = inherited_context (
+              outer_context,
+              flag_context_list_iterator_advance (&context_iter));
+          next_context_iter = passthrough_context_list_iterator;
+          state = 0;
+          continue;
+
+        case token_type_string_literal:
+          {
+            lex_pos_ty pos;
+
+            pos.file_name = logical_file_name;
+            pos.line_number = token.line_number;
+
+            if (extract_all)
+              {
+                char *string = mixed_string_contents (token.mixed_string);
+                mixed_string_free (token.mixed_string);
+                remember_a_message (mlp, NULL, string, true, false,
+                                    inner_context, &pos, NULL, token.comment,
+                                    true);
+              }
+            else
+              arglist_parser_remember (argparser, arg, token.mixed_string,
+                                       inner_context, pos.file_name,
+                                       pos.line_number, token.comment, true);
+          }
+          drop_reference (token.comment);
+          next_context_iter = null_context_list_iterator;
+          state = 0;
+          continue;
+
+        case token_type_eof:
+          arglist_parser_done (argparser, arg);
+          return true;
+
+        case token_type_dot:
+        case token_type_number:
+        case token_type_plus:
+        case token_type_other:
+          next_context_iter = null_context_list_iterator;
+          state = 0;
+          continue;
+
+        default:
+          abort ();
+        }
+    }
+}
+
+void
+extract_rust (FILE *f, const char *real_filename, const char *logical_filename,
+              flag_context_list_table_ty *flag_table, msgdomain_list_ty *mdlp)
+{
+  message_list_ty *mlp = mdlp->item[0]->messages;
+
+  fp = f;
+  real_file_name = real_filename;
+  logical_file_name = xstrdup (logical_filename);
+  line_number = 1;
+
+  phase1_pushback_length = 0;
+
+  lexical_context = lc_outside;
+
+  phase2_pushback_length = 0;
+
+  last_comment_line = -1;
+  last_non_comment_line = -1;
+
+  xgettext_current_source_encoding = (xgettext_global_source_encoding != NULL
+                                          ? xgettext_global_source_encoding
+                                          : po_charset_utf8);
+
+  phase4_pushback_length = 0;
+  phase5_pushback_length = 0;
+
+  flag_context_list_table = flag_table;
+
+  init_keywords ();
+
+  /* Eat tokens until eof is seen.  When extract_parenthesized returns
+     due to an unbalanced closing parenthesis, just restart it.  */
+  while (!extract_parenthesized (mlp, null_context, null_context_list_iterator,
+                                 arglist_parser_alloc (mlp, NULL)))
+    ;
+
+  /* Close scanner.  */
+  fp = NULL;
+  real_file_name = NULL;
+  logical_file_name = NULL;
+  line_number = 0;
+}
diff --git a/gettext-tools/src/x-rust.h b/gettext-tools/src/x-rust.h
new file mode 100644
index 000000000..c71d2e3e7
--- /dev/null
+++ b/gettext-tools/src/x-rust.h
@@ -0,0 +1,45 @@
+/* xgettext Rust backend.
+   Copyright (C) 2022 Free Software Foundation, Inc.
+   Written by Kévin Commaille <zecakeh@tedomum.fr>, 2022.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#include <stdio.h>
+
+#include "message.h"
+#include "xg-arglist-context.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#define EXTENSIONS_RUST { "rs", "Rust" },
+
+#define SCANNERS_RUST                                                         \
+  { "Rust", extract_rust, NULL, &flag_table_rust, &formatstring_rust, NULL },
+
+  extern void extract_rust (FILE *fp, const char *real_filename,
+                            const char *logical_filename,
+                            flag_context_list_table_ty *flag_table,
+                            msgdomain_list_ty *mdlp);
+
+  extern void x_rust_keyword (const char *keyword);
+  extern void x_rust_extract_all (void);
+
+  extern void init_flag_table_rust (void);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/gettext-tools/src/xgettext.c b/gettext-tools/src/xgettext.c
index 0259f6da5..5fc18710d 100644
--- a/gettext-tools/src/xgettext.c
+++ b/gettext-tools/src/xgettext.c
@@ -118,6 +118,7 @@
 #include "x-glade.h"
 #include "x-gsettings.h"
 #include "x-appdata.h"
+#include "x-rust.h"
 
 
 #define SIZEOF(a) (sizeof(a) / sizeof(a[0]))
@@ -197,6 +198,7 @@ static flag_context_list_table_ty flag_table_tcl;
 static flag_context_list_table_ty flag_table_perl;
 static flag_context_list_table_ty flag_table_php;
 static flag_context_list_table_ty flag_table_ycp;
+static flag_context_list_table_ty flag_table_rust;
 
 /* If true, recognize Qt format strings.  */
 static bool recognize_format_qt;
@@ -383,6 +385,7 @@ main (int argc, char *argv[])
   init_flag_table_php ();
   init_flag_table_gcc_internal ();
   init_flag_table_ycp ();
+  init_flag_table_rust ();
 
   while ((optchar = getopt_long (argc, argv,
                                  "ac::Cd:D:eEf:Fhijk::l:L:m::M::no:p:sTvVw:W:x:",
@@ -410,6 +413,7 @@ main (int argc, char *argv[])
         x_lua_extract_all ();
         x_javascript_extract_all ();
         x_vala_extract_all ();
+        x_rust_extract_all ();
         break;
 
       case 'c':
@@ -491,6 +495,7 @@ main (int argc, char *argv[])
         x_javascript_keyword (optarg);
         x_vala_keyword (optarg);
         x_desktop_keyword (optarg);
+        x_rust_keyword (optarg);
         if (optarg == NULL)
           no_default_keywords = true;
         else
@@ -1100,7 +1105,7 @@ Choice of input file language:\n"));
                                 EmacsLisp, librep, Scheme, Smalltalk, Java,\n\
                                 JavaProperties, C#, awk, YCP, Tcl, Perl, PHP,\n\
                                 Ruby, GCC-source, NXStringTable, RST, RSJ,\n\
-                                Glade, Lua, JavaScript, Vala, Desktop)\n"));
+                                Glade, Lua, JavaScript, Vala, Desktop, Rust)\n"));
       printf (_("\
   -C, --c++                   shorthand for --language=C++\n"));
       printf (_("\
@@ -1633,6 +1638,11 @@ xgettext_record_flag (const char *optionstring)
                                                     name_start, name_end,
                                                     argnum, value, pass);
                     break;
+                  case format_rust:
+                    flag_context_list_table_insert (&flag_table_rust, 0,
+                                                    name_start, name_end,
+                                                    argnum, value, pass);
+                    break;
                   default:
                     abort ();
                   }
@@ -2178,6 +2188,7 @@ language_to_extractor (const char *name)
     SCANNERS_GLADE
     SCANNERS_GSETTINGS
     SCANNERS_APPDATA
+    SCANNERS_RUST
     /* Here may follow more languages and their scanners: pike, etc...
        Make sure new scanners honor the --exclude-file option.  */
   };
@@ -2270,6 +2281,7 @@ extension_to_language (const char *extension)
     EXTENSIONS_GLADE
     EXTENSIONS_GSETTINGS
     EXTENSIONS_APPDATA
+    EXTENSIONS_RUST
     /* Here may follow more file extensions... */
   };
 
diff --git a/gettext-tools/tests/Makefile.am b/gettext-tools/tests/Makefile.am
index 68f833c0d..df58c35aa 100644
--- a/gettext-tools/tests/Makefile.am
+++ b/gettext-tools/tests/Makefile.am
@@ -118,6 +118,8 @@ TESTS = gettext-1 gettext-2 \
 	xgettext-python-1 xgettext-python-2 xgettext-python-3 \
 	xgettext-python-4 \
 	xgettext-ruby-1 \
+	xgettext-rust-1 xgettext-rust-2 xgettext-rust-3 xgettext-rust-4 \
+	xgettext-rust-5 \
 	xgettext-scheme-1 xgettext-scheme-2 xgettext-scheme-3 \
 	xgettext-scheme-4 \
 	xgettext-sh-1 xgettext-sh-2 xgettext-sh-3 xgettext-sh-4 xgettext-sh-5 \
@@ -152,6 +154,7 @@ TESTS = gettext-1 gettext-2 \
 	format-qt-1 format-qt-2 \
 	format-qt-plural-1 format-qt-plural-2 \
 	format-ruby-1 format-ruby-2 \
+	format-rust-1 format-rust-2 \
 	format-scheme-1 format-scheme-2 \
 	format-sh-1 format-sh-2 \
 	format-tcl-1 format-tcl-2 \
diff --git a/gettext-tools/tests/format-rust-1 b/gettext-tools/tests/format-rust-1
new file mode 100755
index 000000000..b5a081af0
--- /dev/null
+++ b/gettext-tools/tests/format-rust-1
@@ -0,0 +1,103 @@
+#! /bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# Test recognition of Rust format strings.
+
+cat <<\EOF > f-rs-1.data
+# Valid: one numbered argument
+"abc{0}def"
+# Valid: ten numbered arguments
+"abc{9}def"
+# Valid: two-digit numbered argument
+"abc{00}def"
+# Valid: huge numbered argument
+"abc{500000000}def"
+# Invalid: unterminated
+"abc{"
+# Invalid: unterminated
+"abc{0"
+# Valid: unnumber argument
+"abc{}def"
+# Valid: named argument
+"abc{number}def"
+# Invalid: illegal character
+"abc{-0}def"
+# Valid: two numbered arguments
+"abc{1}def{0}"
+# Valid: multiple uses of same argument
+"abc{1}def{0}ghi{1}"
+# Valid: mixed argument
+"abc{0}d{}e{n}f"
+# Invalid: numbered after named argument
+"abc{n}de{0}f"
+# Invalid: missing format specifier
+"abc{0:}def"
+# Invalid: invalid specifier
+"abc{0:a}def"
+# Valid: valid width
+"abc{1:-7}def"
+# Valid: format specifier
+"abc{1:p}def"
+# Valid: width and format specifiers
+"abc{1:3x}def"
+# Valid: precision
+"abc1def{0:.8}"
+# Valid: fill and align specifiers
+"abc{1:@>}def"
+# Valid: align specifier
+"abc{1:^}def"
+# Valid: sign, alternate, and zero-padding specifiers
+"abc{1:-#0}def"
+# Invalid: wrong order of specifiers
+"abc1def{0:o.8}"
+# Valid: width as named argument
+"abc{1:width$}def"
+# Invalid: width as named argument without '$'
+"abc{1:width}def"
+# Valid: precision as numbered argument
+"abc{1:.1$}def"
+# Valid: sign, alternate, and zero-padding specifiers
+"abc{1:-#0}def"
+# Invalid: missing opening brace
+"abc1}def{0}"
+# Valid: doubled brace
+"abc1}}def{0}"
+# Invalid: doubled brace doesn't start a directive
+"abc{{0}def"
+EOF
+
+: ${XGETTEXT=xgettext}
+n=0
+while read comment; do
+  read string
+  n=`expr $n + 1`
+  cat <<EOF > f-rs-1-$n.in
+gettext(${string});
+EOF
+  ${XGETTEXT} -L Rust -o f-rs-1-$n.po f-rs-1-$n.in || Exit 1
+  test -f f-rs-1-$n.po || Exit 1
+  fail=
+  if echo "$comment" | grep 'Valid:' > /dev/null; then
+    if grep rust-format f-rs-1-$n.po > /dev/null; then
+      :
+    else
+      fail=yes
+    fi
+  else
+    if grep rust-format f-rs-1-$n.po > /dev/null; then
+      fail=yes
+    else
+      :
+    fi
+  fi
+  if test -n "$fail"; then
+    echo "Format string recognition error:" 1>&2
+    cat f-rs-1-$n.in 1>&2
+    echo "Got:" 1>&2
+    cat f-rs-1-$n.po 1>&2
+    Exit 1
+  fi
+  rm -f f-rs-1-$n.in f-rs-1-$n.po
+done < f-rs-1.data
+
+Exit 0
diff --git a/gettext-tools/tests/format-rust-2 b/gettext-tools/tests/format-rust-2
new file mode 100755
index 000000000..372c27b9d
--- /dev/null
+++ b/gettext-tools/tests/format-rust-2
@@ -0,0 +1,67 @@
+#! /bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# Test checking of Rust format strings.
+
+cat <<\EOF > f-rs-2.data
+# Invalid: invalid msgstr
+msgid  "abc{0}def"
+msgstr "abc{"
+# Valid: same numbered arguments
+msgid  "abc{1}def"
+msgstr "xyz{1}"
+# Valid: same numbered arguments, differently written
+msgid  "abc{1}def"
+msgstr "xyz{01}"
+# Invalid: different numbered arguments
+msgid  "abc{500000000}def"
+msgstr "xyz{500000001}"
+# Valid: same named arguments
+msgid  "abc{n}def"
+msgstr "xyz{n}"
+# Valid: permutation
+msgid  "abc{2}{0}{1}def"
+msgstr "xyz{1}{0}{2}"
+# Invalid: too few arguments
+msgid  "abc{1}def{0}"
+msgstr "xyz{0}"
+# Invalid: too many arguments
+msgid  "abc{0}def"
+msgstr "xyz{0}uvw{n}"
+EOF
+
+: ${MSGFMT=msgfmt}
+n=0
+while read comment; do
+  read msgid_line
+  read msgstr_line
+  n=`expr $n + 1`
+  cat <<EOF > f-rs-2-$n.po
+#, rust-format
+${msgid_line}
+${msgstr_line}
+EOF
+  fail=
+  if echo "$comment" | grep 'Valid:' > /dev/null; then
+    if ${MSGFMT} --check-format -o f-rs-2-$n.mo f-rs-2-$n.po; then
+      :
+    else
+      fail=yes
+    fi
+  else
+    ${MSGFMT} --check-format -o f-rs-2-$n.mo f-rs-2-$n.po 2> /dev/null
+    if test $? = 1; then
+      :
+    else
+      fail=yes
+    fi
+  fi
+  if test -n "$fail"; then
+    echo "Format string checking error:" 1>&2
+    cat f-rs-2-$n.po 1>&2
+    Exit 1
+  fi
+  rm -f f-rs-2-$n.po f-rs-2-$n.mo
+done < f-rs-2.data
+
+Exit 0
diff --git a/gettext-tools/tests/xgettext-rust-1 b/gettext-tools/tests/xgettext-rust-1
new file mode 100755
index 000000000..1c707cd38
--- /dev/null
+++ b/gettext-tools/tests/xgettext-rust-1
@@ -0,0 +1,75 @@
+#!/bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# Some tests for Rust support
+
+cat <<EOF > xg-rs-1.rs
+use gettext_rs::gettext;
+
+pub trait TestCase {
+  fn TestCase() {
+    GettextResourceManager rm = new GettextResourceManager("test");
+    // standard usage
+    String test1 = gettext("Test String 1");
+    /* C style comment */
+    String test2 = gettext("Test String 2");
+    // "multiline" string
+    String test3 = gettext("Test " +
+    "String " +
+    "3");
+    // macro
+    String test2 = gettext!("Test String 4");
+    // empty string
+    String test4 = gettext("");
+    // byte string
+    String test2 = gettext(b"Test String 5");
+    // raw string
+    String test5 = gettext(r#"first line
+second line
+third line"#);
+  }
+}
+EOF
+
+: ${XGETTEXT=xgettext}
+${XGETTEXT} --omit-header --no-location -c -d xg-rs-1.tmp xg-rs-1.rs || Exit 1
+LC_ALL=C tr -d '\r' < xg-rs-1.tmp.po > xg-rs-1.po || Exit 1
+
+cat <<EOF > xg-rs-1.ok
+#. standard usage
+msgid "Test String 1"
+msgstr ""
+
+#. C style comment
+msgid "Test String 2"
+msgstr ""
+
+#. "multiline" string
+msgid "Test String 3"
+msgstr ""
+
+#. macro
+msgid "Test String 4"
+msgstr ""
+
+#. empty string
+msgid ""
+msgstr ""
+
+#. byte string
+msgid "Test String 5"
+msgstr ""
+
+#. raw string
+msgid ""
+"first line\n"
+"second line\n"
+"third line"
+msgstr ""
+EOF
+
+: ${DIFF=diff}
+${DIFF} xg-rs-1.ok xg-rs-1.po
+result=$?
+
+exit $result
diff --git a/gettext-tools/tests/xgettext-rust-2 b/gettext-tools/tests/xgettext-rust-2
new file mode 100755
index 000000000..1f861c29b
--- /dev/null
+++ b/gettext-tools/tests/xgettext-rust-2
@@ -0,0 +1,82 @@
+#!/bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# More tests for Rust support: UTF-8 characters
+
+cat <<\EOF > xg-rs-2.rs
+pub trait TestCase {
+  fn main () {
+    catalog.gettext("Russian (Русский): Здравствуйте"));
+    catalog.gettext("Vietnamese (Tiếng Việt): Chào bạn"));
+    catalog.gettext("Japanese (日本語): こんにちは"));
+    catalog.gettext("Thai (ภาษาไทย): สวัสดีครับ"));
+    catalog.gettext("Script: 𝒞"));
+    catalog.gettext("Russian (\u{0420}\u{0443}\u{0441}\u{0441}\u{043a}\u{0438}\u{0439}): \u{0417}\u{0434}\u{0440}\u{0430}\u{0432}\u{0441}\u{0442}\u{0432}\u{0443}\u{0439}\u{0442}\u{0435}"));
+    catalog.gettext("Vietnamese (Ti\u{1ebf}ng Vi\u{1ec7}t): Ch\u{00e0}o b\u{1ea1}n"));
+    catalog.gettext("Japanese (\u{65e5}\u{672c}\u{8a9e}): \u{3053}\u{3093}\u{306b}\u{3061}\u{306f}"));
+    catalog.gettext("Thai (\u{0e20}\u{0e32}\u{0e29}\u{0e32}\u{0e44}\u{0e17}\u{0e22}): \u{0e2a}\u{0e27}\u{0e31}\u{0e2a}\u{0e14}\u{0e35}\u{0e04}\u{0e23}\u{0e31}\u{0e1a}"));
+    catalog.gettext("Script: \u{1d49e}"));
+    // And now a comment with Русский and 日本語 and Unicode escapes: B\u{00f6}se B\u{00fc}bchen
+    catalog.gettext("This string has a multilingual comment"));
+    // Unicode identifiers.
+    String あ = "";
+    String 𐀀 = "";
+  }
+}
+EOF
+
+: ${XGETTEXT=xgettext}
+# delete POT-Creation-Date: line because the date depends on local time.
+${XGETTEXT} --output xg-rs-2.tmp --add-location -c xg-rs-2.rs || Exit 1
+sed -e '/\"POT-Creation-Date:.*/d' < xg-rs-2.tmp | LC_ALL=C tr -d '\r' > xg-rs-2.po || Exit 1
+
+cat <<\EOF > xg-rs-2.ok
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: xg-rs-2.rs:3 xg-rs-2.rs:8
+msgid "Russian (Русский): Здравствуйте"
+msgstr ""
+
+#: xg-rs-2.rs:4 xg-rs-2.rs:9
+msgid "Vietnamese (Tiếng Việt): Chào bạn"
+msgstr ""
+
+#: xg-rs-2.rs:5 xg-rs-2.rs:10
+msgid "Japanese (日本語): こんにちは"
+msgstr ""
+
+#: xg-rs-2.rs:6 xg-rs-2.rs:11
+msgid "Thai (ภาษาไทย): สวัสดีครับ"
+msgstr ""
+
+#: xg-rs-2.rs:7 xg-rs-2.rs:12
+msgid "Script: 𝒞"
+msgstr ""
+
+#. And now a comment with Русский and 日本語 and Unicode escapes: B\u{00f6}se B\u{00fc}bchen
+#: xg-rs-2.rs:14
+msgid "This string has a multilingual comment"
+msgstr ""
+EOF
+
+: ${DIFF=diff}
+${DIFF} xg-rs-2.ok xg-rs-2.po
+result=$?
+
+exit $result
diff --git a/gettext-tools/tests/xgettext-rust-3 b/gettext-tools/tests/xgettext-rust-3
new file mode 100755
index 000000000..85d453e65
--- /dev/null
+++ b/gettext-tools/tests/xgettext-rust-3
@@ -0,0 +1,165 @@
+#!/bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# More tests for Rust support: string syntax
+
+LC_ALL=C tr '%' '\015' <<\EOF > xg-rs-3.rs
+pub trait TestCase {
+  fn main () {
+    // Test recognition of \u escapes.
+    gettext("B\u{00f6}se B\u{00fc}bchen");
+    // \u escapes with more than one u are invalid.
+    gettext("Japanese: \uu{6585}\uuu{6723}\u{8A9e}");
+    gettext("embedded\nnewline");
+    // Two backslashes (unlike in Java, where this is just one backslash).
+    gettext("\u{005c}\u{005c}");
+    // A 6-character string (unlike in Java, where this is just one backslash).
+    gettext("\\u005c");
+    // A single backslash.
+    gettext("\\");
+    // Escape sequences in strings.
+    gettext("t -> \t, n -> \n, dquote -> \", squote -> \' ...");
+    // Hex escapes are recognized.
+    gettext("bel: \x07\n");
+    gettext // Recognized despite comments
+       ( /* Even across multiline
+comment! */ "this is a single " /* now comes the concatenation! */ + // after +
+         "long line");
+    // Byte string literals are extracted.
+    gettext(b"byte 1");
+    // In byte string literals, escape sequences are recognized.
+    gettext(b"byte 2 \u{005c} \\ \t \n");
+    // Raw string literals are extracted.
+    gettext(r"raw 1");
+    // In raw string literals, no escape sequences are recognized.
+    gettext(r"raw 2 \u{005c} \\ \t \n \'");
+    // In raw string literals, only delimiters without the proper number of
+    // hashes are recognized.
+    gettext (r##"raw 3 ""test"##);
+    // Normal and raw string literals can be concatenated.
+    gettext("left - \"quot" + r#"ation"" - right"#);
+    // Character literals are not extracted.
+    gettext('x');
+    // Invalid concatenations are not concatenated.
+    gettext("fooba"+'r');
+  }
+}
+EOF
+
+: ${XGETTEXT=xgettext}
+# delete POT-Creation-Date: line because the date depends on local time.
+${XGETTEXT} --output xg-rs-3.tmp --add-location -c -kmygettext:2 xg-rs-3.rs 2>/dev/null || Exit 1
+sed -e '/\"POT-Creation-Date:.*/d' < xg-rs-3.tmp | LC_ALL=C tr -d '\r' > xg-rs-3.po || Exit 1
+
+cat <<\EOF > xg-rs-3.ok
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Test recognition of \u escapes.
+#: xg-rs-3.rs:4
+msgid "Böse Bübchen"
+msgstr ""
+
+#. \u escapes with more than one u are invalid.
+#: xg-rs-3.rs:6
+#, rust-format
+msgid "Japanese: \\uu{6585}\\uuu{6723}語"
+msgstr ""
+
+#: xg-rs-3.rs:7
+msgid ""
+"embedded\n"
+"newline"
+msgstr ""
+
+#. Two backslashes (unlike in Java, where this is just one backslash).
+#: xg-rs-3.rs:9
+msgid "\\\\"
+msgstr ""
+
+#. A 6-character string (unlike in Java, where this is just one backslash).
+#: xg-rs-3.rs:11
+msgid "\\u005c"
+msgstr ""
+
+#. A single backslash.
+#: xg-rs-3.rs:13
+msgid "\\"
+msgstr ""
+
+#. Escape sequences in strings.
+#: xg-rs-3.rs:15
+msgid ""
+"t -> \t, n -> \n"
+", dquote -> \", squote -> ' ..."
+msgstr ""
+
+#. Hex escapes are recognized.
+#: xg-rs-3.rs:17
+msgid "bel: \a\n"
+msgstr ""
+
+#. Recognized despite comments
+#. Even across multiline
+#. comment!
+#: xg-rs-3.rs:20
+msgid "this is a single long line"
+msgstr ""
+
+#. Byte string literals are extracted.
+#: xg-rs-3.rs:23
+msgid "byte 1"
+msgstr ""
+
+#. In byte string literals, escape sequences are recognized.
+#: xg-rs-3.rs:25
+msgid "byte 2 \\ \\ \t \n"
+msgstr ""
+
+#. Raw string literals are extracted.
+#: xg-rs-3.rs:27
+msgid "raw 1"
+msgstr ""
+
+#. In raw string literals, no escape sequences are recognized.
+#: xg-rs-3.rs:29
+msgid "raw 2 \\u{005c} \\\\ \\t \\n \\'"
+msgstr ""
+
+#. In raw string literals, only delimiters without the proper number of
+#. hashes are recognized.
+#: xg-rs-3.rs:32
+msgid "raw 3 \"test"
+msgstr ""
+
+#. Normal and raw string literals can be concatenated.
+#: xg-rs-3.rs:34
+msgid "left - \"quotation\" - right"
+msgstr ""
+
+#. Invalid concatenations are not concatenated.
+#: xg-rs-3.rs:38
+msgid "fooba"
+msgstr ""
+EOF
+
+: ${DIFF=diff}
+${DIFF} xg-rs-3.ok xg-rs-3.po
+result=$?
+
+exit $result
diff --git a/gettext-tools/tests/xgettext-rust-4 b/gettext-tools/tests/xgettext-rust-4
new file mode 100755
index 000000000..3ed0ed1cb
--- /dev/null
+++ b/gettext-tools/tests/xgettext-rust-4
@@ -0,0 +1,36 @@
+#!/bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# Tests for Rust plural keyword support
+
+cat <<EOF > xg-rs-4.rs
+pub trait TestCase {
+  fn main () {
+    ngettext("test 1", "plural test");
+    myngettext(context, "I've 1", "you've lost");
+  }
+}
+EOF
+
+: ${XGETTEXT=xgettext}
+${XGETTEXT} --omit-header --no-location --keyword=myngettext:2,3 \
+  -d xg-rs-4.tmp xg-rs-4.rs || Exit 1
+LC_ALL=C tr -d '\r' < xg-rs-4.tmp.po > xg-rs-4.po || Exit 1
+
+cat <<EOF > xg-rs-4.ok
+msgid "test 1"
+msgid_plural "plural test"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "I've 1"
+msgid_plural "you've lost"
+msgstr[0] ""
+msgstr[1] ""
+EOF
+
+: ${DIFF=diff}
+${DIFF} xg-rs-4.ok xg-rs-4.po
+result=$?
+
+exit $result
diff --git a/gettext-tools/tests/xgettext-rust-5 b/gettext-tools/tests/xgettext-rust-5
new file mode 100755
index 000000000..edd98a29e
--- /dev/null
+++ b/gettext-tools/tests/xgettext-rust-5
@@ -0,0 +1,28 @@
+#! /bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# Test Rust support: extraction of contexts.
+
+cat <<\EOF > xg-rs-7.rs
+println!(gettext("help"));
+println!(pgettext("Help", "about"));
+EOF
+
+: ${XGETTEXT=xgettext}
+${XGETTEXT} --omit-header --no-location -d xg-rs-7.tmp xg-rs-7.rs || Exit 1
+LC_ALL=C tr -d '\r' < xg-rs-7.tmp.po > xg-rs-7.po || Exit 1
+
+cat <<EOF > xg-rs-7.ok
+msgid "help"
+msgstr ""
+
+msgctxt "Help"
+msgid "about"
+msgstr ""
+EOF
+
+: ${DIFF=diff}
+${DIFF} xg-rs-7.ok xg-rs-7.po
+result=$?
+
+exit $result
-- 
2.34.1

