summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSardorbek Imomaliev <sardorbek.imomaliev@gmail.com>2020-01-13 16:18:10 +0700
committerDavid Lord <davidism@gmail.com>2021-04-05 04:45:05 -0700
commit3fba8980987c127ac998050728031b6af2854626 (patch)
tree45e4af2cbd10d8f9f569b8f19de0e05dec8e7ada
parentbeabf304b0f9baa51729d25a9792844ad16675b3 (diff)
downloadjinja2-3fba8980987c127ac998050728031b6af2854626.tar.gz
add pgettext and npgettext
-rw-r--r--CHANGES.rst2
-rw-r--r--docs/extensions.rst38
-rw-r--r--src/jinja2/ext.py63
-rw-r--r--tests/test_ext.py89
4 files changed, 168 insertions, 24 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index aa8f4a8..73cbd2d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -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
diff --git a/docs/extensions.rst b/docs/extensions.rst
index 3fdc556..f103365 100644
--- a/docs/extensions.rst
+++ b/docs/extensions.rst
@@ -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:
diff --git a/src/jinja2/ext.py b/src/jinja2/ext.py
index 73a2e77..0b2b441 100644
--- a/src/jinja2/ext.py
+++ b/src/jinja2/ext.py
@@ -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):
diff --git a/tests/test_ext.py b/tests/test_ext.py
index 261abd2..9790f95 100644
--- a/tests/test_ext.py
+++ b/tests/test_ext.py
@@ -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):