Jekyll Translation: How to add a Second Language to a Jekyll Website

Adding one or more languages to your existing website can help reach a wider audience.

In this article, you will go through all the steps of adding a second language to your Jekyll website without using plugins.

Requirements

  1. The default language urls stays in the root of the project(/) while the new language gets its own folder. So, if the default language is English, all English posts url’s will start with /. If the new language is German, all the new languages posts have the url starting with /de/.
  2. Keep the collections structure Collections are useful when you want to list a set of pages within another page. This website used 2 collections: team and services. You are required to keep them, so that all the features of the collection pages work.

Getting Started

Setup and run the project locally so that you are able to test each step for yourself.

  1. Ensure you have Jekyll installed on your computer. Instructions for installing Jekyll.
  2. Download your project onto your computer. Link to the project’s files I will be using for this tutorial - Zip file.
  3. Inside your project’s folder, install everything your website needs using the command bundle.
  4. Run your website locally on your computer using the command jekyll serve or bundle exec jekyll serve. Use the one that works for your project.

Note: Not all Jekyll websites use collections. So, there is nothing wrong with your current Jekyll website if it does not have collections.

Organizing the Website Files According To Languages

1. Add a unique ref attribute to each content file(pages and posts)

The current content files on our website are:

about.md
index.md
contact.md
services.md
team.md
_services
 └───  service-1.md
 └───  service-2.md
_teams
   └───  team-member-1.md
   └───  team-member-2.md

Open each content file and inside the frontmatter, add a ref value that is unique to that page or post. For example, in home page(index.md), you can add the value ref: home.

You current full frontmatter should be:

---
title: Jekyll Serif Theme
layout: home
description: Jekyll Serif contains content types for a typical business website. The theme is fully responsive, blazing fast and artfully illustrated.
intro_image: "/images/illustrations/pointing.svg"
intro_image_absolute: true
intro_image_hide_on_mobile: true
show_call_box: true
ref: home
---

Repeat this for all the pages and posts.

The ref will be the unique identifier that will be used to retrieve the same posts that has been translated into different languages. Posts with the same ref attribute means they are a translation of each other.

It is better to add the ref attribute as early as possible, to eliminate the double work of adding the same ref attribute in two or more places.

2. Create the new language website files

Create a new folder that will contain the files of the new language.

Since our new language is German and the German language code is de, you new folder is going to be named _de. Note the folder name start with an underscore. Jekyll, checks for the underscore before creating the collections. Without the underscore, Jekyll will not create any collections.

Copy all the files and folders into new language folder(_de) except the _layouts, _includes, and _data folders. Maintain the same folder structure as the original.

Your project folder should appear like this:

about.md
index.md
contact.md
services.md
team.md
_data
_includes
_layouts
_services
 └───  service-1.md
 └───  service-2.md
_teams
   └───  team-member-1.md
   └───  team-member-2.md
_de
 └───index.md
 └───about.md
 └───contact.md
 └───services.md
 └───team.md
 └───_services
        └───  service-1.md
        └───  service-2.md
 └───_teams
        └───  team-1.md
        └───  team-2.md

3. Add new language collection to configuration(_config.yml) file

To make new language(_de) collection work, add it in the _config.yml under the collections section.

collections:
  services:
    output: true
    sort_by: weight
  team:
    output: true
  de:
    output: true

The “services” and the “team” collections were already there. Keep them there and add your “de” collection below, without the underscore.

4. Rename the _teams and _services folders inside _de to teams and services

Jekyll require collections to be at the root folder and to start with an underscore. So Jekyll is not creating collections for _teams and _services folders insided _de. Remove the underscore on the teams and services folder inside _de folder.

Now Jekyll can successfully generate the teams and services files under _de collection.

5. Translate the Content Files

Since you now have files for the new language, you can start translating the content.

Translate all the content in the files inside your new language folder(_de).

The other content to be translated is in the _data folder. If you need to hire someone to do the translation. Send the files in those two folders.

Since, I don’t know how to speak and write German, I will add the language code(DE) in the title of each file so that I can differentiate which files are being loaded.

6. View Current Website In Browser

Type the command bundle exec jekyll serve to run the website locally. Visit http://localhost:4000 to view the website.

You will note several things: a. The homepage for English(default language) is working while homepage for German(second language) fails.

[Picture Two screenshots side by side]

The contact, about, and pages works properly for both the english and … German version.

b. The layouts for are not working for the pages:

c. The team members and services listed in the German’s Home, Services, and team pages are the original English ones.

Lets proceed to correcting these issues.

7. Make Second Language Home Page Work

Open the file _de/index.md and add permalink: "/de/" in the frontmatter.

Now, if you check, the home page for the second language should load properly.

Image before and after change screenshots

8. Make Second Language Team Page Show Up

The log shows that the page is being displayed on the same path as the English. This happens since the permalink value is set in the front matter. So changing the value from /team/ to /de/team/ solves that problem.

9. Make Teams and Services Layouts work for Second Language

In the _config.yml file under the defaults section, assign the appropriate layouts to these pages using:

defaults:
  - scope:
      path: "_de/services/"
    values:
      layout: "service"
  - scope:
      path: "_de/team/"
    values:
      layout: "team"

Check to confirm that the layouts are working properly.

Screenshots of before and after

10. Set Page Language for All Pages

We need to provide a language identifier to all the pages. Luckily, you can easily add to all the pages with respect to their folder location using the _config.yml file.

defaults:
  - scope:
      path: "_de"
    values:
      lang: de
  - scope:
      path: ""
    values:
      lang: en

Any files in the root directory will get a lang: "en" attribute while any files under _de folder will get an atttribute lang: "de".

You can choose to do it the hard way. Instead of using the _config.yml file, go to each file and add lang: en or lang: de in the front matter of each page depending on the language.

The lang attribute will be very useful in our next step.

Listing Pages Within Pages According To Languages

11. Listing German’s Team Members Pages in Main Team Page

Change the current code for listing team members from:


<div class="container pt-6 pb-6">
    <div class="row">
        {% assign promoted_teams = site.team | where: "promoted", true | sort: "weight" %}
        {% for team in promoted_teams %}
        <div class="col-12 col-md-6 mb-2">
            <div class="team team-summary team-summary-large">
                {% if team.image %}
                <div class="team-image">
                    <img width="90" height="90" alt="{{ team.title }}" class="img-fluid mb-2" src="{{ team.image | relative_url }}" />
                </div>
                {% endif %}
                <div class="team-meta">
                    <h2 class="team-name"><a href="{{ team.url | relative_url }}">{{ team.title }}</a></h2>
                    <p class="team-description">{{ team.jobtitle }}</p>
                    {% if team.linkedinurl %}
                    <a target="_blank" href="{{ team.linkedinurl }}" rel="noreferrer">LinkedIn</a>
                    {% endif %}
                </div>
                <div class="team-content">{{ team.content | truncate: 120 }}</div>
            </div>
        </div>
        {% endfor %}
    </div>
    <div class="row pt-6 pb-6">
        {% assign teams = site.team | where: "promoted", empty | sort: "weight" %}
        {% for team in teams %}
        <div class="col-12 col-md-4 mb-3">
            <div class="team team-summary">
                {% if team.image %}
                <div class="team-image">
                    <img width="60" height="60" alt="{{ team.title }}" class="img-fluid mb-2" src="{{ team.image | relative_url }}" />
                </div>
                {% endif %}
                <div class="team-meta">
                    <h2 class="team-name"><a href="{{ team.url | relative_url }}">{{ team.title }}</a></h2>
                    <p class="team-description">{{ team.jobtitle }}</p>
                </div>
            </div>
        </div>
        {% endfor %}
    </div>
</div>

to:


{% if page.path != page.name %}
    {% comment %}Display translated team members{% endcomment %}
    <div class="container pt-6 pb-6">
        <div class="row">
            {% assign promoted_teams = site[page.lang] | where: "promoted", true | sort: "weight" %}
            {% for team in promoted_teams %}
            <div class="col-12 col-md-6 mb-2">
                <div class="team team-summary team-summary-large">
                    {% if team.image %}
                    <div class="team-image">
                        <img width="90" height="90" alt="{{ team.title }}" class="img-fluid mb-2" src="{{ team.image | relative_url }}" />
                    </div>
                    {% endif %}
                    <div class="team-meta">
                        <h2 class="team-name"><a href="{{ team.url | relative_url }}">{{ team.title }}</a></h2>
                        <p class="team-description">{{ team.jobtitle }}</p>
                        {% if team.linkedinurl %}
                        <a target="_blank" href="{{ team.linkedinurl }}" rel="noreferrer">LinkedIn</a>
                        {% endif %}
                    </div>
                    <div class="team-content">{{ team.content | truncate: 120 }}</div>
                </div>
            </div>
            {% endfor %}
        </div>
        <div class="row pt-6 pb-6">
        {% assign teams = site[page.lang] | where: "promoted", empty | sort: "weight" %}
        {% for team in teams %}
            {% if team.layout == "team" %}
                <div class="col-12 col-md-4 mb-3">
                    <div class="team team-summary">
                        {% if team.image %}
                        <div class="team-image">
                            <img width="60" height="60" alt="{{ team.title }}" class="img-fluid mb-2" src="{{ team.image | relative_url }}" />
                        </div>
                        {% endif %}
                        <div class="team-meta">
                            <h2 class="team-name"><a href="{{ team.url | relative_url }}">{{ team.title }}</a></h2>
                            <p class="team-description">{{ team.jobtitle }}</p>
                        </div>
                    </div>
                </div>
            {% endif %}
        {% endfor %}
    </div>
{% else %}
    {% comment %}Display default language team members {% endcomment %}
    <div class="container pt-6 pb-6">
        <div class="row">
            {% assign promoted_teams = site.team | where: "promoted", true | sort: "weight" %}
            {% for team in promoted_teams %}
            <div class="col-12 col-md-6 mb-2">
                <div class="team team-summary team-summary-large">
                    {% if team.image %}
                    <div class="team-image">
                        <img width="90" height="90" alt="{{ team.title }}" class="img-fluid mb-2" src="{{ team.image | relative_url }}" />
                    </div>
                    {% endif %}
                    <div class="team-meta">
                        <h2 class="team-name"><a href="{{ team.url | relative_url }}">{{ team.title }}</a></h2>
                        <p class="team-description">{{ team.jobtitle }}</p>
                        {% if team.linkedinurl %}
                        <a target="_blank" href="{{ team.linkedinurl }}" rel="noreferrer">LinkedIn</a>
                        {% endif %}
                    </div>
                    <div class="team-content">{{ team.content | truncate: 120 }}</div>
                </div>
            </div>
            {% endfor %}
        </div>
        <div class="row pt-6 pb-6">
            {% assign teams = site.team | where: "promoted", empty | sort: "weight" %}
            {% for team in teams %}
            <div class="col-12 col-md-4 mb-3">
                <div class="team team-summary">
                    {% if team.image %}
                    <div class="team-image">
                        <img width="60" height="60" alt="{{ team.title }}" class="img-fluid mb-2" src="{{ team.image | relative_url }}" />
                    </div>
                    {% endif %}
                    <div class="team-meta">
                        <h2 class="team-name"><a href="{{ team.url | relative_url }}">{{ team.title }}</a></h2>
                        <p class="team-description">{{ team.jobtitle }}</p>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
{% endif %}

12. Listing German’s Service Pages in Main Service Page

Change the current code from:


<div class="container pt-6 pb-6">
    <div class="row">
        {% for service in site.services %}
        <div class="col-12 col-md-6 mb-3">
        <div class="service service-summary">
            <div class="service-content">
            <h2 class="service-title">
                <a href="{{ service.url | relative_url }}">{{ service.title }}</a>
            </h2>
            <p>{{ service.content | markdownify | strip_html | truncate: 100 }}</p>
            </div>
        </div>
        </div>
        {% endfor %}
    </div>
</div>

to:


{% if page.path != page.name %}
    {% comment %}Display translated team members  {% endcomment %}
    <div class="container pt-6 pb-6">
        {% assign translatedservices = site[page.lang] %}
        <div class="row">
            {% for service in translatedservices %}
                    {% if service.layout == "service" %}
                    <div class="col-12 col-md-6 mb-3">
                        <div class="service service-summary">
                            <div class="service-content">
                            <h2 class="service-title">
                                <a href="{{ service.url | relative_url }}">{{ service.title }}</a>
                            </h2>
                            <p>{{ service.content | markdownify | strip_html | truncate: 100 }}</p>
                            </div>
                        </div>
                    </div>
                {% endif %}
            {% endfor %}
        </div>
    </div>
{% else %}
    <div class="container pt-6 pb-6">
        <div class="row">
            {% for service in site.services %}
            <div class="col-12 col-md-6 mb-3">
            <div class="service service-summary">
                <div class="service-content">
                <h2 class="service-title">
                    <a href="{{ service.url | relative_url }}">{{ service.title }}</a>
                </h2>
                <p>{{ service.content | markdownify | strip_html | truncate: 100 }}</p>
                </div>
            </div>
            </div>
            {% endfor %}
        </div>
    </div>
{% endif %}

13. Listing German Service Pages in The Home Page

Change the services section in the homepage from:


<div class="strip">
  <div class="container pt-6 pb-6 pb-md-10">
    <div class="row justify-content-start">
        {% assign limit = site.home.limit_services | default: 6 %}
        {% for service in site.services limit: limit %}
            <div class="col-12 col-md-4 mb-1">
                <div class="service service-summary">
                    <div class="service-content">
                        <h2 class="service-title">
                        <a href="{{ service.url | relative_url }}">{{ service.title }}</a>
                        </h2>
                        <p>{{ service.content | markdownify | strip_html | truncate: 100 }}</p>
                    </div>
                </div>
            </div>
        {% endfor %}
    </div>
    <div class="row justify-content-center">
      <div class="col-auto">
        <a class="button button-primary" href="{{ "services" | relative_url }}">View All Services</a>
      </div>
    </div>
  </div>
</div>

to:


<div class="strip">
  <div class="container pt-6 pb-6 pb-md-10">
    <div class="row justify-content-start">
        {% if page.path != page.name %}
            {% assign translatedservices = site[page.lang]  %}
            {% assign counter = 0 %}
            {% for service in translatedservices %}
                {% if service.layout == "service" %}
                    {% assign counter = counter | plus: 1 %}
                    {% if counter <= 6 %}
                        <div class="col-12 col-md-4 mb-1">
                            <div class="service service-summary">
                                <div class="service-content">
                                    <h2 class="service-title">
                                    <a href="{{ service.url | relative_url }}">{{ service.title }}</a>
                                    </h2>
                                    <p>{{ service.content | markdownify | strip_html | truncate: 100 }}</p>
                                </div>
                            </div>
                        </div>
                    {% endif %}
                {% endif %}
            {% endfor %}
        {% else %}
            {% assign limit = site.home.limit_services | default: 6 %}
            {% for service in site.services limit: limit %}
            <div class="col-12 col-md-4 mb-1">
                <div class="service service-summary">
                    <div class="service-content">
                        <h2 class="service-title">
                        <a href="{{ service.url | relative_url }}">{{ service.title }}</a>
                        </h2>
                        <p>{{ service.content | markdownify | strip_html | truncate: 100 }}</p>
                    </div>
                </div>
            </div>
            {% endfor %}
      {% endif %}
    </div>
    <div class="row justify-content-center">
      <div class="col-auto">
        <a class="button button-primary" href="{{ "services" | relative_url }}">View All Services</a>
      </div>
    </div>
  </div>
</div>

Part II: Adding A Language Switch

14. Create a list of Languages

Inside the config.yml file, add the following code:

default_lang: "en"
languages: ["en", "de"]

15. Grab a List of pages with the same ref value

Create a file named language-switch.html inside _includes folder.

Inside the file add the following code:


<div>
    {% for language in site.languages %}
        {% if language == site.default_lang %}
            {% comment %}
                For default language, check for list of pages in sites.pages
            {% endcomment %}
            {% assign langpages= site.pages | where: "ref", page.ref | sort: 'lang' %}
            {% assign langpagesteams = site.team | where: "ref", page.ref | sort: 'lang' %}
            {% assign langpagesservices= site.services | where: "ref", page.ref | sort: 'lang' %}
            {% for langpage in langpages %}
                <a href="{{ langpage.url }}">{{ language }}</a>
            {% endfor %}
            {% for langpageteam in langpagesteams %}
                <a href="{{ langpageteam.url }}">{{ language }}</a>
            {% endfor %}
            {% for langpageservice in langpagesservices %}
                <a href="{{ langpageservice.url }}">{{ language }}</a>
            {% endfor %}
        {% else %}
            {% comment %}
                For other languages, check for list of pages in sites.language collection
            {% endcomment %}
            {% assign langpages = site[language] | where: "ref", page.ref | sort: 'lang' %}
            {% for langpage in langpages %}
                <a href="{{ langpage.url }}">{{ language }}</a>
            {% endfor %}
        {% endif %}
    {% endfor %}
</div>

The generates a list links to all language with language code as link text. This language switch will only add a link to the translated page only if it is added to the list in step 14 above.

I want to add the translation links to the navbar. The best place to add them will be in main-menu.html and main-menu-mobile.html files. Use this code {% include language-switch.html %} to add the language switch anywhere in your website.

main-menu.html


<div id="main-menu" class="main-menu">
  {% assign mainmenu = site.data.menus.main | sort: 'weight'  %}
  <ul>
    {% for item in mainmenu %}
    <li class="{% if item.url == page.url %}active{% endif %}">
      <a href="{{ item.url | relative_url }}">{{ item.name }}</a>
    </li>
    {% endfor %}
    {% include language-switch.html %}
  </ul>
</div>

main-menu-mobile.html


<div id="main-menu-mobile" class="main-menu-mobile">
  {% assign mainmenu = site.data.menus.main | sort: 'weight'  %}
  <ul>
    {% for item in mainmenu %}
    <li class="{% if item.url == page.url %}active{% endif %}">
      <a href="{{ item.url | relative_url }}">{{ item.name }}</a>
    </li>
    {% endfor %}
  </ul>
  {% include language-switch.html %}
</div>

The website use data files to populate the menu, footer and socials. I will only be changing the menu on the top and the bottom. The rest will stay the same.

16. Translate the Data File

First I change the menu data file to accomodate a new language from:

main:
  - name: "Services"
    url: "/services/"
    weight: 2
  - name: "Team"
    url: "/team/"
    weight: 3
  - name: "About"
    url: "/about/"
    weight: 4
  - name: "Contact"
    url: "/contact/"
    weight: 5

footer:
  - name: "Home"
    url: "/"
    weight: 1
  - name: "Contact"
    url: "/contact/"
    weight: 2

to:

main:
  en:
    - name: "Services"
      url: "/services/"
      weight: 2
    - name: "Team"
      url: "/team/"
      weight: 3
    - name: "About"
      url: "/about/"
      weight: 4
    - name: "Contact"
      url: "/contact/"
      weight: 5
  de:
    - name: "Services DE"
      url: "/de/services/"
      weight: 2
    - name: "Team DE"
      url: "/de/team/"
      weight: 3
    - name: "About DE"
      url: "/de/about/"
      weight: 4
    - name: "Contact DE"
      url: "/de/contact/"
      weight: 5

footer:
  en:
    - name: "Home"
      url: "/"
      weight: 1
    - name: "Contact"
      url: "/contact/"
      weight: 2
  de:
    - name: "Home DE"
      url: "/"
      weight: 1
    - name: "Contact DE"
      url: "/de/contact/"
      weight: 2

17. Load data from new location

To ensure the menu load the place change the code in line 2 on files main-menu.html and main-menu-mobile.html inside _includes folder from:


{% assign mainmenu = site.data.menus.main | sort: 'weight'  %}

to:


{% assign mainmenu = site.data.menus.main[page.lang] | sort: 'weight'  %}

For the footer, change line 8 on file from:


{% assign footermenu = site.data.menus.footer | sort: 'weight'  %}

to:


{% assign footermenu = site.data.menus.footer[page.lang] | sort: 'weight'  %}

That’s it how you add a second language to a Jekyll website.

Need Help Adding a Second Language to Your Jekyll Website?

As an expert, I can help you smoothly add two or more languages on your Jekyll website.

Book a FREE Consultation Call to get started now.