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!