Adding languages to a Hugo site

illustrations illustrations illustrations illustrations illustrations illustrations illustrations
post-thumb

Published on 16 March 2023 by Andrew Owen (4 minutes)

I’m a long-term advocate for localization, but this site has been monolingual for over a year now. It’s past time I started following my own advice. So last weekend I finally got around to localizing the site for French.

As with most localization tasks, I’m retrofitting it to an existing project. Fortunately, I chose Hugo as my static site generator, and it has built-in internationalization support using go-i18n. On the downside, I chose a theme that doesn’t fully support localization, which meant there was some work involved. But not as much as I’d expected. So let me take you through the steps involved.

First, I updated my config.toml file to tell Hugo that my site is now multilingual:

defaultContentLanguageInSubdir = false
DefaultContentLanguage = "en-us"
[languages]
  [languages.en-us]
    title = "Andrew Owen | Writer | Designer"
    languageName = "English"
    weight = 1
  [languages.fr]
    title = "Andrew Owen | Écrivain | Concepteur"
    languageName = "Français"
    weight = 2

Setting defaultContentLanguageInSubdir leaves the language out of the URL for the default language, in my case US English. Any settings you leave out from the language definitions will fall back to the default.

My 404.md page is already stored in a folder called en. So adding a French version was just a case of creating a new folder called fr, copying the file across and changing the text.

The next step was to set up some a set of translatable phrases for the generated content. This is as simple as creating an i18n folder in the root of the Hugo repository and adding a YAML or TOML file for each language. In my case en-us.yaml and fr.yaml. These phrases are included using the shortcode {{T "phrase" | formatting}}. If you want to include HTML such as <br> tags, then use safeHTML for the formatting value. Each value should have an ID and a definition in each language file. For example:

- id: January
  translation: "janvier"

In several places in the site, titles were derived from data contained in YAML files. In each of these cases, I replaced the data-derived version with a shortcode to the translated version instead. I also had to modify some of the links to go to the correct place when viewing the site in French. In most cases, that just meant using the .Site.Language.Lang value.

Because I’m only supporting two languages, I wanted to have a simple toggle. The nav bar was starting to get rather full, so I removed the Home link, because the logo already takes you there. I had to change the RSS link to make it work in French, so I also replaced it with the icon. I figure anyone who still cares about RSS should recognize it:

<li class="nav-item">
  {{ if eq .Site.Language.Lang "en-us" }}
  <a class="nav-link" href="/blog/index.xml"><i class='fa fa-rss'></i></a>
  {{ else }}
  <a class="nav-link" href="/fr/blog/index.xml"><i class='fa fa-rss'></i></a>
  {{ end }}
</li>

The last thing to do was to translate the individual articles. Hugo gives you two ways of doing this. You can set up a folder for each language, and if you have three or more languages, you should do this. But by default, Hugo will treat anything ending in .md as the default language, and you can add other languages by adding the language code to the extension, for example: .fr.md. If you’re using folders, then you add the folder details to the language definitions in your TOML file. For example:

[languages]
  [languages.en]
    contentDir = "content/english"

While I was editing the site in VScode, I also took the opportunity to reorganize the images. They were getting a bit tricky to navigate with TinaCMS, so I moved the blog images into folders by year. I also did some cleanup on the top nav bar. The last thing to do was change the article date string from {{ .PublishDate.Format “2 January 2006” }} to {{.Date.Day}} {{i18n .Date.Month}} {{.Date.Year}} after setting up a set of translatable date strings in the YAML files in the i18n folder.

Of course, just when I thought I’d caught everything, I noticed that tag links were broken. I fixed this by making the URL relative so that it would pick up the current language. I’m pleased to say that I didn’t have to do anything at all to localize search.

With the technical piece done, all that’s left to do is the translation. There are ways to automate the process, but there’s still no substitute for a human reviewer. I’m using a combination of DeepL, LanguageTool and my own modest language ability. To begin with, I’m only translating the core content. But eventually I intend to have a fully translated site. If you’re a native French user, and you spot any egregious mistakes, please drop me an email.