Multilingual Content With Hugo

Handling Multilingual Content is where Hugo really shines. However, since internationalization (i18n for short) is a pretty complex task, let’s throw together a brief list of

Requirements

  • The content translations should be added in an orderly manner.
  • The content translations should be directly linkable to each other.
  • Templates should not contain any hard-coded language-specific text. Instead, translations for such text should be defined separately and loaded according to the language of the content.
  • The inclusion of bundled page resources must be configured in one place only, unless there is need to do otherwise. In other words, if the same picture should be included in all five translations of the same page, this should be configured in one rather than five places.
  • Should a bundled resource have some multilingual dependencies of its own (example: a photo and text captions in five languages), such dependencies must be bundled with the resource (captions with the photo in this example), rather than with the main page of the bundle.

Adding a Translation of a Content Unit

This is the simplest part. Since we have configured two languages to be used at our site , we can add the Russian translation of ‘The Walrus and the Carpenter’ now by creating a new content file (notice ru in the file name):

hugo new poetry/lewis-carroll/the-walrus-and-the-carpenter/index.ru.md

… and adding the content:

---
title: "Морж и Плотник"
date: 2020-05-10T11:51:48+02:00
draft: true
description: TBD
slug: "морж-и-плотник"
---
# Морж и Плотник

Сияло солнце в небесах,
Светило во всю мочь,
Была светла морская гладь,
Как зеркало точь-в-точь,
Что очень странно — ведь тогда
Была глухая ночь.

If you now point your browser to http://localhost:1313/ru/poetry/lewis-carroll/морж-и-плотник/ , you will see a Russian translation of the immortal rhyme.

Notice the custom slug parameter, that allows to make each translation’s URL language-specific, even in a different alphabet. This should be helpful for search engine optimization of your site.

Linking the Translations

How many a time did you click on a ‘language’ link, hoping to get the translation of the page, only to end up on the website’s front page instead? With Hugo, this kind of hiccups can be eliminated once and for all.

Let’s create another partial in out theme, themes/comic/layouts/partials/translations.html:

{{if .IsTranslated}}
<nav class="translations">
    {{range .Translations}}
    <a href="{{.Permalink}}">
        {{.Site.Language.LanguageName}}
    </a>
    {{end}}
</nav>
{{end}}

and include it into our themes/comic/layouts/_default/baseof.html:

...
<body>
    {{partial "breadcrumbs.html" .}}
    <div class="container">
        {{partial "translations.html" .}}
        <main>
            {{block "main" .}}
            {{end}}
        </main>
    </div>
</body>
...

The {{.Translations}} variable contains a list of translations for this particular content (except the current language), each having the usual page properties, like .Permalink. The .Site.Language.LanguageName refers to the name of a particular language as configured in config/languages.yaml. Now, each page on our site has links to its translations, if there are any. {{if .IsTranslated}} checks if the page has translations, and if not, the entire <nav> element is skipped.

Let’s add another poem, this time without translation:

hugo new poetry/lewis-carroll/mad-gardeners-song/index.md

with this content:

---
title: "Mad Gardeners Song"
date: 2020-05-11T18:58:10+02:00
draft: true
description: TBD
---

# The Mad Gardener’s Song

He thought he saw an Elephant  
That practised on a fife:  
He looked again, and found it was  
A letter from his wife.  
"At length I realise," he said,  
"The bitterness of Life!"  

If you look it up in a browser, no translation link would appear on the page.

That was simple enough, as our template didn’t contain any language-specific text strings. But what if we wanted to display some text next to our translation links, like saying “Other languages:” in English and “Другие языки:” in Russian?

Translating Strings in Templates

Create an i18n directory in the root of your project:

mkdir i18n

In this directory, let’s create YAML files with translations, i18n/en.yaml and i18n/ru.yaml.

i18n/en.yaml:

- id: other_lang
  translation: "Other languages:"

i18n/ru.yaml:

- id: other_lang
  translation: "Другие языки:"

Let’s improve our translations.html partial by adding an HTML header with {{i18n "other_lang"}} as its text:

{{if .IsTranslated}}
<nav class="translations">
    <h1 class="languages">
        {{i18n "other_lang"}}
    </h1>
    {{range .Translations}}
    <a href="{{.Permalink}}">
        {{.Site.Language.LanguageName}}
    </a>
    {{end}}
</nav>
{{end}}

Our translation should now be visible: i18n function takes care of that. It finds a translation file whose name matches the configured language code, and looks up a translation for other_lang in that file.

In my experience, Hugo server doesn’t always pick the changes in translations, so you might need to restart it.

Sometimes, you need to pass a numeric parameter to the translation. For instance, to say: ‘There are 7 pages in this category’. Adding the magic dot after the string identifier {{i18n "ship_count" .}} makes the entire page context available in the string translation. Just refer to page variables as you would from a template.

This brings us to a daunting task of handling plurals. Adding ’s’ at the end of an English noun doesn’t always work (think of mouse – mice or goose – geese), but if you thought it’s the worst of your problems, let’s have some

Fun With Russian Plurals

While goose – geese is an exception in English, in Russian having a singular and up to three plural forms for a noun is normal. Some nouns have only singular form, while others exist only in plural. This makes Russian probably the most tricky use case for handling plurals, and thus a perfect example for a tutorial. In this example, we shall be counting ships.

Let’s update our i18n file for Russian:

- id: other_lang
  translation: "Другие языки:"

- id: ship
  translation:
    zero: ни одного корабля
    one: корабль
    few: корабля
    many: кораблей
    other: корабли

- id: from_page_context
  translation: "Количество: {{.Params.count}}"

Our translation for ship has got quite bushy now, as we have added all plural forms that go-i18n library supports as of this writing. Hugo will pick the correct variant for the number that you either pass to i18n function directly, like in {{i18n "ship" 2}}, or refer to from a translation, like for from_page_context string in the example above.

Not all of the plural variants are supported for each language, as we shall see shortly. Most of the time it makes sense, too. The English version would contain only one and other keys, the latter being a sort of catch-all variant.

This works for a single numeric parameter. There is no solution for handling ‘1 ship and 3 geese’ in one string. Should things get too tricky, you can resort to a less natural form, that allows you assign the translation to translation key directly, dropping plural variants altogether. We did so for from_page_context string (“Количество” means ‘Quantity’). Here, we pass a custom count parameter defined in the front matter part of a content file. Speaking of which, let’s create one:

hugo new numerals/ships.ru.md

In the front matter, let’s add a custom parameter count. We do not need any actual content for our little experiment:

---
title: "Ships"
date: 2020-05-11T19:50:44+02:00
draft: true
description: TBD
count: 16
---

Hugo provides for defining your own front matter parameters. They can be accessed from templates and translations using expression {{.Params.<parameter name>}}.

The layouts directory provides the convenience of overriding default templates we created in our theme. In this case, we need to create a custom single.html template for our numerals section. Create a directory with the same name under layouts

mkdir layouts/numerals/

and add layouts/numerals/single.html with the following content:

<ul>
    <li>
        0: {{i18n "ship" 0}}
    </li>
    <li>
        1: {{i18n "ship" 1}}
    </li>
    <li>
        2: {{i18n "ship" 2}}
    </li>
    <li>
        5: {{i18n "ship" 5}}
    </li>
    <li>
        11: {{i18n "ship" 11}}
    </li>
    <li>
        23: {{i18n "ship" 23}}
    </li>
    <li>
        101171: {{i18n "ship" 101171}}
    </li>
</ul>
<p>
    {{i18n "from_page_context" .}}
</p>

Now, if you point your browser to http://localhost:1313/ru/numerals/ships/ , you will see Russian plurals in action:

0: кораблей
1: корабль
2: корабля
5: кораблей
11: кораблей
23: корабля
101171: корабль

Notice that zero variant is not supported, and so there is no way to say neither ‘no ship’ nor ‘no ships’, neither in English nor in Russian.

Markdown Files as Page Bundle Resources

Should you include Markdown files as page resources, Hugo will look for a Markdown file for a particular language, and will not fall back to the default language version, if such file is missing. This is sane and useful feature, but sometimes you might want to include some language-agnostic Markdown. In such case, give that file a different suffix – .txt seems reasonable enough – and in the template pipe it through markdownify function, like this:

{{.Content | markdownify}}

This way, you can both have a cake, and eat it, too!

Built with Errorist theme for Hugo site generator.