Search code examples
pythonflaskjinja2python-babelflask-babel

Flask, Jinja2, Babel error on "$" character


I'm migrated my code from webapp2 to Flask. (I deploy my code in Google App Engine)

However, I can no longer use this string: "Error: Max %1$d characters"

Initialization

flask_app = Flask(__name__)
babel = Babel(flask_app, default_domain='strings')

Html template

<div class="...">{{ _('error_long_value') | replace('%1$d', '200') }}</div>

I know that this is not the best use, but I need to keep %1$d as placeholder. (It was working with webapp2)

Log:

...
File ".../libs/flask/templating.py", line 135, in render_template
context, ctx.app)
File ".../libs/flask/templating.py", line 117, in _render
rv = template.render(context)
File ".../libs/jinja2/environment.py", line 1008, in render
return self.environment.handle_exception(exc_info, True)
File ".../libs/jinja2/environment.py", line 780, in handle_exception
reraise(exc_type, exc_value, tb)
File ".../app/templates/filename.html", line 567, in top-level template code
<div class="invalid-feedback">{{ _('error_long_value') | replace('%1$d', '200') }}</div>
ValueError: unsupported format character '$' (0x24) at index 29

I've already tried to use the "| e" or "| safe" after " _('error_long_value')" in the HTML template, removing the replace().


Solution

  • This is a two-fold problem coming from the fact that flask (and jinja more specifically), when interpreting text from templates and applying filters and/or context processors, it uses the string % something operation liberally.

    This operation interprets the %1$d text like a formatting string, an invalid one that is causing the error. So what you need to do is replace both of the operations you're doing in the template, since the ones provided by flask (and its extensions, usually) are likely to cause errors because of the aforementioned modulus operation.

    First, you can create a context processor using the babel gettext directly:

    from flask_babel import gettext
    #...
    @app.context_processor
    def my_gettext():
      return {'my_gettext': gettext}
    

    Now a filter for the text replacement:

    @app.template_filter()
    def my_replace(text, old, new):
      return text.replace(old, new)
    

    With this, in your template you'd use:

    <div class="...">{{ my_gettext('error_long_value') | my_replace('%1$d', '200') }}</div>