Hugo - Tutorial Part 3 - Create i18n multilingual site

i18n - Part 3 of 3 instructions for creating a Hugo multilingual website. With the following topics: The T (Translate) feature of Hugo, switching the language on the website including source code and SCSS, Hugo problems with multilingual tags, tag cloud as topic overview, tag post list including source code and SCSS, internal linking.
-
Hugo - Tutorial Part 1 - Create i18n multilingual site
Part 1 describes initial configuration steps in hugo.toml, the Page Bundle structure of the web pages, and Front Matter entries in the Markdown files.
-
Hugo - Tutorial Part 2 - Create i18n multilingual site
The second part goes into the depth of theme development and shows how to program the multilingual website. With the following topics: Entries in the html and head tag of the baseof.html and menu structure in the hugo.toml, including the navigation partials nav.html and sidepanel.html.
Post reviewed on June 7, 2023
I have checked the content of this post. The content is still up to date.
Translate strings with the i18n or T function
Another translation function? Yes and no. 99% of the texts are translated in my Markdown file index.en.md
. The remaining 1% can be translated by the i18n
or T
function.
The Hugo T
function is an alias for the Hugo i18n
function. I have chosen to use the abbreviation T = Translate
. This translation function can be very useful in templates, partials or shortcodes. So in source code.
Hugo manages translated strings similar to the .po
“portable object” files known in PHP.
The files are named after the country abbreviation with the extension yaml, toml or json. Below is an excerpt from my themes/tekki/i18n/en.toml:
..
[LeseMehr]
other = "Weiterlesen"
[naechsterBeitrag]
other = "NΓ€chster Beitrag"
[noscriptJS]
other = "Kommentare kΓΆnnen ohne JavaScript nicht angezeigt werden."
[readingTime]
one = "1 Minute Lesezeit"
other = "{{.Count}} Minuten Lesezeit"
[rssDescription]
other = "Aktuelle Inhalte auf - "
[rssDescriptionTags]
other = "Aktuelle Inhalte fΓΌr das Tag - "
..
And the same excerpt from the themes/tekki/i18n/en.toml:
..
[LeseMehr]
other = "Read more"
[naechsterBeitrag]
other = "Next post"
[noscriptJS]
other = "Comments cannot be displayed without JavaScript."
[readingTime]
one = "One minute to read"
other = "{{.Count}} minutes to read"
[rssDescription]
other = "Recent content on - "
[rssDescriptionTags]
other = "Current content for the tag - "
..
The call to the T
function in a partial then looks like this:
..
<div class="readingtime">
{{- T "readingTime" .ReadingTime -}}
</div>
..
The result looks like this in German:

And in english:

- RΓ©gis Philibert has in his blog post - Hugo Multilingual Part 2: Strings localization - explains the translation of strings very well. Therefore I save myself this.
- Hugo Documentation - Translation of Strings
- Hugo Documentation - i18n
Language switcher on the website
Since I only support 2 languages on my website, I don’t need a menu to select the language I want. When a German web page is displayed, I show the English flag. For English texts the German flag.
At first I had trouble really understanding the output of range
as a go comment in the source code shown below.
Language switcher source code
But first the sourcecode. You could make an extra partial out of it, but I didn’t. For me the sourcecode is in the partial themes/tekki/layouts/partials/header.html:
.
..
<ul class="i18n">
{{ if .IsTranslated }}
{{ range .Translations }}
{{/* .Site.Language.Lang */}}
{{ if eq .Site.Language.Lang "en" }}
{{ $imgpath := printf "%s%s" .Site.BaseURL "img/flag-great-britain.png" }}
<li><a title="English" href="{{ .RelPermalink }}"><img id="trans-flag" src={{ $imgpath }} alt="Flag Great Britain" /></a></li>
{{ else }}
{{ $imgpath := printf "%s%s" .Site.BaseURL "img/flag-germany.png" }}
<li><a title="Deutsch" href="{{ .RelPermalink }}"><img id="trans-flag" src={{ $imgpath }} alt="Flag Germany" /></a></li>
{{ end }}
{{ end }}
{{ end }}
</ul>
..
Out of habit I have included the language switcher in a ul
HTML tag. However, the list will only ever contain one list item if there are 2 languages. With
If a translation is not present, it will not be displayed in the overviews either. Not in the post list, in the displayed number of posts in the tag cloud, etc. What I mean by this - it’s not bad, the untranslated post is simply missing.
.Site.Language.Lang
of the other languages for the current web page. In my case, with 2 languages, logically only the other language is signaled. In the first moment this was illogical for me, but if you think about it a bit it can’t be done any other way.
In the following if statement I ask if the language is English. If yes, the English flag is displayed. If no, the german one.
I have saved the png-files of the flags in the directory static/img
. With printf
I tinker the link, to the respective flag, with the BaseURL and the file name together. After that follows the link to the translated web page wrapped in a li
HTML tag. No text is passed to the link but the flag as image.
So .RelPermalink
is used to access the translated page. Once you understand the logic behind it, it’s actually simple.
Language switcher SCSS
.i18n {
padding-left: 0;
margin-bottom: 0;
list-style: none;
li {
padding-left: 1.0rem;
a {
background-image: none;
}
#trans-flag {
display: flex;
height: 1.3rem;
margin-top: 0.9rem;
transition: margin-top 2.0s;
border: 1px solid var(--wk-accent-border-color);
}
}
}
With the a
HTML tag, the background-image
is turned off. This disables the animation that is otherwise triggered by the mouse cursor touch.
Hugo and multilingual tag problems
At the beginning I didn’t even notice the problems with multilingual tags. In my blog I write only about IT topics. You don’t translate English IT terms, otherwise no one will understand you. The problems appeared only when I created the tag #kurztipp
on the German side and the counterpart #quick-tip
on the English side. With this tag I wanted to mark blog posts that briefly present a topic and offer a solution to the problem.
My menu items for navigation on this website are limited to the very bare essentials. I designed the theme from the beginning so that the actual navigation can be done via a tag cloud. That is, if the visitor wants to navigate at all.
Although I specified the #quick-tip
tag on the English web pages in Front Matter, the English tag cloud outputted #kurztipp
. In the English teaser and post page as well. In the Hugo documentation you can’t find anything about this. And a Google search also yields only very, very sparse hints.
In short, Joe Mooring from the Hugo team helped me again in this case. You can find my request in the Hugo Forum here: Multilingual Tags - bypass default linking
So it is that posts find each other via the Front Matter parameter translationKey
. But the translation of tags does not work through this. How can it? How should the Hugo generator establish a connection between the tag terms kurztipp and quick-tip? The only way to do that is through a link provided by the programmer. More about this below.
Bottom line: without a connection file, Hugo Generator sets the main language article, in my case the German article with the “German” tag terms, as the top-level instance and kicks out tag terms in the connected English article that don’t match and takes the top-level tag terms.
But the problem is spreading. I recently got involved with Hugo’s RSS feeds. Lo and behold, my multilingual problems with kurztipp/quick-tip and bilder/images are back. I haven’t had time to take care of it yet. So you can trace the error on my page - RSS feed information - reproduce. First go to the German page and look at the XML Channel Title of #bilder or #kurztipp. And after that the english version.
Tags in a Hugo website
If access to information is to be offered to the searcher in a more structured way, a taxonomy is very helpful. A search engine goes one step further and deeper. It depends on what you want to achieve with a taxonomy. In my case, for a quick topic overview, a taxonomy must not be too lush. Otherwise it becomes confusing. I tend to assign too many tags myself.
For this website, I decided against categories. Tags fulfill in this case more detailed the desire for a fast topic overview.
hugo.toml Change
Hugo automatically creates taxonomies for categories and tags. If you don’t want this, you can turn it off completely or customize it in hugo.toml
as in my case:
..
[taxonomies]
tag = "tags"
..
Tags in the front matter of the web page
The tags for this web page are set in the front matter of the index.md
or index.en.md
as follows:
---
..
tags: ["hugo", "multilingual", "webdesign"]
..
---
The solution to the problem with multilingual tags
The problem described above, that tags of the main language are not translated, can be solved by providing Markdown files. These files contain only a few Front Matter specifications.
The placement of the structure in the content
directory is very important. Since tags are involved, the directory name must also be tags
.
content/
βββ tags/
βββ bilder/
βββ index.en.md
βββ index.md
βββ kurztipp/
βββ _index.en.md
βββ _index.md
For the tags to be translated, currently these are the terms bilder
and kurztipp
, a directory with the same name must be created below tags
.
The Markdown file for the main language content/tags/kurztipp/index.md,
contains the tag name in the title,
in my case in German. There is no more information in the Markdown file.
---
title: "kurztipp"
draft: false
---
The counterpart to be translated in the file content/tags/kurztipp/index.en.md
, contains the translated tag name in the title
. Additionally, the relative URL for the link to the tag post list is specified.
---
title: "quick-tip"
draft: false
url: 'tags/quick-tip'
---
In combination with these “translation files” the tag name is displayed correctly in the German and English version.
Multilingual Hugo Tag Cloud
When I was still working with the CMS WordPress, a tag cloud was always part of my websites. If the number of different tags is not too large, something like this just looks good.
On the website of Henrik Sommerfeld I found a port from the WordPress environment. Thanks for that - Henrik Sommerfeld - Hugo Tag Cloud .
List Template for the i18n Hugo Tag Cloud
Since I use the tag cloud as a central navigation element, I copied the source code into the List Template themes/tekki/layouts/taxonomy/list.html
.
{{ define "main" }}
<section class="tag-content">
<div class="container">
<h1>{{ .Title }}</h1>
<div class="tag-cloud">
<!-- Original source: https://www.henriksommerfeld.se/hugo-tag-could/ -->
{{- if gt (len .Site.Taxonomies.tags) 0 -}}
{{- $fontUnit := "rem" -}}
{{- $largestFontSize := 1.3 -}}
{{- $smallestFontSize := 1.0 -}}
{{- $fontSizeSpread := sub $largestFontSize $smallestFontSize -}}
<!--<div>Font size unit: {{ $fontUnit }}</div>
<div>Font min size: {{ $smallestFontSize }}</div>
<div>Font max size: {{ $largestFontSize }}</div>
<div>Font size spread: {{ $fontSizeSpread }}</div>-->
{{- $maxCount := 3 -}}
<!--<div>Max tag count: {{ $maxCount }}</div>-->
{{- $minCount := 1 -}}
<!--<div>Min tag count: {{ $minCount }}</div>-->
{{- $countSpread := sub $maxCount $minCount -}}
<!--<div>Tag count spread: {{ $countSpread }}</div>-->
{{- $.Scratch.Set "sizeStep" 0 -}}
{{- if gt $countSpread 0 -}}
{{- $.Scratch.Set "sizeStep" ( div $fontSizeSpread $countSpread ) -}}
{{- end -}}
{{- $sizeStep := ( $.Scratch.Get "sizeStep" ) -}}
<!-- <div>Font step: {{ $sizeStep }}</div> -->
<div class="widget">
<div class="tag-cloud-tags widget-content">
{{- $taxonomy := "tags" -}}
{{- range $term, $weightedPages := index site.Taxonomies $taxonomy -}}
{{- $t := site.GetPage (printf "/%s/%s" $taxonomy $term) -}}
{{- $currentFontSize := (add $smallestFontSize (mul (sub $weightedPages.Count $minCount) $sizeStep)) -}}
<a href="{{ $t.RelPermalink }}" aria-label="{{ $t.Title }} ({{ $weightedPages.Count }} posts)" class="tag-link" style="font-size:{{- $currentFontSize -}}rem; color:{{- partial "shuffelColor.html" . -}}; transform:rotate({{- partial "shuffelRotate.html" . -}});">{{- $t.Title -}}
<span style="font-size: {{- $smallestFontSize -}}rem"> ({{- $weightedPages.Count -}})</span></a>
{{- end -}}
</div>
</div>
{{- end -}}
</div>
</div>
</section>
{{end}}
Behind the tag name I output the number of posts for this tag name. Henrik Sommerfeld has implemented the rotate of the tag names with JavaScript. I used Hugo’s Go functions in my partial shuffleRotate.html
. In the partial shuffleColor.html
I also randomly assign one of the given colors to the tag name.
Partial shuffleColor.html for the i18n Hugo tag cloud
{{ $randomColor := shuffle (slice "#143a84" "#6f42c1" "#d63384" "#fd7e14" "#0dcaf0" "#198754" "#b56b00") }}
{{ range first 1 $randomColor }}
{{- . -}}
{{ end }
The go function slice
creates an array with 7 different color codes. shuffle
mixes the order of the color codes and in range
the first entry, from the mixed array, is returned from the partial to the calling location.
Partial shuffleRotate.html for i18n hugo tag cloud
{{ $randomRotate := shuffle (slice "-3deg" "0deg" "3deg") }}
{{ range first 1 $randomRotate }}
{{- . -}}
{{ end }}
The go function slice
creates an array with 3 different degrees. shuffle
shuffles the array and range
returns the first array entry to the calling location.
SCSS for i18n hugo tag cloud
.tag-cloud {
.tag-link {
display: inline-block;
margin-right: 1.5rem;
}
}
Very clear, but absolutely necessary. Without display: inline-block;
the rotate is not displayed.
Tag Template - all teasers of a tag
When a tag is clicked in the tag cloud, post teaser or post page, all relevant post teasers for that tag are displayed.
Tag Template - for one tag
I saved the template for the display at themes/tekki/layouts/taxonomy/tag.html
.
{{- define "main" -}}
<section class="container tag">
<h1>
<span class="tag-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tags" viewBox="0 0 16 16">
<path d="M3 2v4.586l7 7L14.586 9l-7-7H3zM2 2a1 1 0 0 1 1-1h4.586a1 1 0 0 1 .707.293l7 7a1 1 0 0 1 0 1.414l-4.586 4.586a1 1 0 0 1-1.414 0l-7-7A1 1 0 0 1 2 6.586V2z"/>
<path d="M5.5 5a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0 1a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM1 7.086a1 1 0 0 0 .293.707L8.75 15.25l-.043.043a1 1 0 0 1-1.414 0l-7-7A1 1 0 0 1 0 7.586V3a1 1 0 0 1 1-1v5.086z"/>
</svg>
</span>
#{{- .Title -}}
</h1>
<div class="tagArticle">
<p class="tagHint">{{- T "tagHint1" -}}<span>#{{- .Title -}}</span>{{- T "tagHint2" -}}</p>
</div>
</section>
{{- partial "tagTeaser.html" . -}}
{{- end -}}
The template calls with tagTeaser.html
.
Partial tagTeaser.html - calling the article teaser
The partial tagTeaser.html
needs the reference to tags and calls in a range
in turn the partial articleTeaser.html
, which outputs the actual teaser.
{{- range .Paginator.Pages.ByLastmod.Reverse -}}
{{- partial "articleTeaser.html" . -}}
{{- end -}}
{{- template "_internal/pagination.html" . -}}
This second step is necessary because I use the partial articelTeaser.html
not only for tags but also for posts. I don’t explain articelTeaser.html
in this series, because no multilingual specifics are processed there.
SCSS for the tag template
.tag {
margin-bottom: 2.0rem;
.tagHint {
span {
font-size: 1.2rem;
font-weight: 600;
margin: 0 0.5rem;
}
}
}
Internal linking of i18n web pages
I illustrate the internal linking of web pages with a constructed example. The variable $url
is assigned the URL of part 1 of this series. Then a a
HTML tag is called with this variable.
Link of the main language - in my case in german
$url := "/hugo-i18n-howto-teil-1/"
<a href={{ $url }}>Hugo - Anleitung Teil 1 - i18n multilinguale Site erstellen</a>
The result is the following link: Hugo - Anleitung Teil 1 - i18n multilinguale Site erstellen
To make it work not only locally, in the hugo.toml
you need to specify the baseURL
parameter with your domain name. My entry looks like this:
baseURL = "https://tekki-tipps.de/"
Link of the translated language - in my case in english
$url := "/en/hugo-i18n-howto-part-1/"
<a href={{ $url }}>Hugo - Tutorial Part 1 - Create i18n multilingual site</a>
The result is the following link: Hugo - Tutorial Part 1 - Create i18n multilingual site
Conclusion
A multilingual website takes work. But it increases the reach enormously. This is the end of this series. I hope that this information is helpful to you. If so, a comment would be nice.
Link list for this article
- Hugo Multilingual Part 2: Strings localization
- Hugo Documentation - Translation of Strings
- Hugo Documentation - i18n
- Hugo Documentation - List All Available Languages
- Hugo Documentation - Taxonomies
- Hugo Documentation - Taxonomy Templates
- Hugo Documentation - Example: Removing default taxonomies
- Henrik Sommerfeld - Hugo Tag Cloud
- Hugo Documentation - slice
- Hugo Documentation - shuffle
You might also be interested in
- Hugo - Tutorial Part 1 - Create i18n multilingual site
Part 1 describes initial configuration steps in hugo.toml, the Page Bundle structure of the web pages, and Front Matter entries in the Markdown files.
- Hugo - Tutorial Part 2 - Create i18n multilingual site
The second part goes into the depth of theme development and shows how to program the multilingual website. With the following topics: Entries in the html and head tag of the baseof.html and menu structure in the hugo.toml, including the navigation partials nav.html and sidepanel.html.
- Use free Bootstrap SVG Icons in Hugo
Integrate SVG icons with HTML and CSS/SCSS in Hugo
With the German language setting, comments are not displayed in the English version of the website and vice versa.