eckler singular noun = a dumb thing on the internet
the selected theme is DARK
the selected font is CLASSIC (pixelated)

setting up a multilingual website with Zola

> go back to: my blog

the official zola docs were incomplete or unclear, so here is my own take on it

why this article exists

the main goal of this article is to explore how Zola lets you create a multilingual website and what else you can do with some templating magic

its contents are heavily inspired by my own experience of working with Zola and trying to set my site up so i can have both an english and a french version of it

what Zola gives you out of the box

let's first take a look at what the Zola engine lets us do

automatic term translation

by default, the config.toml file lets you specify translations of some variables. let's see what it looks like with the following example:

# Default language is English
default_language = "en"

# English versions of some terms
[translations]
foo = "foo"
bar = "bar"

# French versions of some terms
[languages.fr.translations]
foo = "toto"
bar = "titi"

in the above example, the default language for the website will be en (English). we then listed the default translations for two variable under the [translations] section

we also defined translations for these variables in fr (French) in the [languages.fr.translations] part

these translations are then exposed to all the templates (ending in .html), and can be retrieved using the trans function, like this:

<button>{{ trans(key="foo") }}</button>

and the result will be: (what we set for the default translation)

what is not obvious when looking at the docs is that this function takes an optional second argument named lang. this argument should be a valid language code defined in your config for the key you passed to the function. what you's expect is that the default behaviour is to pass the current page's lang to the function. however, what Zola does is passing the default lang you specified in the config.toml

to be clear, what this means is that

{{ trans(key="bar") }}

is always equivalent to

{{ trans(key="bar", lang="en") }}

in my example

rest assured, the simple workaround for this is to always use

{{ trans(key="bar", lang=lang) }}

this variant is able to retrieve the current page's lang variable and pass it to the function so that the translation works as intended. this leads to more verbose code, but it works

language conditional text

as said previously, every page and section exposes a lang variable that is set to the current language code of it. this variable can then be used to define conditional html parts in a template

1{% if lang == 'en' %}
2<p>FOO <i>bar</i></p>
3{% elif lang == 'fr' %}
4<p>BAR <i>foo</i></p>
5{% endif %}

for example, the above code would display <p>FOO <i>bar</i></p> on english pages and <p>BAR <i>foo</i></p> on french ones

for this page, the result would be:

FOO bar

moreover, if we ever modified the code like this:

5{% else %}
6<p><b>Language "{{ lang }}" not supported</b></p>
7{% endif %}

, it would display that the language of a page has no option for this particular part of the template. if the {% else %} part is not set, this would simply not display anything

this different way of dealing with multiple languages is particularly useful if there are some places where it'd make it more readable to have all the versions in the same place instead of in the config.toml, which should preferably be used for text that's used in multiple places or is short

furthermore, leaving a fallback case when the language is not supported allows us to work first on some part of the translation, leaving the rest for later while still having a functioning website

by default, every page exposes a translations variable, described here in the docs

this variable is accessible within templates, using the section.translations or page.translations variables. for example, one could create a list of variants for a page like this:

{% if page.translations | length > 1 %}
    <ul>
    {% for t in page.translations %}
        <li><a href="{{ t.permalink }}">{{ t.lang }} version: {{ t.title }}</a></li>
    {% endfor %}
    </ul>
{% endif %}

note: we check that the translations list has more than one element, because it will always contain the current version of the page

what is possible with some tweaks

now, let's see what other things we can achieve using the knowledge we have

extending translated pages navigation to make it automatic

one of the main issues with the method described in the previous section is that one would need to add the translation list to every template, adapting it so that it works with both sections and pages. one way to do it is using a small html file that is then included in the page, such as this one:

<!-- translations_list.html -->
{% if page.translations %}
    {% set translations = page.translations %}
{% else %}
    {% set translations = section.translations %}
{% endif %}

{% if translations | length > 1 %}
<ul>
    {% for t in translations %}
        {% if lang != t.lang %}
            {% if t.lang == 'fr' %}
            <li>cette page existe aussi en <a href="{{ t.permalink }}"><i>français : {{ t.title }}</i></a></li>
            {% elif t.lang == 'en' %}
            <li>this page is also available in <a href="{{ t.permalink }}"><i>english: {{ t.title }}</i></a></li>
            {% endif %}
        {% endif %}
    {% endfor %}
</ul>
{% endif %}

integrating this to an existing base of templates

one issue we might encounter is that most templates one uses will be extended from a base.html template, and the current version of the engine doesn't accept the inclusion of another template when the current one already extends one. simply put, the code below would not function:

{% extends "base.html" %}

{% block content %}

{% include "translations_list.html" %}

{% endblock content %}

to solve this issue, i simply included the translations_list.html file inside of my base.html file in a specific block, and then added this block to every template i might want it in, adding some context around, like the example below:

<!-- base.html -->
{% block top %}

{% include "components/translations_list.html" %}
                
{% endblock top %}

<!-- page.html -->
{% extends "base.html" %}

{% block top %}

<h1>{{ page.title }}</h1>

{{ super() }}

<h2>{{ page.description }}</h2>

{% endblock top %}

this setup is what lets me have the "available translations" list at the top of every page and section on this website

aside, what else is possible with Zola

now that we've taken a look at what is possible with translations, i'll present some other interesting things i have achieved with Zola and its templating engine

by default, every page and section exposes a list of ancestors. this list contains the name of the files defining the sections prior to it. for example, this article would have:

// English version
page.ancestors = [ "_index.md", "blog/_index.md" ]

// French version
page.ancestors = [ "_index.fr.md", "blog/_index.fr.md" ]

this set of ancestors is very useful, because it allows us to craft breadcrumbs or links back to the previous section. to be able to do this, we need to use the provided function called get_section, that gives back a bunch of metadata about the section for which we specified the path (relative to the content/ folder). what interests us the most in our case is:

  1. section.title: the title of the section we retrieved
  2. section.permalink: the link to it

these two pieces of information allow us to craft links to the section we got from the get_section function, like this:

{% set ancestor = get_section(path=ancestors | last) %}
<a href="{{ ancestor.permalink }}">{{ ancestor.title }}</a>

automated breadcrumbs

previously, we used the last mapping to get only the last element of the page's ancestors. this is what i use for this website to create the little "go back to: [thing]" link on top of my pages. however, one could also use it to create breadcrumbs for their website, like this:

{% for a in ancestors %}
    {% set ancestor = get_section(path=a) %}
    > <a href="{{ ancestor.permalink }}">{{ ancestor.title }}</a>
{% endfor %}
> {% if page.title %} {{ page.title }} {% else %} {{ section.title }} {% endif %}

for this page, the result would be:

> welcome to my swamp > my blog > setting up a multilingual website with Zola

note on this section

in this section's code listings, i have assumed the ancestors variable to be accessible. this is not the default Zola behaviour though, so what is necessary for it to work across all pages and sections is just to add the simple code below before any use of the variable. this exposes the correct variable corresponding to the current type of html template being processed

{% if page.ancestors %}
    {% set ancestors = page.ancestors %}
{% else %}
    {% set ancestors = section.ancestors %}
{% endif %}

furthermore, the part about backlinks would be correct for every page except the landing page

this is due to the way Tera always gives back an element from {{ ancestors | last }}, even when the ancestors array is empty. this mapping gives back an empty string, which would result in the following error when rendering the home page:

Error: Reason: Function call 'get_section' failed
Error: Reason: Section `` not found.

to avoid this error, we can just wrap the whole call to ancestors in an if statement, giving the following code:

{% if page.ancestors %}
    {% set ancestors = page.ancestors %}
{% else %}
    {% set ancestors = section.ancestors %}
{% endif %}

{% if ancestors | length > 0  %}
    {% set parent = get_section(path=(ancestors | last)) %}
    <a href="{{ parent.permalink }}" style="margin-left: 0.5rem;">> {{ trans(key="back", lang=lang )}} {{ parent.title }}</a>
{% endif %}

note: in the code sample above, we reference the translated variable back. to see how these are setup, check the automatic term translation section of this article

conclusion

Zola is a great SSG, and it allows a lot of stuff out of the box. however, it was lacking some features that i really wanted to have for my own website. these were not that trivial to implement, and the official documentation didn't help that much to understand how everything works

pro-tip if you ever get lost in what one object you're manipulating contains, just write the code sample below in any template file the current page uses and see what fields you have at your disposal

{% for key, value in OBJECT %}
<p><b>{{ key }}</b> -> {{ value }}</p>
{% endfor %}

this simple thing helped a lot when trying to implement the features described in this article, so i can attest personnally of all the good it does

thanks for reading this article, i appreciate any feedback you could have about it 💜

-- eckler