Search code examples
pythoncompilationjinja2python-babel

Extending Jinja's {% trans %} to use JavaScript variables


I'd like to extend the behaviour of trans by rendering variables not as as values from the context, but instead as html (without using the context). My aim is to be able to populate those variables on the client through JavaScript.

Jinja as it seems doesn't allow for a great deal of customisation of this kind or I'm just unable to find the right hooks.

Here's what I'd like to achieve:

{% etrans name=username %}
My name is {{ name }}
{% endetrans %}

This should render to:

My name is <span id='#username'></span>

Of course, I could just use the normal {% trans %} directive and pass my html code to template.render(html_code_params), but that would require to have them defined in the template and the rendering code which I'd like to avoid.

Here's what I got so far (not much) which allows for a new etrans tag and the ability to use whatever goodies InternationalizationExtension has to offer.

from jinja2.ext import InternationalizationExtension
from jinja2.runtime import concat


class JavaScriptVariableExtension(InternationalizationExtension):
    tagname = 'etrans'
    tags = set([tagname])

    def _parse_block(self, parser, allow_pluralize):
        """Parse until the next block tag with a given name.

        Copy from InternationalizationExtension, as this uses hardcoded
        `name:endtrans` instead of relying on tag name
        """
        referenced = []
        buf = []
        while 1:
            if parser.stream.current.type == 'data':
                buf.append(parser.stream.current.value.replace('%', '%%'))
                next(parser.stream)
            elif parser.stream.current.type == 'variable_begin':
                next(parser.stream)
                name = parser.stream.expect('name').value
                referenced.append(name)
                buf.append('%%(%s)s' % name)
                parser.stream.expect('variable_end')
            elif parser.stream.current.type == 'block_begin':
                next(parser.stream)
                # can't use hardcoded "endtrans"
                # if parser.stream.current.test('name:endtrans'):
                if parser.stream.current.test('name:end%s' % self.tagname):
                    break
                elif parser.stream.current.test('name:pluralize'):
                    if allow_pluralize:
                        break
                    parser.fail('a translatable section can have only one '
                                'pluralize section')
                parser.fail('control structures in translatable sections are '
                            'not allowed')
            elif parser.stream.eos:
                parser.fail('unclosed translation block')
            else:
                assert False, 'internal parser error'

        return referenced, concat(buf)


i18n_extended = JavaScriptVariableExtension

I don't mind overloading more methods (although the reason for above one should perhaps fixed upstream).

Stepping through the code is quite an interesting adventure. However, I hit a snag and am interested if anyone can give some advice.

The problem I see is that during the compilation, the function context.resolve() gets baked into the compiled code. jinja2.jinja2.compiler.CodeGenerator doesn't really allow any different handling here (correct me if I'm wrong). Ideally, I would define another node (for the variable) and this node would handle the way it's dealt with during compilation, but I don't see how this is possible. I might be too focussed on this as a solution, so perhaps someone can provide alternatives.


Solution

  • As suggested by @Garrett's comment, a much easier solution is to pass in a function to the template renderer that interpolates the variables. In my case, my target client-side framework is Angular, but this also works for any JS variables that you want to use within a {% trans %} environment. Here are the building blocks:

    def text_to_javascript(string):
        # modify as needed... 
        return "<span>{{ %s }}</span>" % string
    
    def render():
        tmpl = jinja_env.get_template(template_filename)
        return tmpl.render({'js': text_to_javascript})
    

    And this how I make use of it in the template file:

    {% trans username=js('user.name') %}
      My name is {{ username }}
    {% endtrans %}
    

    In the Angular controller, the variable user is bound to the $scope like so:

    $scope.user = {'name': 'Bugs Bunny'}