]> git.ipfire.org Git - thirdparty/gettext.git/commitdiff
xgettext: JavaScript: Support customized handling of tagged template literals.
authorBruno Haible <bruno@clisp.org>
Sat, 28 Sep 2024 00:09:36 +0000 (02:09 +0200)
committerBruno Haible <bruno@clisp.org>
Sat, 28 Sep 2024 00:09:36 +0000 (02:09 +0200)
Reported by Evan Welsh <contact@evanwelsh.com>
and Philip Chimento <philip.chimento@gmail.com>
at <https://savannah.gnu.org/bugs/index.php?60027>.

* gettext-tools/src/str-list.h (string_list_append_move): New declaration.
* gettext-tools/src/str-list.c (string_list_append_move): New function.
* gettext-tools/src/x-javascript.h (x_javascript_tag): New declaration.
* gettext-tools/src/x-javascript.c: Include str-list.h.
(tag_step1_fn): New type.
(gnome_step1, get_tag_step1_fn): New functions.
(struct tag_definition): New type.
(tags): New variable.
(x_javascript_tag): New function.
(struct token_ty): Add fields template_tag, template_parts.
(free_token): Update.
(struct level_info): New type.
(levels): Change element type to 'struct level_info'.
(new_level, level_type): Update.
(phase5_get, x_javascript_lex): Arrange to collect the parts of a template
literal in the last token's template_parts field.
(extract_balanced): Handle tagged template literals.
* gettext-tools/src/xgettext.c (long_options): Add option --tag.
(main): Handle option --tag.
(usage): Document ption --tag.
* gettext-tools/tests/xgettext-javascript-13: New file.
* gettext-tools/tests/Makefile.am (TESTS): Add it.
* gettext-tools/doc/gettext.texi (No string concatenation): Mention the tagged
template literals as alternative.
* gettext-tools/doc/xgettext.texi: Document the --tag option.
* gettext-tools/doc/lang-javascript.texi (JavaScript): Mention the tagged
template literals syntax.
* NEWS: Mention the change.

NEWS
gettext-tools/doc/gettext.texi
gettext-tools/doc/lang-javascript.texi
gettext-tools/doc/xgettext.texi
gettext-tools/src/str-list.c
gettext-tools/src/str-list.h
gettext-tools/src/x-javascript.c
gettext-tools/src/x-javascript.h
gettext-tools/src/xgettext.c
gettext-tools/tests/Makefile.am
gettext-tools/tests/xgettext-javascript-13 [new file with mode: 0755]

diff --git a/NEWS b/NEWS
index 2c1410feba18f49ac81ea0de3c83f003f42b1a6d..3961ba2e18f7fd37a3d0108606ce42f045929eab 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -16,7 +16,10 @@ Version 0.23 - September 2024
     o xgettext now recognizes comments of the form '#; <expression>'.
   - Java: Improved recognition of format strings when the String.formatted
     method is used.
-  - JavaScript: xgettext now parses template literals inside JSX correctly.
+  - JavaScript:
+    o xgettext now parses template literals inside JSX correctly.
+    o xgettext has a new option --tag that customizes the behaviour of tagged
+      template literals.
   - C#: Strings with embedded expressions (a.k.a. interpolated strings) are now
     recognized.
   - awk: String concatenation by juxtaposition is now recognized.
index ebedb5675c40f9eca6631402387a837e5dd46ceb..88096771faf1cb8c049109585b0d17c4876a5518 100644 (file)
@@ -2424,6 +2424,17 @@ into a statement involving a format string:
 print ('Replace %s with %s?'.format(object1.name, object2.name))
 @end example
 
+@noindent
+Specifically in JavaScript,
+an alternative is to use a @emph{tagged} template literal:
+
+@example
+print (@var{tag}`Replace $@{object1.name@} with $@{object2.name@}?`)
+@end example
+
+@noindent
+and pass an option @samp{--tag=@var{tag}:@var{format}} to @code{xgettext}.
+
 @subheading Format strings with embedded named references
 
 Format strings with embedded named references are different:
index c678725ec8d0ee719aa85362f884f61a60cd2655..3759dca945ac84be33ee9f12c1e8da087e34ec07 100644 (file)
@@ -24,6 +24,10 @@ gjs
 
 @item @code{`abc`}
 
+@c https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
+@item @code{@var{tag}`abc$@{@var{expression}@}def@{@var{expression}@}...`},
+see the description of @samp{--tag} in @ref{xgettext Invocation}.
+
 @end itemize
 
 @item gettext shorthand
index cb3d3dff945ac18b73be12c167d6254852ec8fc5..a6037c11d16ac5666b875d51c765dc035e1fe6d4 100644 (file)
@@ -409,6 +409,22 @@ This option has an effect with most languages, namely C, C++, ObjectiveC,
 Shell, Python, Lisp, EmacsLisp, librep, Scheme, Guile, Java, C#, awk,
 YCP, Tcl, Perl, PHP, GCC-source, Lua, JavaScript, Vala.
 
+@item --tag=@var{word}:@var{format}
+@opindex --tag@r{, @code{xgettext} option}
+Defines the behaviour of tagged template literals with tag @var{word}.
+This option has an effect only with language JavaScript.
+@*
+@var{format} is a symbolic description
+of the first step of the JavaScript function named @var{word},
+namely how this function constructs a format string
+based on the parts of the template literal.
+Currently only one value is supported: @code{javascript-gnome-format},
+which describes the construction of a format string with numbered placeholders
+@code{@{0@}}, @code{@{1@}}, @code{@{2@}}, etc.
+For example, @code{javascript-gnome-format} transforms the template literal
+@code{@var{word}`My name is $@{id.name@} and I am $@{id.age@} years old.`}
+into the format string @code{"My name is @{0@} and I am @{1@} years old."}.
+
 @item -T
 @itemx --trigraphs
 @opindex -T@r{, @code{xgettext} option}
index 4909edc23751e2b44e4f6b8ec3c5fa24ccb6f15a..afe36d596c3c261f0e18b20e0a4ac69101234fa8 100644 (file)
@@ -1,6 +1,5 @@
 /* GNU gettext - internationalization aids
-   Copyright (C) 1995, 1998, 2000-2004, 2006, 2009, 2020 Free Software
-   Foundation, Inc.
+   Copyright (C) 1995-2024 Free Software Foundation, Inc.
 
    This file was written by Peter Miller <millerp@canb.auug.org.au>
 
@@ -75,6 +74,26 @@ string_list_append (string_list_ty *slp, const char *s)
 }
 
 
+/* Append a freshly allocated single string to the end of a list of strings,
+   transferring its ownership to SLP.  */
+void
+string_list_append_move (string_list_ty *slp, char *s)
+{
+  /* Grow the list.  */
+  if (slp->nitems >= slp->nitems_max)
+    {
+      size_t nbytes;
+
+      slp->nitems_max = slp->nitems_max * 2 + 4;
+      nbytes = slp->nitems_max * sizeof (slp->item[0]);
+      slp->item = (const char **) xrealloc (slp->item, nbytes);
+    }
+
+  /* Add the string itself to the end of the list.  */
+  slp->item[slp->nitems++] = s;
+}
+
+
 /* Append a single string to the end of a list of strings, unless it is
    already contained in the list.  */
 void
index a39f65823bcc5fe2528a0d231597db73778eb803..e7f8006fb7d5e8bf184339f13c5cbf3f50b473e2 100644 (file)
@@ -1,6 +1,5 @@
 /* GNU gettext - internationalization aids
-   Copyright (C) 1995-1996, 1998, 2000-2004, 2009, 2020 Free Software
-   Foundation, Inc.
+   Copyright (C) 1995-2024 Free Software Foundation, Inc.
 
    This file was written by Peter Miller <millerp@canb.auug.org.au>
 
@@ -51,6 +50,10 @@ extern string_list_ty *string_list_alloc (void);
 /* Append a single string to the end of a list of strings.  */
 extern void string_list_append (string_list_ty *slp, const char *s);
 
+/* Append a freshly allocated single string to the end of a list of strings,
+   transferring its ownership to SLP.  */
+extern void string_list_append_move (string_list_ty *slp, char *s);
+
 /* Append a single string to the end of a list of strings, unless it is
    already contained in the list.  */
 extern void string_list_append_unique (string_list_ty *slp, const char *s);
index 024b7b36460ad3932f3c3692b287fb0ed0508e82..07b6034b354ffcc4c9c5ba4cad8760f392c83448 100644 (file)
@@ -34,6 +34,7 @@
 #include <error.h>
 #include "attribute.h"
 #include "message.h"
+#include "str-list.h"
 #include "rc-str-list.h"
 #include "xgettext.h"
 #include "xg-pos.h"
@@ -150,6 +151,105 @@ init_flag_table_javascript ()
 }
 
 
+/* ======================== Tag set customization.  ======================== */
+
+/* Tagged template literals are described in
+   <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals>.
+
+   A tagged template literal looks like this in the source code:
+     TAG`part0 ${expression 1} part1 ${expression 2} ... ${expression N} partN`
+
+   A tag, immediately before a template literal, denotes a function that takes
+   as arguments:
+     - A list of the N+1 parts of the template literal,
+     - The N values of the N expressions between these parts.
+
+   In our use case, the tag function is supposed to
+     1. Convert the N+1 parts to a format string.
+     2. Look up the translation of this format string. (It is supposed to
+        accept the same number of arguments.)
+     3. Call a formatting facility that substitutes the N values into the
+        translated format string.
+   The type of format string is not fixed.  */
+
+/* Type of a C function that implements step 1 of what a tag function does.
+   It takes a non-empty string_list_ty as argument and returns a freshly
+   allocated string.  */
+typedef char * (*tag_step1_fn) (string_list_ty *parts);
+
+/* Tag step 1 function that produces a format string with placeholders
+   {0}, {1}, {2}, etc.  */
+static char *
+gnome_step1 (string_list_ty *parts)
+{
+  size_t n = parts->nitems - 1;
+  string_list_ty pieces;
+  unsigned long i;
+
+  pieces.nitems = 2 * n + 1;
+  pieces.nitems_max = pieces.nitems;
+  pieces.item = XNMALLOC (pieces.nitems, const char *);
+  for (i = 0; i <= n; i++)
+    {
+      pieces.item[2 * i] = parts->item[i];
+      if (i < n)
+        pieces.item[2 * i + 1] = xasprintf ("{%lu}", i);
+    }
+
+  char *result = string_list_concat (&pieces);
+
+  for (i = 0; i < n; i++)
+    free ((char *) pieces.item[2 * i + 1]);
+
+  return result;
+}
+
+/* Returns the tag step 1 function for a given format, or NULL if that format
+   is unknown.  */
+static tag_step1_fn
+get_tag_step1_fn (const char *format)
+{
+  if (strcmp (format, "javascript-gnome-format") == 0)
+    return gnome_step1;
+  /* ... More formats can be added here ...  */
+  return NULL;
+}
+
+/* Information associated with a tag.  */
+struct tag_definition
+{
+  const char *format;
+  tag_step1_fn step1_fn;
+};
+
+/* Mapping tag -> format.  */
+static hash_table tags;
+
+void
+x_javascript_tag (const char *name)
+{
+  const char *colon = strchr (name, ':');
+  if (colon != NULL)
+    {
+      const char *format = colon + 1;
+      tag_step1_fn step1_fn = get_tag_step1_fn (format);
+      if (step1_fn != NULL)
+        {
+          /* Heap-allocate a 'struct tag_definition'  */
+          struct tag_definition *def = XMALLOC (struct tag_definition);
+          def->format = xstrdup (format);
+          def->step1_fn = step1_fn;
+
+          if (tags.table == NULL)
+            hash_init (&tags, 10);
+
+          /* Insert it in the TAGS table.  */
+          hash_set_value (&tags, name, colon - name, def);
+        }
+    }
+}
+
+
 /* ======================== Reading of characters.  ======================== */
 
 /* The input file stream.  */
@@ -699,9 +799,15 @@ typedef struct token_ty token_ty;
 struct token_ty
 {
   token_type_ty type;
-  char *string;                  /* for token_type_symbol, token_type_keyword */
-  mixed_string_ty *mixed_string;        /* for token_type_string, token_type_template */
-  refcounted_string_list_ty *comment;   /* for token_type_string, token_type_template */
+  char *template_tag;                 /* for token_type_template, token_type_ltemplate,
+                                         token_type_rtemplate */
+  char *string;                       /* for token_type_symbol, token_type_keyword */
+  mixed_string_ty *mixed_string;      /* for token_type_string, token_type_template,
+                                         token_type_ltemplate, token_type_mtemplate,
+                                         token_type_rtemplate */
+  string_list_ty *template_parts;     /* for token_type_rtemplate */
+  refcounted_string_list_ty *comment; /* for token_type_string, token_type_template,
+                                         token_type_ltemplate, token_type_rtemplate */
   int line_number;
 };
 
@@ -710,13 +816,23 @@ struct token_ty
 static inline void
 free_token (token_ty *tp)
 {
+  if (tp->type == token_type_template || tp->type == token_type_ltemplate
+      || tp->type == token_type_rtemplate)
+    free (tp->template_tag);
   if (tp->type == token_type_symbol || tp->type == token_type_keyword)
     free (tp->string);
-  if (tp->type == token_type_string || tp->type == token_type_template)
-    {
-      mixed_string_free (tp->mixed_string);
-      drop_reference (tp->comment);
-    }
+  if (tp->type == token_type_string || tp->type == token_type_template
+      /* For these types, tp->mixed_string is already freed earlier, when we
+         build up the level's template_parts.  */
+      #if 0
+      || tp->type == token_type_ltemplate || tp->type == token_type_mtemplate
+      || tp->type == token_type_rtemplate
+      #endif
+     )
+    mixed_string_free (tp->mixed_string);
+  if (tp->type == token_type_string || tp->type == token_type_template
+      || tp->type == token_type_ltemplate || tp->type == token_type_rtemplate)
+    drop_reference (tp->comment);
 }
 
 
@@ -1010,8 +1126,15 @@ enum level_ty
   level_xml_element        = 3,
   level_embedded_js_in_xml = 4
 };
+struct level_info
+{
+  enum level_ty type;
+  char *template_tag;                          /* for level_template_literal */
+  string_list_ty *template_parts;              /* for level_template_literal */
+  refcounted_string_list_ty *template_comment; /* for level_template_literal */
+};
 /* The level stack.  */
-static unsigned char *levels /* = NULL */;
+static struct level_info *levels /* = NULL */;
 /* Number of allocated elements in levels.  */
 static size_t levels_alloc /* = 0 */;
 /* Number of currently used elements in levels.  */
@@ -1026,16 +1149,17 @@ new_level (enum level_ty l)
       levels_alloc = 2 * levels_alloc + 1;
       /* Now level < levels_alloc.  */
       levels =
-        (unsigned char *)
-        xrealloc (levels, levels_alloc * sizeof (unsigned char));
+        (struct level_info *)
+        xrealloc (levels, levels_alloc * sizeof (struct level_info));
     }
-  levels[level++] = l;
+  levels[level].type = l;
+  level++;
 }
 
 /* Returns the current level's type,
    as one of the level_* enum items, or 0 if the level stack is empty.  */
 #define level_type() \
-  (level > 0 ? levels[level - 1] : 0)
+  (level > 0 ? levels[level - 1].type : 0)
 
 /* Parses some XML markup.
    Returns 0 for an XML comment,
@@ -1300,6 +1424,7 @@ phase5_get (token_ty *tp)
 
                 if (uc == P7_EOF || uc == P7_STRING_END)
                   {
+                    tp->template_tag = NULL;
                     tp->mixed_string = mixed_string_buffer_result (&msb);
                     tp->comment = add_reference (savable_comment);
                     tp->type = last_token_type = token_type_template;
@@ -1308,9 +1433,14 @@ phase5_get (token_ty *tp)
 
                 if (uc == P7_TEMPLATE_START_OF_EXPRESSION)
                   {
-                    mixed_string_buffer_destroy (&msb);
+                    tp->template_tag = NULL;
+                    tp->mixed_string = mixed_string_buffer_result (&msb);
+                    tp->comment = add_reference (savable_comment);
                     tp->type = last_token_type = token_type_ltemplate;
                     new_level (level_template_literal);
+                    levels[level - 1].template_tag = NULL;
+                    levels[level - 1].template_parts = NULL;
+                    levels[level - 1].template_comment = NULL;
                     break;
                   }
 
@@ -1466,23 +1596,51 @@ phase5_get (token_ty *tp)
           else if (level_type () == level_template_literal)
             {
               /* Middle or right part of template literal.  */
+              struct mixed_string_buffer msb;
+
+              lexical_context = lc_string;
+              /* Start accumulating the string.  */
+              mixed_string_buffer_init (&msb, lexical_context,
+                                        logical_file_name, line_number);
               for (;;)
                 {
                   int uc = phase7_getuc ('`');
 
+                  /* Keep line_number in sync.  */
+                  msb.line_number = line_number;
+
                   if (uc == P7_EOF || uc == P7_STRING_END)
                     {
+                      tp->mixed_string = mixed_string_buffer_result (&msb);
                       tp->type = last_token_type = token_type_rtemplate;
+                      string_list_append_move (levels[level - 1].template_parts,
+                                               mixed_string_contents_free1 (tp->mixed_string));
+                      /* Move info from the current level to the token.  */
+                      tp->template_tag = levels[level - 1].template_tag;
+                      tp->template_parts = levels[level - 1].template_parts;
+                      tp->comment = levels[level - 1].template_comment;
                       level--;
                       break;
                     }
 
                   if (uc == P7_TEMPLATE_START_OF_EXPRESSION)
                     {
+                      tp->mixed_string = mixed_string_buffer_result (&msb);
                       tp->type = last_token_type = token_type_mtemplate;
                       break;
                     }
+
+                  if (IS_UNICODE (uc))
+                    {
+                      assert (UNICODE_VALUE (uc) >= 0
+                              && UNICODE_VALUE (uc) < 0x110000);
+                      mixed_string_buffer_append_unicode (&msb,
+                                                          UNICODE_VALUE (uc));
+                    }
+                  else
+                    mixed_string_buffer_append_char (&msb, uc);
                 }
+              lexical_context = lc_outside;
               return;
             }
           tp->type = last_token_type = token_type_rbrace;
@@ -1538,6 +1696,7 @@ static void
 x_javascript_lex (token_ty *tp)
 {
   phase5_get (tp);
+
   if (tp->type == token_type_string || tp->type == token_type_template)
     {
       mixed_string_ty *sum = tp->mixed_string;
@@ -1573,19 +1732,40 @@ x_javascript_lex (token_ty *tp)
       token_ty token2;
 
       phase5_get (&token2);
-      if (token2.type == token_type_template)
+      if (token2.type == token_type_template
+          || token2.type == token_type_ltemplate)
         {
-          /* The value of
-               tag `abc`
-             is the value of the function call
-               tag (["abc"])
-             We don't know anything about this value.  Therefore, don't
-             let the extractor see this template literal.  */
-          free_token (&token2);
+          /* Merge *tp and token2:
+             tag `abc`    becomes    tag`abc`
+             tag `abc${   becomes    tag`abc${
+           */
+          tp->type = token2.type;
+          tp->template_tag = tp->string;
+          tp->mixed_string = token2.mixed_string;
+          tp->comment = token2.comment;
+          tp->line_number = token2.line_number;
         }
       else
         phase5_unget (&token2);
     }
+
+  /* Move info from the token into the current level.  */
+  if (tp->type == token_type_ltemplate
+      || tp->type == token_type_mtemplate)
+    {
+      if (!(level_type () == level_template_literal))
+        abort ();
+      if (tp->type == token_type_ltemplate)
+        {
+          levels[level - 1].template_tag = tp->template_tag;
+          tp->template_tag = NULL;
+          levels[level - 1].template_parts = string_list_alloc ();
+          levels[level - 1].template_comment = tp->comment;
+          tp->comment = NULL;
+        }
+      string_list_append_move (levels[level - 1].template_parts,
+                               mixed_string_contents_free1 (tp->mixed_string));
+    }
 }
 
 
@@ -1782,25 +1962,68 @@ extract_balanced (message_list_ty *mlp,
 
         case token_type_string:
         case token_type_template:
+        case token_type_rtemplate:
           {
             lex_pos_ty pos;
 
             pos.file_name = logical_file_name;
             pos.line_number = token.line_number;
 
-            if (extract_all)
+            mixed_string_ty *mixed_string =
+              (token.type != token_type_rtemplate ? token.mixed_string : NULL);
+            /* For a tagged template literal, perform the tag step 1.  */
+            if ((token.type == token_type_template
+                 || token.type == token_type_rtemplate)
+                && token.template_tag != NULL)
               {
-                char *string = mixed_string_contents (token.mixed_string);
-                mixed_string_free (token.mixed_string);
-                remember_a_message (mlp, NULL, string, true, false,
-                                    inner_region, &pos,
-                                    NULL, token.comment, true);
+                const char *tag = token.template_tag;
+                string_list_ty *parts;
+
+                if (token.type == token_type_template)
+                  {
+                    parts = string_list_alloc ();
+                    string_list_append_move (parts,
+                                             mixed_string_contents (mixed_string));
+                  }
+                else /* (token.type == token_type_rtemplate) */
+                  parts = token.template_parts;
+
+                void *tag_value;
+                if (tags.table != NULL
+                    && hash_find_entry (&tags, tag, strlen (tag), &tag_value) == 0)
+                  {
+                    struct tag_definition *def = tag_value;
+
+                    /* Invoke the tag step 1 function.  */
+                    char *string = def->step1_fn (parts);
+
+                    /* Extract the string.  */
+                    remember_a_message (mlp, NULL, string, true, false,
+                                        inner_region, &pos,
+                                        NULL, token.comment, true);
+                  }
+
+                string_list_free (parts);
+
+                /* Due to the tag, the value is not a constant.  */
+                mixed_string = NULL;
+              }
+
+            if (mixed_string != NULL)
+              {
+                if (extract_all)
+                  {
+                    char *string = mixed_string_contents_free1 (mixed_string);
+                    remember_a_message (mlp, NULL, string, true, false,
+                                        inner_region, &pos,
+                                        NULL, token.comment, true);
+                  }
+                else
+                  arglist_parser_remember (argparser, arg, mixed_string,
+                                           inner_region,
+                                           pos.file_name, pos.line_number,
+                                           token.comment, true);
               }
-            else
-              arglist_parser_remember (argparser, arg, token.mixed_string,
-                                       inner_region,
-                                       pos.file_name, pos.line_number,
-                                       token.comment, true);
           }
           drop_reference (token.comment);
           next_context_iter = null_context_list_iterator;
@@ -1844,7 +2067,6 @@ extract_balanced (message_list_ty *mlp,
 
         case token_type_ltemplate:
         case token_type_mtemplate:
-        case token_type_rtemplate:
         case token_type_keyword:
         case token_type_start:
         case token_type_dot:
index 09b530704bc2848909acd81915e1be6b62f4146d..545b68711e88ce8e066b5c9324e162c4ed692fbe 100644 (file)
@@ -1,5 +1,5 @@
 /* xgettext JavaScript backend.
-   Copyright (C) 2002-2003, 2006, 2010, 2013-2014, 2018, 2020 Free Software Foundation, Inc.
+   Copyright (C) 2002-2024 Free Software Foundation, Inc.
    This file was written by Andreas Stricker <andy@knitter.ch>, 2010.
    It's based on x-python from Bruno Haible.
 
@@ -46,6 +46,8 @@ extern void x_javascript_extract_all (void);
 
 extern void init_flag_table_javascript (void);
 
+extern void x_javascript_tag (const char *tag);
+
 
 #ifdef __cplusplus
 }
index fb185464fde900afb464d999595483012fe45347..989bbcade9d1e9a80ce43486274456577152c5ce 100644 (file)
@@ -276,6 +276,7 @@ static const struct option long_options[] =
   { "string-limit", required_argument, NULL, 'l' },
   { "stringtable-output", no_argument, NULL, CHAR_MAX + 7 },
   { "style", required_argument, NULL, CHAR_MAX + 15 },
+  { "tag", required_argument, NULL, CHAR_MAX + 21 },
   { "trigraphs", no_argument, NULL, 'T' },
   { "verbose", no_argument, NULL, 'v' },
   { "version", no_argument, NULL, 'V' },
@@ -672,12 +673,16 @@ main (int argc, char *argv[])
           error (EXIT_FAILURE, 0, _("sentence end type '%s' unknown"), optarg);
         break;
 
+      case CHAR_MAX + 19: /* --itstool */
+        add_itstool_comments = true;
+        break;
+
       case CHAR_MAX + 20: /* --its */
         explicit_its_filename = optarg;
         break;
 
-      case CHAR_MAX + 19: /* --itstool */
-        add_itstool_comments = true;
+      case CHAR_MAX + 21: /* --tag */
+        x_javascript_tag (optarg);
         break;
 
       default:
@@ -1164,6 +1169,11 @@ Language specific options:\n"));
                                 C#, awk, YCP, Tcl, Perl, PHP, GCC-source,\n\
                                 Lua, JavaScript, Vala)\n"));
       printf (_("\
+      --tag=WORD:FORMAT       defines the behaviour of tagged template literals\n\
+                              with tag WORD\n"));
+      printf (_("\
+                                (only language JavaScript)\n"));
+      printf (_("\
   -T, --trigraphs             understand ANSI C trigraphs for input\n"));
       printf (_("\
                                 (deprecated; only languages C, C++, ObjectiveC)\n"));
index d71b797ea1fea8b4497b38400fd3c9cbfb06b4bc..0bbd1c022a31e93c93b6d6b31000a5366b332036 100644 (file)
@@ -119,6 +119,7 @@ TESTS = gettext-1 gettext-2 \
        xgettext-javascript-4 xgettext-javascript-5 xgettext-javascript-6 \
        xgettext-javascript-7 xgettext-javascript-8 xgettext-javascript-9 \
        xgettext-javascript-10 xgettext-javascript-11 xgettext-javascript-12 \
+       xgettext-javascript-13 \
        xgettext-javascript-stackovfl-1 xgettext-javascript-stackovfl-2 \
        xgettext-javascript-stackovfl-3 xgettext-javascript-stackovfl-4 \
        xgettext-javascript-stackovfl-5 xgettext-javascript-stackovfl-6 \
diff --git a/gettext-tools/tests/xgettext-javascript-13 b/gettext-tools/tests/xgettext-javascript-13
new file mode 100755 (executable)
index 0000000..60b165d
--- /dev/null
@@ -0,0 +1,178 @@
+#!/bin/sh
+. "${srcdir=.}/init.sh"; path_prepend_ . ../src
+
+# Test of JavaScript support: tagged template literals.
+
+cat <<\EOF > xg-js-13.js
+var s0 = _`A template literal without substitutions`;
+var s1 = _ `A template literal with
+embedded
+newlines`);
+var s2 = _ /* XYZZY */ `A template literal with ${n} substitutions`;
+var s3 = _`A template literal with several substitutions: ${a} and ${b} and ${c} and so on`;
+var s4 = `/${looks_like_regex}`;
+var s5 = _('not part of a regex');
+var s6 = _`that's a valid string. ` + _('This too');
+var s7 = _ tag`A template literal with two tags`;
+var s8 = _`a${_`b${_`c`+d}`}e`;
+var s9 = _("a normal string");
+var s10 = _`abc${foo({},_('should be extracted'))}xyz`;
+var f1 = function () {
+  return _("first normal string") + _`${foo}` + _("second normal string");
+};
+var s11 = _("another normal string");
+var s12 = { property: _`A template literal with ${n} substitution` };
+var s13 = _("yet another normal string");
+// Mixing JSX with template literals.
+var s14 = 0;
+var s15 = (
+  <div>
+    _("Expected translation string #10");
+    {_`pre11${ _("Expected translation string #11") }post11`}
+    _("Expected translation string #12");
+  </div>
+);
+var s16 = _("Expected translation string #13");
+var s17 = <div className={_`pre14${ _("Expected translation string #14") }post14`} /> ;
+var s18 = _("Expected translation string #15");
+var s19 = { a: 1, b: <div className={_`pre16${ _("Expected translation string #16") }post16`} /> };
+var s20 = _("Expected translation string #17");
+var s21 = _`begin${ <div> _("Expected translation string #18") </div> }end`;
+var s22 = _("Expected translation string #19");
+EOF
+
+: ${XGETTEXT=xgettext}
+${XGETTEXT} --add-comments --no-location --tag=_:javascript-gnome-format \
+            -o xg-js-13.tmp xg-js-13.js 2>xg-js-13.err
+test $? = 0 || { cat xg-js-13.err; Exit 1; }
+func_filter_POT_Creation_Date xg-js-13.tmp xg-js-13.pot
+
+cat <<\EOF > xg-js-13.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=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "A template literal without substitutions"
+msgstr ""
+
+msgid ""
+"A template literal with\n"
+"embedded\n"
+"newlines"
+msgstr ""
+
+#. XYZZY
+msgid "A template literal with {0} substitutions"
+msgstr ""
+
+msgid ""
+"A template literal with several substitutions: {0} and {1} and {2} and so on"
+msgstr ""
+
+msgid "not part of a regex"
+msgstr ""
+
+msgid "that's a valid string. "
+msgstr ""
+
+msgid "This too"
+msgstr ""
+
+msgid "c"
+msgstr ""
+
+msgid "b{0}"
+msgstr ""
+
+msgid "a{0}e"
+msgstr ""
+
+msgid "a normal string"
+msgstr ""
+
+msgid "should be extracted"
+msgstr ""
+
+msgid "abc{0}xyz"
+msgstr ""
+
+msgid "first normal string"
+msgstr ""
+
+msgid "{0}"
+msgstr ""
+
+msgid "second normal string"
+msgstr ""
+
+msgid "another normal string"
+msgstr ""
+
+msgid "A template literal with {0} substitution"
+msgstr ""
+
+msgid "yet another normal string"
+msgstr ""
+
+msgid "Expected translation string #10"
+msgstr ""
+
+msgid "Expected translation string #11"
+msgstr ""
+
+msgid "pre11{0}post11"
+msgstr ""
+
+msgid "Expected translation string #12"
+msgstr ""
+
+msgid "Expected translation string #13"
+msgstr ""
+
+msgid "Expected translation string #14"
+msgstr ""
+
+msgid "pre14{0}post14"
+msgstr ""
+
+msgid "Expected translation string #15"
+msgstr ""
+
+msgid "Expected translation string #16"
+msgstr ""
+
+msgid "pre16{0}post16"
+msgstr ""
+
+msgid "Expected translation string #17"
+msgstr ""
+
+msgid "Expected translation string #18"
+msgstr ""
+
+msgid "begin{0}end"
+msgstr ""
+
+msgid "Expected translation string #19"
+msgstr ""
+EOF
+
+: ${DIFF=diff}
+${DIFF} xg-js-13.ok xg-js-13.pot
+result=$?
+
+exit $result