From 3fba8980987c127ac998050728031b6af2854626 Mon Sep 17 00:00:00 2001 From: Sardorbek Imomaliev Date: Mon, 13 Jan 2020 16:18:10 +0700 Subject: [PATCH] add pgettext and npgettext --- CHANGES.rst | 2 + docs/extensions.rst | 38 ++++++++++++++----- src/jinja2/ext.py | 63 +++++++++++++++++++++++++++++--- tests/test_ext.py | 89 +++++++++++++++++++++++++++++++++++++++++---- 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: - 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) + + 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: + 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):