Repository: pallets/jinja (python)

Issue:
Add block syntax for pgettext
Follow up on #441, #1126

It was difficult to come to an agreement on the syntax for block translation with context, so it was left out of #1126. However this should still be added.

The possibilities that were discussed are:

* `context` keywords: `{% trans context "mycontext" num=3 %}One Text{% pluralize %}{{num}} Texts{% endtrans%}`
  * [This resembles what Django uses](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#translate-template-tag)
* `{% context %}` block: `{% trans %}Click me{% context %}In annoying popup{% endtrans %}` (similar to `{% pluralize %}`
  * But context is usually very short, using a block might be wasteful
* Adding on `trans` directly: `{% trans "greeting" %}Hello{% endtrans %}` or `{% trans(greeting) %}Hello{% endtrans %}`

I personally don't feel strongly at all, but I really could use such a syntax.

Pull request description:
specify context for translation block
A context string can be passed as the first token to a `{% trans %}` block to use `pgettext` and `npgettext`.

```python
{% trans "title" trimmed count=count %}
```

fixes #1430

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index d641712d..323fc80d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -23,6 +23,9 @@ Unreleased
     -   ``unicode_urlencode`` is renamed to ``url_quote``.
 
 -   Add support for native types in macros. :issue:`1510`
+-   The ``{% trans %}`` tag can use ``pgettext`` and ``npgettext`` by
+    passing a context string as the first token in the tag, like
+    ``{% trans "title" %}``. :issue:`1430`
 
 
 Version 3.0.3
diff --git a/docs/templates.rst b/docs/templates.rst
index 89958b86..9071ad67 100644
--- a/docs/templates.rst
+++ b/docs/templates.rst
@@ -1732,11 +1732,35 @@ to disable it for a block.
 .. versionadded:: 2.10
    The ``trimmed`` and ``notrimmed`` modifiers have been added.
 
+If the translation depends on the context that the message appears in,
+the ``pgettext`` and ``npgettext`` functions take a ``context`` string
+as the first argument, which is used to select the appropriate
+translation. To specify a context with the ``{% trans %}`` tag, provide
+a string as the first token after ``trans``.
+
+.. code-block:: jinja
+
+    {% trans "fruit" %}apple{% endtrans %}
+    {% trans "fruit" trimmed count -%}
+        1 apple
+    {%- pluralize -%}
+        {{ count }} apples
+    {%- endtrans %}
+
+.. versionadded:: 3.1
+    A context can be passed to the ``trans`` tag to use ``pgettext`` and
+    ``npgettext``.
+
 It's possible to translate strings in expressions with these functions:
 
--   ``gettext``: translate a single string
--   ``ngettext``: translate a pluralizable string
--   ``_``: alias for ``gettext``
+-   ``_(message)``: Alias for ``gettext``.
+-   ``gettext(message)``: Translate a message.
+-   ``ngettext(singluar, plural, n)``: Translate a singular or plural
+    message based on a count variable.
+-   ``pgettext(context, message)``: Like ``gettext()``, but picks the
+    translation based on the context string.
+-   ``npgettext(context, singular, plural, n)``: Like ``npgettext()``,
+    but picks the translation based on the context string.
 
 You can print a translated string like this:
 
diff --git a/src/jinja2/ext.py b/src/jinja2/ext.py
index d21b83aa..d5550540 100644
--- a/src/jinja2/ext.py
+++ b/src/jinja2/ext.py
@@ -354,13 +354,19 @@ class InternationalizationExtension(Extension):
     def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
         """Parse a translatable tag."""
         lineno = next(parser.stream).lineno
-        num_called_num = False
+
+        context = None
+        context_token = parser.stream.next_if("string")
+
+        if context_token is not None:
+            context = context_token.value
 
         # find all the variables referenced.  Additionally a variable can be
         # defined in the body of the trans block too, but this is checked at
         # a later state.
         plural_expr: t.Optional[nodes.Expr] = None
         plural_expr_assignment: t.Optional[nodes.Assign] = None
+        num_called_num = False
         variables: t.Dict[str, nodes.Expr] = {}
         trimmed = None
         while parser.stream.current.type != "block_end":
@@ -455,6 +461,7 @@ class InternationalizationExtension(Extension):
         node = self._make_node(
             singular,
             plural,
+            context,
             variables,
             plural_expr,
             bool(referenced),
@@ -510,6 +517,7 @@ class InternationalizationExtension(Extension):
         self,
         singular: str,
         plural: t.Optional[str],
+        context: t.Optional[str],
         variables: t.Dict[str, nodes.Expr],
         plural_expr: t.Optional[nodes.Expr],
         vars_referenced: bool,
@@ -526,21 +534,18 @@ class InternationalizationExtension(Extension):
             if plural:
                 plural = plural.replace("%%", "%")
 
-        # singular only:
-        if plural_expr is None:
-            gettext = nodes.Name("gettext", "load")
-            node = nodes.Call(gettext, [nodes.Const(singular)], [], None, None)
+        func_name = "gettext"
+        func_args: t.List[nodes.Expr] = [nodes.Const(singular)]
 
-        # singular and plural
-        else:
-            ngettext = nodes.Name("ngettext", "load")
-            node = nodes.Call(
-                ngettext,
-                [nodes.Const(singular), nodes.Const(plural), plural_expr],
-                [],
-                None,
-                None,
-            )
+        if context is not None:
+            func_args.insert(0, nodes.Const(context))
+            func_name = f"p{func_name}"
+
+        if plural_expr is not None:
+            func_name = f"n{func_name}"
+            func_args.extend((nodes.Const(plural), plural_expr))
+
+        node = nodes.Call(nodes.Name(func_name, "load"), func_args, [], None, None)
 
         # in case newstyle gettext is used, the method is powerful
         # enough to handle the variable expansion and autoescape


Test patch:
diff --git a/tests/test_ext.py b/tests/test_ext.py
index b54e905f..2e842e0a 100644
--- a/tests/test_ext.py
+++ b/tests/test_ext.py
@@ -43,6 +43,9 @@ newstyle_i18n_templates = {
     "pgettext.html": '{{ pgettext("fruit", "Apple") }}',
     "npgettext.html": '{{ npgettext("fruit", "%(num)s apple", "%(num)s apples",'
     " apples) }}",
+    "pgettext_block": "{% trans 'fruit' num=apples %}Apple{% endtrans %}",
+    "npgettext_block": "{% trans 'fruit' num=apples %}{{ num }} apple"
+    "{% pluralize %}{{ num }} apples{% endtrans %}",
     "transvars1.html": "{% trans %}User: {{ num }}{% endtrans %}",
     "transvars2.html": "{% trans num=count %}User: {{ num }}{% endtrans %}",
     "transvars3.html": "{% trans count=num %}User: {{ count }}{% endtrans %}",
@@ -593,11 +596,20 @@ class TestNewstyleInternationalization:
         tmpl = newstyle_i18n_env.get_template("pgettext.html")
         assert tmpl.render(LANGUAGE="de") == "Apple"
 
-    def test_context_newstyle_plural(self):
+    def test_context_plural(self):
         tmpl = newstyle_i18n_env.get_template("npgettext.html")
         assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apple"
         assert tmpl.render(LANGUAGE="de", apples=5) == "5 Apples"
 
+    def test_context_block(self):
+        tmpl = newstyle_i18n_env.get_template("pgettext_block")
+        assert tmpl.render(LANGUAGE="de") == "Apple"
+
+    def test_context_plural_block(self):
+        tmpl = newstyle_i18n_env.get_template("npgettext_block")
+        assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apple"
+        assert tmpl.render(LANGUAGE="de", apples=5) == "5 Apples"
+
 
 class TestAutoEscape:
     def test_scoped_setting(self):