Adding url_for() links to Jinja templates of a Flask multilanguage website

15 September 2019 Updated 15 September 2019 by Peter

In a multilanguage website with multilanguage slugs we can no longer use <a href="{{ url_for('pages.about') }}">{{ _('About') }}</a>.

post main image
unsplash.com/@andrewtneel
Before you read on, you may want to read my previous posts about multilanguage and language fallback, see links below. When I gave myself the assignment to develop and implement a multilanguage Flask website with SQLAlchemy I knew things could get difficult. I did not really took the time to design everything beforehand, I just read a lot about multilanguage on the internet and trusted my experience to design while writing code that could be expanded without to much effort. Still, sometimes you can get so involved in getting code working that you forget simple things. 

The problem: url_for() in Jinja templates

In the pages blueprint views.py I had defined the the endpoints for the contact, about, terms of use and privacy policy page: 
@pages_blueprint.route('/contact')
def contact():
    return page_view('contact')


@pages_blueprint.route('/about')
def about():
    return page_view('about')

@pages_blueprint.route('/terms-of-use')
def terms_of_use():
    return page_view('terms-of-use')


@pages_blueprint.route('/privacy-policy')
def privacy_policy():
    return page_view('privacy-policy')

I removed these lines so the only remaining endpoint for pages would be the page_view function:

@pages_blueprint.route('/<slug>', methods=['GET', 'POST'])
def page_view(slug):
    ...

Then the pages stopped working ... of course. At some places in the templates I still used url_for() to generate the link to a page. For example, the 'Read more' was still specified as:

<a href="{{ url_for('pages.about') }}">{{ _('Read more') }}</a>

And on the Register page there was the link:

* {{ _('See our') }} <a href="{{ url_for('pages.privacy_policy') }}" class="link">{{ _('privacy policy') }}</a> {{ _('how we process and protect your personal data.') }}

Both failed with the famous:

werkzeug.routing.BuildError: Could not build url for endpoint ...

Nothing special and yes it was expected, but how to solve this and more important, how to solve this in a multilanguage way?

The solution: a nested dict for every language selected 

The solution is not very difficult.  As I mentioned in previous posts we have two tables:

ContentItem
ContentItemTranslation

ContentItem contains a 'common' name. For example the About page has a common name 'About' and then the ContentItemTranslation records contain the translated page title, for the Dutch language this is 'Over ons' and the derived slug is 'over-ons'.  To be able to supply the proper data to the url_for() function we need to create a dictionary with the following structure:

{
    'About': { 'title': title, 'slug': slug },
    'Privacy policy': { 'title': title, 'slug': slug },
    ... 
}

Then we need the context processor to get this dictionary into the template, here I use app_template_slug as the dictionary name. In the Jinja template we can use this in the following way:

* {{ _('See our') }} <a href="{{ url_for('pages.page_view', slug=app_template_slug['Privacy policy']['slug']) }}" class="link">{{ app_template_slug['Privacy policy']['title'] }}</a> {{ _('how we process and protect your personal data.') }}

We use the 'common' name to get the proper title and slug, so nothing language dependent here. As with other functionalities I created the object that takes cares of the generation of the nested dictionary in the create_app function.

Links / credits

Multilanguage fallback revisited and a page footer with multilanguage links
/en/blog/multilanguage-fallback-revisited-and-a-page-footer-with-multilanguage-links

Refining multilanguage: adding language fallback as an option
/en/blog/refining-multilanguage-adding-language-fallback-as-an-option