How to translate a Jekyll site

This blog is available in two language: English and German. You can switch between the languages in the top right corner (at the time of writing at least). In this blog post I will write down the steps necessary to archive this.

The plugin

We use the plugin jekyll-multiple-languages-plugin.

Add this to your Gemfile:

group :jekyll_plugins do
   gem 'jekyll-multiple-languages-plugin'

and run bundle install.

Activate the plugin in your Jekyll config:

  - jekyll-multiple-languages-plugin

languages: ['en', 'de']
  exclude_from_localizations: ["javascript", "images", "css"]
  language_default: 'en'
    de: DE
    en: EN

I also added two languages, English and German. The default language of the website is set to English.

How to organize your files

Next, you have to create the correct folder. All translated posts will be put into the _i18n subfolder:

jekyll folder structure

Blog posts go to _i18n/en/_posts for English entries and _i18n/de/_posts. The static pages end up in _i18n/en or _i18n/de directly.

We keep the original _pages folder. The previous _posts folder is moved permanently to _i18n.

The language switcher

I use Pure for styling, so don’t care about the pure.button-group classes in the code.

<div class="pure-button-group language_switcher" role="group" aria-label="...">
{% for lang in site.languageNames \%\}
  {% for lang in site.languageNames \%\}
  {% if lang[0] == site.language_default %}
    {% if lang[0] == site.lang %}
      <a class="pure-button pure-button-active" href="{{ site.baseurl_root }}/">{{ lang[1] }}</a>
    {% else %}
      <a class="pure-button" href="{{ site.baseurl_root }}/">{{ lang[1] }}</a>
    {% endif %}
  {% else %}
    {% if lang[0] == site.lang %}
      <a class="pure-button pure-button-active" href="{{ site.baseurl_root }}/{{ lang[0] }}/">{{ lang[1] }}</a>
    {% else %}
      <a class="pure-button" href="{{ site.baseurl_root }}/{{ lang[0] }}/">{{ lang[1] }}</a>
    {% endif %}
  {% endif %}
{% endfor %}

Changes to the templates


In my default layout I set the HTML tag like this:

<!DOCTYPE html>
<html lang="{{ site.lang }}">

I also changed the site’s tagline like this:

<div class="site-info">
  <h1 class="site-name"><a href="{{ site.baseurl }}/">{{ }}</a></h1>
  <p class="site-description">{% t global.description  %}</p>

And here you can see the translation for the first time. {% t global.description} translates the site description based on the language the user has selected (and defaulting to english).

But where does the translation come from?

That’s were the yaml files in _i18n are used:

#_i18n/en.yml - english
  description: Welcome to nerdwana
  about: About
  categories: Categories
  filed_under: Filed under
  read_more: Read more
  welcome: welcome
  recent_blog_posts: Recent blog posts
  404-page-not-found: 404 - Page not found

The corresponding file for the German translation looks like this:

#_i18n/de.yml - german
  description: Willkommen im nerdwana
  about: Über
  categories: Kategorien
  filed_under: Eingeordnet unter
  read_more: Weiterlesen
  welcome: Willkommen
  recent_blog_posts: Kürzliche Blogeinträge
  404-page-not-found: 404 - Page not found

The string global.description is used for the tagline above. The remaining entries are used in the navigation or in the post layout (you can see examples later in the blog post):

<div class="custom-wrapper pure-g" id="menu">
    <div class="pure-u-1 pure-u-md-1-2">
        <div class="pure-menu">
            <a href="{{ site.baseurl }}/" class="pure-menu-heading custom-brand" style="text-transform: none; color: #777;">Blog</a>
            <a href="#" class="custom-toggle" id="toggle"><s class="bar"></s><s class="bar"></s></a>
    <div class="pure-u-1 pure-u-md-1-2">
        <div class="pure-menu pure-menu-horizontal custom-menu-3 custom-can-transform">
            <ul class="pure-menu-list">
                <li class="pure-menu-item"><a href="{{ site.baseurl }}/categories" class="pure-menu-link">{% t global.categories %}</a></li>
                <li class="pure-menu-item"><a href="{{ site.baseurl }}/about" class="pure-menu-link">{% t global.about %}</a></li>

Use baseurl_root for CSS files:

<link rel="stylesheet" href="{{ "/your.css" | prepend: site.baseurl_root }}"/>


Posts go to the _posts subfolder. The rest should work out of the box. A visitor will only see posts in the selected language, if you use site.posts:

<h1 class="content-subhead">{% t global.recent_blog_posts %}</h1>
<div class="posts">
  {% for post in site.posts %}
    <article class="post">
      <h2 class="post-title"><a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a></h2>
      <div class="content">
        {{ post.excerpt }}
      <a href="{{ site.baseurl }}{{ post.url }}" class="read-more">{% t global.read_more %}</a>
  {% endfor %}


What about pages? They are a bit more complicated as you don’t want to link them for every language separately. We don’t use the t or translate method here but tf to translate whole files.

First create a page stub in the _pages folder outside of _i8n like this:

layout: page
title: About
permalink: /about/
{% tf %}

This will look for a file in the language specific _i18n subfolder.

Next add the corresponding translation to either _i18n/en or _i18n/de:

#_i18n/en/ - the english version of this file
This is the About page.