]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
add pgettext and npgettext 1126/head
authorSardorbek Imomaliev <sardorbek.imomaliev@gmail.com>
Mon, 13 Jan 2020 09:18:10 +0000 (16:18 +0700)
committerDavid Lord <davidism@gmail.com>
Mon, 5 Apr 2021 11:45:05 +0000 (04:45 -0700)
CHANGES.rst
docs/extensions.rst
src/jinja2/ext.py
tests/test_ext.py

index aa8f4a8bb4bc2c205e9167e6a9e4382002a2a34d..73cbd2df49bfdd88f665b35f561d093bbcab1270 100644 (file)
@@ -46,6 +46,8 @@ Unreleased
     available in a template before using it. Test functions can be
     decorated with ``@environmentfunction``, ``@evalcontextfunction``,
     or ``@contextfunction``. :issue:`842`, :pr:`1248`
+-   Support ``pgettext`` and ``npgettext`` (message contexts) in i18n
+    extension. :issue:`441`
 
 
 Version 2.11.3
index 3fdc5566950512707e61e705b13a22236893a7cc..f1033656b97dd6373c4dd757964bb783fb7ce9db 100644 (file)
@@ -34,9 +34,11 @@ The i18n extension can be used in combination with `gettext`_ or
 `Babel`_.  When it's enabled, Jinja provides a ``trans`` statement that
 marks a block as translatable and calls ``gettext``.
 
-After enabling, an application has to provide ``gettext`` and
-``ngettext`` functions, either globally or when rendering. A ``_()``
-function is added as an alias to the ``gettext`` function.
+After enabling, an application has to provide functions for ``gettext``,
+``ngettext``, and optionally ``pgettext`` and ``npgettext``, either
+globally or when rendering. A ``_()`` function is added as an alias to
+the ``gettext`` function.
+
 
 Environment Methods
 ~~~~~~~~~~~~~~~~~~~
@@ -47,11 +49,16 @@ additional methods:
 .. method:: jinja2.Environment.install_gettext_translations(translations, newstyle=False)
 
     Installs a translation globally for the environment. The
-    ``translations`` object must implement ``gettext`` and ``ngettext``.
+    ``translations`` object must implement ``gettext``, ``ngettext``,
+    and optionally ``pgettext`` and ``npgettext``.
     :class:`gettext.NullTranslations`, :class:`gettext.GNUTranslations`,
     and `Babel`_\s ``Translations`` are supported.
 
-    .. versionchanged:: 2.5 Added new-style gettext support.
+    .. versionchanged:: 3.0
+        Added ``pgettext`` and ``npgettext``.
+
+    .. versionchanged:: 2.5
+        Added new-style gettext support.
 
 .. method:: jinja2.Environment.install_null_translations(newstyle=False)
 
@@ -61,16 +68,21 @@ additional methods:
 
     .. versionchanged:: 2.5 Added new-style gettext support.
 
-.. method:: jinja2.Environment.install_gettext_callables(gettext, ngettext, newstyle=False)
+.. method:: jinja2.Environment.install_gettext_callables(gettext, ngettext, newstyle=False, pgettext=None, npgettext=None)
 
-    Install the given ``gettext`` and ``ngettext`` callables into the
-    environment. They should behave exactly like
-    :func:`gettext.gettext` and :func:`gettext.ngettext`.
+    Install the given ``gettext``, ``ngettext``, ``pgettext``, and
+    ``npgettext`` callables into the environment. They should behave
+    exactly like :func:`gettext.gettext`, :func:`gettext.ngettext`,
+    :func:`gettext.pgettext` and :func:`gettext.npgettext`.
 
     If ``newstyle`` is activated, the callables are wrapped to work like
     newstyle callables.  See :ref:`newstyle-gettext` for more information.
 
-    .. versionadded:: 2.5 Added new-style gettext support.
+    .. versionchanged:: 3.0
+        Added ``pgettext`` and ``npgettext``.
+
+    .. versionadded:: 2.5
+        Added new-style gettext support.
 
 .. method:: jinja2.Environment.uninstall_gettext_translations()
 
@@ -154,6 +166,10 @@ done with the ``|format`` filter. This requires duplicating work for
     {{ ngettext(
            "%(num)d apple", "%(num)d apples", apples|count
        )|format(num=apples|count) }}
+    {{ pgettext("greeting", "Hello, World!") }}
+    {{ npgettext(
+           "fruit", "%(num)d apple", "%(num)d apples", apples|count
+       )|format(num=apples|count) }}
 
 New style ``gettext`` make formatting part of the call, and behind the
 scenes enforce more consistency.
@@ -163,6 +179,8 @@ scenes enforce more consistency.
     {{ gettext("Hello, World!") }}
     {{ gettext("Hello, %(name)s!", name=name) }}
     {{ ngettext("%(num)d apple", "%(num)d apples", apples|count) }}
+    {{ pgettext("greeting", "Hello, World!") }}
+    {{ npgettext("fruit", "%(num)d apple", "%(num)d apples", apples|count) }}
 
 The advantages of newstyle gettext are:
 
index 73a2e7794771e102172dffe5e361555a996bc39b..0b2b441d04333dfe878a8390aedf7dc95b79bd1e 100644 (file)
@@ -30,7 +30,7 @@ from .utils import import_string
 
 # I18N functions available in Jinja templates. If the I18N library
 # provides ugettext, it will be assigned to gettext.
-GETTEXT_FUNCTIONS = ("_", "gettext", "ngettext")
+GETTEXT_FUNCTIONS = ("_", "gettext", "ngettext", "pgettext", "npgettext")
 _ws_re = re.compile(r"\s*\n\s*")
 
 
@@ -167,6 +167,37 @@ def _make_new_ngettext(func):
     return ngettext
 
 
+def _make_new_pgettext(func):
+    @contextfunction
+    def pgettext(__context, __string_ctx, __string, **variables):
+        variables.setdefault("context", __string_ctx)
+        rv = __context.call(func, __string_ctx, __string)
+
+        if __context.eval_ctx.autoescape:
+            rv = Markup(rv)
+
+        # Always treat as a format string, see gettext comment above.
+        return rv % variables
+
+    return pgettext
+
+
+def _make_new_npgettext(func):
+    @contextfunction
+    def npgettext(__context, __string_ctx, __singular, __plural, __num, **variables):
+        variables.setdefault("context", __string_ctx)
+        variables.setdefault("num", __num)
+        rv = __context.call(func, __string_ctx, __singular, __plural, __num)
+
+        if __context.eval_ctx.autoescape:
+            rv = Markup(rv)
+
+        # Always treat as a format string, see gettext comment above.
+        return rv % variables
+
+    return npgettext
+
+
 class InternationalizationExtension(Extension):
     """This extension adds gettext support to Jinja."""
 
@@ -200,23 +231,43 @@ class InternationalizationExtension(Extension):
         ngettext = getattr(translations, "ungettext", None)
         if ngettext is None:
             ngettext = translations.ngettext
-        self._install_callables(gettext, ngettext, newstyle)
+
+        pgettext = getattr(translations, "pgettext", None)
+        npgettext = getattr(translations, "npgettext", None)
+        self._install_callables(
+            gettext, ngettext, newstyle=newstyle, pgettext=pgettext, npgettext=npgettext
+        )
 
     def _install_null(self, newstyle=None):
         self._install_callables(
-            lambda x: x, lambda s, p, n: s if n == 1 else p, newstyle
+            lambda s: s,
+            lambda s, p, n: s if n == 1 else p,
+            newstyle=newstyle,
+            pgettext=lambda c, s: s,
+            npgettext=lambda c, s, p, n: s if n == 1 else p,
         )
 
-    def _install_callables(self, gettext, ngettext, newstyle=None):
+    def _install_callables(
+        self, gettext, ngettext, newstyle=None, pgettext=None, npgettext=None
+    ):
         if newstyle is not None:
             self.environment.newstyle_gettext = newstyle
         if self.environment.newstyle_gettext:
             gettext = _make_new_gettext(gettext)
             ngettext = _make_new_ngettext(ngettext)
-        self.environment.globals.update(gettext=gettext, ngettext=ngettext)
+
+            if pgettext is not None:
+                pgettext = _make_new_pgettext(pgettext)
+
+            if npgettext is not None:
+                npgettext = _make_new_npgettext(npgettext)
+
+        self.environment.globals.update(
+            gettext=gettext, ngettext=ngettext, pgettext=pgettext, npgettext=npgettext
+        )
 
     def _uninstall(self, translations):
-        for key in "gettext", "ngettext":
+        for key in ("gettext", "ngettext", "pgettext", "npgettext"):
             self.environment.globals.pop(key, None)
 
     def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS):
index 261abd21a15299ca9da09ce9906057d92924d7b4..9790f9517e60f7bab06e333a02cfcae5fe852b49 100644 (file)
@@ -40,6 +40,9 @@ newstyle_i18n_templates = {
     "ngettext.html": '{{ ngettext("%(num)s apple", "%(num)s apples", apples) }}',
     "ngettext_long.html": "{% trans num=apples %}{{ num }} apple{% pluralize %}"
     "{{ num }} apples{% endtrans %}",
+    "pgettext.html": '{{ pgettext("fruit", "Apple") }}',
+    "npgettext.html": '{{ npgettext("fruit", "%(num)s apple", "%(num)s apples",'
+    " apples) }}",
     "transvars1.html": "{% trans %}User: {{ num }}{% endtrans %}",
     "transvars2.html": "{% trans num=count %}User: {{ num }}{% endtrans %}",
     "transvars3.html": "{% trans count=num %}User: {{ count }}{% endtrans %}",
@@ -57,41 +60,88 @@ languages = {
         "%(user_count)s users online": "%(user_count)s Benutzer online",
         "User: %(num)s": "Benutzer: %(num)s",
         "User: %(count)s": "Benutzer: %(count)s",
-        "%(num)s apple": "%(num)s Apfel",
-        "%(num)s apples": "%(num)s Äpfel",
+        "Apple": {None: "Apfel", "fruit": "Apple"},
+        "%(num)s apple": {None: "%(num)s Apfel", "fruit": "%(num)s Apple"},
+        "%(num)s apples": {None: "%(num)s Äpfel", "fruit": "%(num)s Apples"},
     }
 }
 
 
+def _get_with_context(value, ctx=None):
+    if isinstance(value, dict):
+        return value.get(ctx, value)
+
+    return value
+
+
 @contextfunction
 def gettext(context, string):
     language = context.get("LANGUAGE", "en")
-    return languages.get(language, {}).get(string, string)
+    value = languages.get(language, {}).get(string, string)
+    return _get_with_context(value)
 
 
 @contextfunction
 def ngettext(context, s, p, n):
     language = context.get("LANGUAGE", "en")
+
+    if n != 1:
+        value = languages.get(language, {}).get(p, p)
+        return _get_with_context(value)
+
+    value = languages.get(language, {}).get(s, s)
+    return _get_with_context(value)
+
+
+@contextfunction
+def pgettext(context, c, s):
+    language = context.get("LANGUAGE", "en")
+    value = languages.get(language, {}).get(s, s)
+    return _get_with_context(value, c)
+
+
+@contextfunction
+def npgettext(context, c, s, p, n):
+    language = context.get("LANGUAGE", "en")
+
     if n != 1:
-        return languages.get(language, {}).get(p, p)
-    return languages.get(language, {}).get(s, s)
+        value = languages.get(language, {}).get(p, p)
+        return _get_with_context(value, c)
+
+    value = languages.get(language, {}).get(s, s)
+    return _get_with_context(value, c)
 
 
 i18n_env = Environment(
     loader=DictLoader(i18n_templates), extensions=["jinja2.ext.i18n"]
 )
-i18n_env.globals.update({"_": gettext, "gettext": gettext, "ngettext": ngettext})
+i18n_env.globals.update(
+    {
+        "_": gettext,
+        "gettext": gettext,
+        "ngettext": ngettext,
+        "pgettext": pgettext,
+        "npgettext": npgettext,
+    }
+)
 i18n_env_trimmed = Environment(extensions=["jinja2.ext.i18n"])
+
 i18n_env_trimmed.policies["ext.i18n.trimmed"] = True
 i18n_env_trimmed.globals.update(
-    {"_": gettext, "gettext": gettext, "ngettext": ngettext}
+    {
+        "_": gettext,
+        "gettext": gettext,
+        "ngettext": ngettext,
+        "pgettext": pgettext,
+        "npgettext": npgettext,
+    }
 )
 
 newstyle_i18n_env = Environment(
     loader=DictLoader(newstyle_i18n_templates), extensions=["jinja2.ext.i18n"]
 )
 newstyle_i18n_env.install_gettext_callables(  # type: ignore
-    gettext, ngettext, newstyle=True
+    gettext, ngettext, newstyle=True, pgettext=pgettext, npgettext=npgettext
 )
 
 
@@ -401,6 +451,20 @@ class TestInternationalization:
             (6, "ngettext", ("%(users)s user", "%(users)s users", None), ["third"]),
         ]
 
+    def test_extract_context(self):
+        from jinja2.ext import babel_extract
+
+        source = BytesIO(
+            b"""
+             {{ pgettext("babel", "Hello World") }}
+             {{ npgettext("babel", "%(users)s user", "%(users)s users", users) }}
+             """
+        )
+        assert list(babel_extract(source, ("pgettext", "npgettext", "_"), [], {})) == [
+            (2, "pgettext", ("babel", "Hello World"), []),
+            (3, "npgettext", ("babel", "%(users)s user", "%(users)s users", None), []),
+        ]
+
 
 class TestScope:
     def test_basic_scope_behavior(self):
@@ -525,6 +589,15 @@ class TestNewstyleInternationalization:
         t = newstyle_i18n_env.get_template("explicitvars.html")
         assert t.render() == "%(foo)s"
 
+    def test_context(self):
+        tmpl = newstyle_i18n_env.get_template("pgettext.html")
+        assert tmpl.render(LANGUAGE="de") == "Apple"
+
+    def test_context_newstyle_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"
+
 
 class TestAutoEscape:
     def test_scoped_setting(self):