How I Localized My Hugo Theme

How I Localized My Hugo Theme

· 6 minutes reading time Development go hugo

When I first decided to localize my Hugo theme, I thought, How hard can it be? But as I dug deeper, I realized there were more nuances than I had expected, especially with date formatting and pluralization. After some trial and error and online research, I found effective solutions. I share them here to help you avoid similar obstacles.

Extracting Strings for Localization

The first step was to identify and extract all user-facing text into language files. Hardcoding strings, as shown in the example below, makes the theme inflexible:

<p>Published on {{ .Date.Format "January 2, 2006" }}</p>

I replaced the hardcoded Published on with a call to Hugo’s i18n function, which retrieves translations from language files based on the active language setting:

<p>{{ i18n "published_on" }} {{ .Date.Format "January 2, 2006" }}</p>

To manage translations and get started, I set up an i18n directory and added en.yaml as the first language file:

# Date Format
- date_format: "January 02, 2006"

# Single Post
- published_on: "Published on"
- reading_time: "reading time"
- minutes: "minutes"

For German, I created de.yaml:

# Date Format
- date_format: "2. January 2006"

# Single Post
- published_on: "Veröffentlicht am"
- reading_time: "Lesezeit"
- minutes: "Minuten"

This structure kept the translations organized and easy to manage.

You should extract all hardcoded strings from your theme’s template files and store/translate them in the language files. Key elements to extract strings from include:

  • Navigation labels
  • Buttons
  • Dates and times
  • Informational messages
  • Error messages
  • Tooltips and alt text for images
  • Forms and placeholders

In the language files, use descriptive keys like post_comment_button instead of vague ones like button, to keep translations clear and maintainable. If it improves clarity, add a comment.

Formatting Dates in a Localized Way

Formatting dates for localization proved trickier than expected. I initially tried:

<p>{{ i18n "published_on" }} {{ .Date.Local.Format (i18n "date_format") }}</p>

But this didn’t work as intended. Month names remained in English, even with the defaultContentLanguage set to German. This is because .Date.Local.Format only applies the format string without translating month names or other language-specific elements, making it unsuitable for full localization.

At first, I couldn’t figure out why my dates still showed January or March instead of Januar or März. Because I didn’t know better, I tried to use the German word for January:

- date_format: "2. Januar 2006"

This didn’t yield the expected results either. Every month became Januar now. After some reading, I found that the correct approach was to use Hugo’s dateFormat function:

<p>{{ i18n "published_on" }} {{ dateFormat (i18n "date_format") .Date }}</p>

Turns out, Hugo expects the English month names in the date_format key to handle automatic translation. A small but important detail.

That is why the date_format entry in de.yaml must use the English month name January because the Go programming language and Hugo rely on this name to handle date formatting and automatic translation to local equivalents.

- date_format: "2. January 2006"  # English month name for automatic translation

If you want to know exactly how to format a date works, look at the comment in the Go source file src/time/format.go:

// Here is a summary of the components of a layout string. Each element shows by
// example the formatting of an element of the reference time. Only these values
// are recognized. Text in the layout string that is not recognized as part of
// the reference time is echoed verbatim during Format and expected to appear
// verbatim in the input to Parse.
//
//	Year: "2006" "06"
//	Month: "Jan" "January" "01" "1"
//	Day of the week: "Mon" "Monday"
//	Day of the month: "2" "_2" "02"
//	Day of the year: "__2" "002"

Handling Singular and Plural Forms

Pluralization is often overlooked but crucial for localization. Handling singular and plural forms ensures text like 1 minute or 5 minutes reads naturally in each language, accommodating linguistic rules. For example, in Polish, 1 minute translates to 1 minuta, while 2 minutes becomes 2 minuty, and 5 minutes is 5 minut.

For text that varies based on numbers, like reading time, I initially used this template:

<p>{{ .ReadingTime }} {{ i18n "minutes" }}</p>

However, this approach failed to handle singular and plural cases. To address this, I defined separate one (singular) and other (plural) forms in the language files, allowing Hugo to dynamically select the correct form based on the value.

For en.yaml:

- minute:
    one: "minute"  # Singular form
    other: "minutes"  # Plural form

For de.yaml:

- minute:
    one: "Minute"
    other: "Minuten"

Like Polish, certain languages have additional plural forms like zero or few. Refer to the Unicode CLDR rules for details.

In the template, I updated the logic to:

<!-- Dynamically selects singular or plural form based on ReadingTime -->
<p>{{ .ReadingTime }} {{ i18n "minute" .ReadingTime }}</p>

This approach worked perfectly. Hugo automatically chose the correct form based on the value of .ReadingTime.

Testing the Localization

To ensure the localization worked as expected, I thoroughly tested the implementation by switching between languages and verifying edge cases. This process involved checking strings, dates, and plural forms in various scenarios.

Setting Up the Default Language

I set the defaultContentLanguage to German in the config.toml file:

defaultContentLanguage = "de"  # Set the default language to German

Testing Translated Strings

Running hugo server, I verified that all user-facing strings were translated correctly. For example:

  • The text Published on displayed as Veröffentlicht am when German was selected.
  • Navigation labels, buttons, and informational messages all reflected the appropriate translations.

Validating Date Localization

Dates were displayed with correctly localized month names, such as Januar and März, adhering to the selected language. Using dateFormat with the date_format key ensured month names were automatically translated.

Testing Pluralization

I tested reading times across different values to ensure the correct singular or plural forms were applied:

  • 1 Minute for singular.
  • 5 Minuten for plural. For complex languages like Polish or Arabic, I simulated values such as 0, 1, 2, 5, and 10 to confirm that the appropriate plural forms (zero, one, other, etc.) were selected based on the rules.

Edge Cases

I tested edge cases such as:

  • 0 minutes to ensure proper handling of zero plural forms.
  • Unexpected values like -1 or 1000 to verify the robustness of the implementation.

These checks confirmed that all user-facing elements adhered to the selected language and its cultural conventions.

Lessons Learned

Looking back, these things made it possible that the theme can adapt to different languages:

  • Extract user-facing strings into language files using the theme’s i18n directory.
  • Use dateFormat for properly localized dates and month names.
  • Handle singular and plural forms with one and other keys in language files.

Despite some trial and error, localization now makes my Hugo theme more adaptable and easier to tailor to different languages and users.