Skip to content

How to Maintain Language Preferences on a Hugo Website

Maintaining user language preferences is a key consideration for multilingual websites. The default language can be set in the config file, but this approach does not always result in the most user-friendly experience.

Consider my own website. It's accessible in three languages, with English as the default. As a result, Dutch users always see the English version first and must switch to the Dutch version each time they visit.

A better approach would be to detect and utilize the visitor's preferred browser language. However, even this solution is not sufficient because users may wish to view the website in a language that is different from their browser settings.

The ideal scenario is for the website to remember the user's language selection, even after the browser has been closed, and present the site in that language upon the user's return.

In this article, I will walk you through how I implemented a more user-friendly solution by first using the browser's language setting and then incorporating the use of local browser storage to remember the user's language preference. I adapted and expanded on an initial solution proposed by Li Zhennan.

Step 1: Create a file alias.html in the layouts folder

First, create a new file named alias.html in the layouts folder. Paste the following code into this new file:

<!DOCTYPE html>
<html>
<head>
    <title>{{ .Permalink }}</title>
    <link rel="canonical" href="{{ .Permalink }}"/>
    <meta name="robots" content="noindex">
    <meta charset="utf-8"/>
    <noscript>
        <meta http-equiv="refresh" content="0; url={{ .Permalink }}"/>
    </noscript>
    <script>
      ;(function () {
        // Only do i18n at root, 
        // otherwise, redirect immediately
        if (window.location.pathname !== '/') {
          window.location.replace('{{ .Permalink }}')
          return
        }

        const getFirstBrowserLanguage = function () {
          let nav = window.navigator,
              browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'],
              i,
              language;

          if (Array.isArray(nav.languages)) {
            for (i = 0; i < nav.languages.length; i++) {
              language = nav.languages[i]
              if (language && language.length) {
                 return language
              }
            }
          }

          // support for other well known properties in browsers
          for (i = 0; i < browserLanguagePropertyKeys.length; i++) {
            language = nav[browserLanguagePropertyKeys[i]]
            if (language && language.length) {
              return language
            }
          }
          return 'en'
        };

        const languages = {
          'en': '/en/',
          'nl': '/nl/',
          'eo': '/eo/'
        };

        const savedLanguage = localStorage.getItem('selectedlanguage');
        // if a language has been selected earlier
        if (savedLanguage) {
          // then reselect that language
          window.location.replace(languages[savedLanguage]);

        } else {
          // check the preferred browser language
          const preferLang = getFirstBrowserLanguage().slice(0, 2);
          const url = languages[preferLang];
          // if that language is available on the website then select it
          if (url) {
            window.location.replace(url);
          } else {
            // fallback to English
            window.location.replace(languages['en']);
          }
        }
      })()
    </script>
</head>
<body>
<h1>Rerouting</h1>
<p>You should be rerouted in a jiff, if not, <a href="{{ .Permalink }}">click here</a>.</p>
</body>
</html>

Compared to the original solution by Li Zhennan, I have made the following changes:

  1. Introduced a dictionary (const languages) where you can define your website's languages.
  2. Included a check for a previously saved language selection in localStorage. If a selection exists, it is used.
  3. If localStorage has no saved selection, the browser language is used.
  4. If the browser language is not listed in the languages dictionary, the code falls back to English. You can change this if you wish to fall back on a different language.

Step 2. Modify the Theme

The second step is to make necessary changes in the theme to save the user's language preference in localStorage. This will depend on the theme you are using. As I use the Blist theme, my implementation is based on its source code. Below is a brief description of what I did, which should give you a rough idea of how to adapt your own theme.

First, find the language selection menu. In the Blist theme, this is located in the header.html file:

    {{ if .Site.IsMultiLingual }}
    {{ if ge (len .Site.Languages) 3 }}
    <li class="relative cursor-pointer">
      ...
      <div
        class="language-dropdown absolute top-full mt-2 left-0 flex-col gap-2 bg-gray-100 dark:bg-gray-900 dark:text-white z-10 hidden">
        {{ range .Site.Languages }}
        {{ if not (eq .Lang $.Site.Language.Lang) }}
        <a class="lang-{{ .Lang }} px-3 py-2 hover:bg-gray-200 dark:hover:bg-gray-700" href="/{{ .Lang }}/" lang="{{ .Lang }}">{{ default .Lang .LanguageName }}</a>
        {{ end }}
        {{ end }}
      </div>
    </li>

In the above code, I added the class lang-{{ .Lang }} to the language text. Each language menu item now has a unique class identifier like lang-en, lang-nl, etc. We can use this class to define an eventListener that will detect clicks on the language text.

The Javascript functions in the Blist theme are located in the footer.html file, but you can also add the following code at the end of the header.html file:

{{ range .Site.Languages }}
{{ if not (eq .Lang $.Site.Language.Lang) }}
<script>
    langLabel = document.querySelector('.lang-{{ .Lang }}');
    langLabel.addEventListener('click', setLang);
    function setLang() {
            localStorage.setItem('selectedlanguage', '{{ .Lang }}');
        }
</script>
{{ end }}
{{ end }}

This code adds a listener function for each language using the class names we added to the menu. Each time a language is clicked, the selection is saved in localStorage.

And that's it!