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 asVerö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
andother
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.