Γ—
Home RSS feed info
i18n - Part 3 of 3 sequential tutorials for creating a Hugo multilingual website.

Hugo - Tutorial Part 3 - Create i18n multilingual site

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.

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:

Readingtime in german
Readingtime in german

And in english:

Readingtime in englisch
ReadingTime in englisch

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 .Translations }}. I had expected the language of the current web page to be output. It isn’t. I left the output of the language abbreviation in the 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 .IsTranslated }} it is queried if a translation exists for the current web page. If not, no flag will be displayed and the icons to the left of the flag will move to the far right.

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.

{{ range .Translations }} provides in each loop, among other things, the .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 {{ range .Translations }} passes all the data of the translated web page in each loop. Therefore, .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 {{ partial "tagTeaser.html" . }} the partial 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.

$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/"
$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

You might also be interested in

Update:  |
15 minutes to read
0
This post was created with Hugo version 0.115.2.

With the German language setting, comments are not displayed in the English version of the website and vice versa.

© 2023 - Frank Kunert  -  About me