Right, let’s get your hugo.toml (or config.toml for you old-timers) ready for its new life as a polyglot. This isn’t just flipping a switch; it’s about telling Hugo, “Hey, buddy, we’re not just doing one language anymore, so get your act together.” We’ll do this step-by-step, and I’ll explain the why behind the weirdness because some of these configuration options look like they were chosen by a random word generator.

First, the main event: defining your languages. You do this in a [languages] TOML table. The key for each sub-table is the language code, and inside that sub-table, you define its characteristics. The most important one is languageName, which should be the name of the language in that language (e.g., “Español” for Spanish, not “Spanish”). This isn’t just pedantry; it’s what Hugo will use to generate language switches and other i18n features.

[languages]
[languages.en]
languageName = "English"
weight = 1 # The default language gets a weight of 1

[languages.es]
languageName = "Español"
weight = 2

See that weight parameter? It’s crucial. It doesn’t affect the sort order of your languages in a dropdown (that’s alphabetical by language code, because why would it be intuitive?). No, weight is used to determine the default language when using ugly URLs (more on that nightmare later). The language with weight 1 is the default. So get that right.

The Non-Negotiable Basics: Title and Params

Each language needs its own title for the site. You’ll also want to set language-specific parameters here. This is where you’d put things like the localised name for your “Read More” button or a language-specific description for SEO. Don’t just dump everything at the top level; Hugo’s i18n system expects this structure.

[languages.en]
languageName = "English"
weight = 1
title = "My Brilliant Site"
[languages.en.params]
description = "A site about things and stuff"
read_more = "Read more..."

[languages.es]
languageName = "Español"
weight = 2
title = "Mi Sitio Brillante"
[languages.es.params]
description = "Un sitio sobre cosas y otras cosas"
read_more = "Leer más..."

Now you can use these in your templates with .Site.Params.read_more and it will automatically pull the correct value for the current language. Magic.

The Menu Conundrum

Menus are where this gets… interesting. You have two choices, and both are mildly annoying. You can define a separate menu for each language, which is clean but verbose. Or you can use Hugo’s i18n menus, which is clever but can make your config file look like an inscrutable spellbook.

Method 1: The “I Value My Sanity” Separate Menu Approach

[[languages.en.menu.main]]
name = "Home"
url = "/"
weight = 1

[[languages.en.menu.main]]
name = "About"
url = "/about/"
weight = 2

[[languages.es.menu.main]]
name = "Inicio"
url = "/"
weight = 1

[[languages.es.menu.main]]
name = "Acerca de"
url = "/about/"
weight = 2

It’s repetitive, but it’s crystal clear. For a site with a few menu items, this is my preferred method. You know exactly what’s going on.

Method 2: The “I’m Feeling Clever” i18n Approach

This method uses the i18n translation files. First, you define your menu in the default language only, using a translation key.

# In your default language section only
[[languages.en.menu.main]]
name = ":i18n:nav.home"
url = "/"
weight = 1
identifier = "home"

[[languages.en.menu.main]]
name = ":i18n:nav.about"
url = "/about/"
weight = 2
identifier = "about"

Then, in your i18n/en.yaml and i18n/es.yaml files, you define the actual text.

# i18n/en.yaml
nav:
  home: "Home"
  about: "About"

# i18n/es.yaml
nav:
  home: "Inicio"
  about: "Acerca de"

The identifier is the magic glue that ties the menu item across languages together. This is more scalable for large menus but adds a layer of indirection that can be confusing to debug. Choose your poison.

The Ugly Truth About Ugly URLs

Let’s talk about content directories and URLs. By default, Hugo will put all your English content in content/ and Spanish content in content.es/. The Spanish about.md will live at mysite.com/es/about. This is clean. This is good.

But if you set uglyURLs = true in your config, you enter a world of pain. The default language will output to /about.html, but the Spanish version will output to /es/about.html. See the problem? The file extensions are inconsistent. It’s a mess. My advice? Never, ever use uglyURLs = true on a multilingual site. It’s just not worth the headache. Hugo’s pretty URLs are one of its best features; use them.

The final, critical piece is setting the defaultContentLanguage. This should match the key of your default language (the one with weight = 1). This tells Hugo which language to use when it finds content that isn’t explicitly in a language content dir.

defaultContentLanguage = "en"

And there you have it. Your hugo.toml is now a passport to a multilingual world. It seems like a lot of boilerplate, and it is, but once it’s set up, it works like a charm. Just remember: test your menus, test your links, and for the love of all that is holy, avoid ugly URLs.