add pgettext and npgettext

This commit is contained in:
Sardorbek Imomaliev 2020-01-13 16:18:10 +07:00 committed by David Lord
parent beabf304b0
commit 3fba898098
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
4 changed files with 168 additions and 24 deletions

View 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

View 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:

View 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):

View 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:
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):