Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ext.i18n policy to fallback to untranslated string when interpolation fails #1764

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/jinja2/defaults.py
Expand Up @@ -45,4 +45,5 @@
"json.dumps_function": None,
"json.dumps_kwargs": {"sort_keys": True},
"ext.i18n.trimmed": False,
"ext.i18n.newstyle_fallback_interpolation": False,
}
80 changes: 62 additions & 18 deletions src/jinja2/ext.py
Expand Up @@ -167,21 +167,30 @@ def _gettext_alias(
return __context.call(__context.resolve("gettext"), *args, **kwargs)


def _make_new_gettext(func: t.Callable[[str], str]) -> t.Callable[..., str]:
def _make_new_gettext(
func: t.Callable[[str], str], newstyle_fallback_interpolation: bool
) -> t.Callable[..., str]:
@pass_context
def gettext(__context: Context, __string: str, **variables: t.Any) -> str:
rv = __context.call(func, __string)
if __context.eval_ctx.autoescape:
rv = Markup(rv)
# Always treat as a format string, even if there are no
# variables. This makes translation strings more consistent
# and predictable. This requires escaping
return rv % variables # type: ignore
try:
# Always treat as a format string, even if there are no
# variables. This makes translation strings more consistent
# and predictable. This requires escaping
return rv % variables # type: ignore
except KeyError:
if newstyle_fallback_interpolation:
return __string % variables
raise

return gettext


def _make_new_ngettext(func: t.Callable[[str, str, int], str]) -> t.Callable[..., str]:
def _make_new_ngettext(
func: t.Callable[[str, str, int], str], newstyle_fallback_interpolation: bool
) -> t.Callable[..., str]:
@pass_context
def ngettext(
__context: Context,
Expand All @@ -194,13 +203,22 @@ def ngettext(
rv = __context.call(func, __singular, __plural, __num)
if __context.eval_ctx.autoescape:
rv = Markup(rv)
# Always treat as a format string, see gettext comment above.
return rv % variables # type: ignore
try:
# Always treat as a format string, see gettext comment above.
return rv % variables # type: ignore
except KeyError:
if newstyle_fallback_interpolation:
if __num > 1:
return __singular % variables
return __plural % variables
raise

return ngettext


def _make_new_pgettext(func: t.Callable[[str, str], str]) -> t.Callable[..., str]:
def _make_new_pgettext(
func: t.Callable[[str, str], str], newstyle_fallback_interpolation: bool
) -> t.Callable[..., str]:
@pass_context
def pgettext(
__context: Context, __string_ctx: str, __string: str, **variables: t.Any
Expand All @@ -211,14 +229,19 @@ def pgettext(
if __context.eval_ctx.autoescape:
rv = Markup(rv)

# Always treat as a format string, see gettext comment above.
return rv % variables # type: ignore
try:
# Always treat as a format string, see gettext comment above.
return rv % variables # type: ignore
except KeyError:
if newstyle_fallback_interpolation:
return __string % variables
raise

return pgettext


def _make_new_npgettext(
func: t.Callable[[str, str, str, int], str]
func: t.Callable[[str, str, str, int], str], newstyle_fallback_interpolation: bool
) -> t.Callable[..., str]:
@pass_context
def npgettext(
Expand All @@ -236,8 +259,15 @@ def npgettext(
if __context.eval_ctx.autoescape:
rv = Markup(rv)

# Always treat as a format string, see gettext comment above.
return rv % variables # type: ignore
try:
# Always treat as a format string, see gettext comment above.
return rv % variables # type: ignore
except KeyError:
if newstyle_fallback_interpolation:
if __num > 1:
return __singular % variables
return __plural % variables
raise

return npgettext

Expand Down Expand Up @@ -320,17 +350,31 @@ def _install_callables(
pgettext: t.Optional[t.Callable[[str, str], str]] = None,
npgettext: t.Optional[t.Callable[[str, str, str, int], str]] = None,
) -> None:
newstyle_fallback_interpolation = self.environment.policies[
"ext.i18n.newstyle_fallback_interpolation"
]
if newstyle is not None:
self.environment.newstyle_gettext = newstyle # type: ignore
if self.environment.newstyle_gettext: # type: ignore
gettext = _make_new_gettext(gettext)
ngettext = _make_new_ngettext(ngettext)
gettext = _make_new_gettext(
gettext, newstyle_fallback_interpolation=newstyle_fallback_interpolation
)
ngettext = _make_new_ngettext(
ngettext,
newstyle_fallback_interpolation=newstyle_fallback_interpolation,
)

if pgettext is not None:
pgettext = _make_new_pgettext(pgettext)
pgettext = _make_new_pgettext(
pgettext,
newstyle_fallback_interpolation=newstyle_fallback_interpolation,
)

if npgettext is not None:
npgettext = _make_new_npgettext(npgettext)
npgettext = _make_new_npgettext(
npgettext,
newstyle_fallback_interpolation=newstyle_fallback_interpolation,
)

self.environment.globals.update(
gettext=gettext, ngettext=ngettext, pgettext=pgettext, npgettext=npgettext
Expand Down
17 changes: 17 additions & 0 deletions tests/test_ext.py
Expand Up @@ -52,6 +52,7 @@
"novars.html": "{% trans %}%(hello)s{% endtrans %}",
"vars.html": "{% trans %}{{ foo }}%(foo)s{% endtrans %}",
"explicitvars.html": '{% trans foo="42" %}%(foo)s{% endtrans %}',
"broken_interpolation.html": "{% trans %}Username: {{ username }}{% endtrans %}",
}


Expand All @@ -66,6 +67,7 @@
"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"},
"Username: %(username)s": "Nutzername: %(user_name)",
}
}

Expand Down Expand Up @@ -147,6 +149,14 @@ def npgettext(context, c, s, p, n):
gettext, ngettext, newstyle=True, pgettext=pgettext, npgettext=npgettext
)

newstyle_i18n_env_fallback = Environment(
loader=DictLoader(newstyle_i18n_templates), extensions=["jinja2.ext.i18n"]
)
newstyle_i18n_env_fallback.policies["ext.i18n.newstyle_fallback_interpolation"] = True
newstyle_i18n_env_fallback.install_gettext_callables( # type: ignore
gettext, ngettext, newstyle=True, pgettext=pgettext, npgettext=npgettext
)


class ExampleExtension(Extension):
tags = {"test"}
Expand Down Expand Up @@ -610,6 +620,13 @@ def test_context_plural_block(self):
assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apple"
assert tmpl.render(LANGUAGE="de", apples=5) == "5 Apples"

def test_broken_translation_interpolation(self):
tmpl = newstyle_i18n_env.get_template("broken_interpolation.html")
with pytest.raises(KeyError):
tmpl.render(LANGUAGE="de", username="NewUser")
tmpl = newstyle_i18n_env_fallback.get_template("broken_interpolation.html")
assert tmpl.render(LANGUAGE="de", username="NewUser") == "Username: NewUser"


class TestAutoEscape:
def test_scoped_setting(self):
Expand Down