Miguel David |||

How to make Jekyll multilingual

One of the few requirements that existed for this blog was to be multilingual. This is something that one can easily do with WordPress these days, but then I got turned on to GitHub Pages and one thing led to another and …. many hours later I had a blog that became multilingual, through mercilessly hacking the beautiful Whiteglass theme. Here is a brief explanation of how I made it happen.

Disclaimer: The code is not pretty. Consider it an MVP. :)

What you can learn from this post

  • How to turn a Jekyll theme multilingual

Where I started

Google of course! My thinking was There are multiple plugins for this in WordPress, there must be one for Jekyll”. And… I was wrong. For Jekyll, only one language at a time is allowed to exist in the world. But I did come across some very useful blog posts:

  • This one by Sylvain Durand was the most useful and insightful

  • The one from developmentseed was not as detailed but still insightful in using categories for languages

  • Finally these two: by drallgood and by Anthony Gaudino are good alternatives, but I already had a theme, I was limited in terms of plugins because of GitHub Pages and did not want to keep using translations inside the posts themselves

So I was stuck, and turned back to the author of the theme

Yous, the person who coded the Whiteglass theme, was super helpful and although he/she said that making the theme multilingual was not in the roadmap, he/she offered some tips on how to do it.

Here’s how…


In _config.yml I changed three things (relevant to the multilingual business)

{% raw %}

lang: en
languages: ["en", "pt"]
permalink: /:categories/:year/:month/:day/:title/

{% endraw %}

Line 1 establishes the general default language of the blog

Line 2 establishes an array of possible languages

Line 3 establishes that the URLs should include categories which will be used later


The _data/navigation.yml file was changed to add menus for the different languages as well

{% raw %}

  - language: "en"
    - title: "About"
      url: /about/
    - title: "Archives"
      url: /archives/
    - title: "GitHub"
      url: https://github.com/minac
    - title: "pt"
      url: /pt/
  - language: "pt"
    - title: "Sobre"
      url: /sobre/
    - title: "Arquivos"
      url: /arquivos/
    - title: "GitHub"
      url: https://github.com/minac
    - title: "en"
      url: /

{% endraw %}

There is no explaining to do, two languages, two subtrees with the correct URLs for either.


Then I needed to incorporate that into the _includes/header.html

{% raw %}

{% for item in site.data.navigation.languages %}
  {% if item.language == page.lang %}
    {% for link in item.links %}
      {% if link.url contains "http" %}
        {% assign url = link.url %}
      {% else %}
        {% assign url = link.url | relative_url %}
      {% endif %}
      <a class="page-link" href=""></a>
    {% endfor %}
  {% endif %}
{% endfor %}

{% endraw %}

Only the first 2 lines are important here, a loop and an if clause to show the navigation in the right language.


Then the magic starts happening in the _layouts/archive.html

{% raw %} This <h1 class="page-heading">Blog Archive</h1> becomes this <h1 class="page-heading"></h1>. To avoid hard coding titles. {% endraw %}

This {% raw %}{% for post in site.posts %}{% endraw %} becomes this:

{% raw %}

{% assign posts=site.posts | where: "lang", page.lang %}
{% for post in posts %}
{% endfor %}

{% endraw %}

So that the archives page is language aware.

And finally this:

{% raw %}

<span class="post-meta">{% if post.categories != empty %} • {% include category_links.html categories=post.categories %}{% endif %}</span>

{% endraw %}

Becomes this:

{% raw %}

<span class="post-meta">{% if post.tags != empty %} • {% for tag in post.tags %}{% endfor %}{% endif %}</span>

{% endraw %}

Because we are using categories for the languages, I went with tags for the categorization of the posts. That way each post will have one or more tags but the URL will only reflect the categories themselves.

_layouts/page.html and _layouts/post.html

Once the hard work is done, the _layouts/page.html and _layouts/post.html become easy.

Here’s some meta data added to the page’s post-header:

{% raw %}

<header class="post-header">
  <h1 class="post-title"></h1>
  <p class="post-meta"><time datetime="" itemprop="datePublished"></time> • {% assign pages=site.pages | where: "ref", page.ref | sort: 'lang' %}{% for page in pages %}<a href="" class=""></a> {% endfor %}</p>

{% endraw %}

And some meta data added to the post’s post-header:

{% raw %}

<p class="post-meta"><time datetime="" itemprop="datePublished"></time>{% if page.tags != empty %} • {% for tag in page.tags %} • {% endfor %}{% endif %} {% assign posts=site.posts | where: "ref", page.ref | sort: 'lang' %}{% for post in posts %}<a href="" class=""></a> {% endfor %} •  words</p>

{% endraw %}


I decided out of lazyness to use the index.html instead of home which the theme author suggests. Here are the contents:

{% raw %}

<div class="home">
  {% capture site_lang %}{% endcapture %}
  {% assign posts=site.posts | where:"lang", page.lang %}
  <ul class="post-list">
    {% for post in posts %}
      {% capture lang %}{% if post.lang != site_lang %}{% else %}{% endif %}{% endcapture %}
      <li{% if lang != empty %} lang=""{% endif %}>
        <header class="post-header">
          <h1 class="post-title">
            <a class="post-link" href="">{% if post.external-url %} &rarr;{% endif %}</a>
          <p class="post-meta">{% if post.tags != empty %} • {% for tag in post.tags %}  • {% endfor %}{% endif %}</p>
        <div class="post-content">
        {% if post.content contains site.excerpt_separator %}
          <p class="post-continue">
            <a href="">Read on &rarr;</a>
        {% endif %}
    {% endfor %}
  {% include pagination.html %}

{% endraw %}

Line 2, 3 and 6 are the only ones that matter for this purpose.

about’ becomes sobre’, archives’ become arquivos’

After all this was done I needed to create Portuguese pages for the English equivalents. So about.md got a sister sobre.md and archives.md got arquivos.md. The contents are not relevant.

File tree

So in the end this is the tree of files of the project:

{% raw %}


{% endraw %}

Most of the files were already part of the theme. The ones that I would like to draw your attention to are on lines 26/27, 28/29 and the English directory 35-37 and the Portuguese directory 38-40. pt.html is the unimaginatively named Portuguese index.html.

So now that all of that is done, how does one create a new post in Portuguese and English (or either one)?

Front matter for new posts

Well, see ./en/_posts/2017-03-04-new-blog-new-life.md and ./pt/_posts/2017-03-04-new-blog-new-life.md? Here are the respective Front matters:

{% raw %}

layout: post
title: "New blog, new life"
tags: [blog]
author: "Miguel David"
date: 2017-03-04 16:12:07 +0000
lang: en
ref: new-blog-new-life

{% endraw %}

And for the Portuguese one:

{% raw %}

layout: post
title: "Novo blog, nova vida"
tags: [blog]
author: "Miguel David"
date: 2017-03-04 16:12:07 +0000
lang: pt
ref: new-blog-new-life

{% endraw %}

What distinguises them (besides the contents after the front matter) is their localized title and lang. Everything else is the same, including the reference which is used to change between both languages when looking at the specific post!

I hope this helps!

Up next Podcasts I listen to I was asked the other day by a friend going on a long trip which podcasts I listen to. I typically listen to them while running, but they’re all My MySQL Cookbook Over the years I’ve been accumulating notes on how to do quick operations in a variety of technologies. These have been, up to now, for my eyes
Latest posts Git: Choose Your Adventure Stop before you’re stopped Today I learned about git and permissions Today I learned about view My git flow Using multiple versions of kubectl on macOS Merging two MySQL (or MySQL compatible) databases in AWS using DMS Inspired Recommendations for Portugal God is dead (and we are suffering from it) Advice to my unborn child: be a plumber and an artist On light and shadow You can usually do more/better than you think you can Upkeep Comparing myself to others Fear, Procrastination. Procrastination, Fear Liberalism and a new system Chores Finding my calm place Turning 38 Privilege Humbling Habits Frustration The miracle of the blank page On being late Love is the base of it all A letter to my dead grandfather Self imposed stress Rent the world Start at 6:30