Files
chill.social/themes/chill-theme/docs/INTERNATIONALIZATION.md
2026-01-28 09:40:37 +01:00

22 KiB

Internationalization (i18n) Guide

Complete guide for implementing multilingual support in Hugo Saasify Theme.

Table of Contents

Overview

Hugo Saasify Theme provides full multilingual support with:

  • Automatic language switcher in the header
  • Persistent language preference (remembers user's choice)
  • 40+ translation keys for UI elements
  • Language-specific content directories
  • Language-specific menus and parameters
  • Date format localization
  • Translated blog sidebar, post navigation, and more

Quick Start

The theme comes with three fully implemented language examples in the demo site:

  • English (en) - Default language
  • Chinese (zh-cn) - Complete translation
  • German (de) - Complete translation

You can use these as references when adding your own languages.

Basic Setup

  1. Configure languages in hugo.toml:
defaultContentLanguage = "en"

[languages]
  [languages.en]
    languageCode = "en-us"
    languageName = "English"
    title = "Your Site"
    weight = 1
    contentDir = "content"

  [languages.zh-cn]
    languageCode = "zh-cn"
    languageName = "简体中文"
    title = "Your Site"
    weight = 2
    contentDir = "content/zh-cn"

Tip: Check the demo site's hugo.toml for complete English, Chinese, and German configurations you can copy and adapt.

  1. Copy translation files:
# Copy English translations (already included in theme)
cp themes/chill-theme/i18n/en.toml i18n/en.toml

# Copy Chinese example and rename
cp themes/chill-theme/docs/zh-cn.toml.example i18n/zh-cn.toml
  1. Create language-specific content:
content/
├── _index.md              # English homepage
├── blog/
│   └── my-post.md         # English blog post
└── zh-cn/
    ├── _index.md          # Chinese homepage
    └── blog/
        └── my-post.md     # Chinese blog post
  1. Configure language-specific menus:
# English menu
[[languages.en.menu.main]]
  name = "Blog"
  url = "/blog"
  weight = 1

# Chinese menu
[[languages.zh-cn.menu.main]]
  name = "博客"
  url = "/zh-cn/blog"
  weight = 1

The language switcher will automatically appear in the header!

Configuration

Language Preference Persistence

New Feature: The theme now automatically remembers the user's language preference using browser localStorage. This provides a seamless multilingual experience.

How It Works

  1. First Visit: User sees the default language (no redirect)
  2. User Selects Language: When clicking a language in the switcher:
    • Their preference is saved to localStorage
    • They navigate to the selected language
  3. Return Visits: On subsequent page loads:
    • The system checks for a saved preference
    • If found, automatically redirects to their preferred language
    • This happens once per session to avoid interfering with manual navigation

User Experience Examples

Scenario 1: New User

1. User lands on https://example.com/ (English, default)
2. Clicks "简体中文" in language switcher
3. Navigates to https://example.com/zh-cn/
4. Preference saved: zh-cn
5. Next visit: automatically redirects to Chinese version

Scenario 2: Deep Links

1. User has preference: zh-cn
2. Clicks link to https://example.com/blog/my-post
3. Auto-redirects to https://example.com/zh-cn/blog/my-post
4. Original path preserved, just language changed

Scenario 3: Manual Navigation

1. User has preference: zh-cn (currently on Chinese site)
2. Types https://example.com/en/contact directly in address bar
3. Sees English contact page (no forced redirect)
4. Can still use language switcher to change languages

Technical Details

  • Storage: Uses browser localStorage with key hugo_preferred_language
  • Fallback: Works gracefully without JavaScript (standard links still function)
  • Privacy: Stored locally in user's browser only
  • Session Safety: Uses sessionStorage flag to prevent redirect loops
  • Progressive Enhancement: Site works perfectly with JavaScript disabled

Disabling the Feature

If you want to disable language preference persistence:

  1. Remove the script include from layouts/_default/baseof.html:
<!-- Remove or comment out these lines: -->
{{ if .Site.IsMultiLingual }}
<script src="{{ "js/language-preference.js" | relURL }}"></script>
{{ end }}
  1. Or delete the file: static/js/language-preference.js

The language switcher will still work; it just won't remember the user's choice.

Automatic Language Detection with VisitorAPI

New Feature: The theme supports automatic language detection using VisitorAPI to detect the visitor's language based on their location.

How It Works
  1. Every Page Load (No Stored Preference): If the user has no stored language preference:
    • The system calls VisitorAPI on every page load to detect the visitor's language
    • If a matching language is available, the user is automatically redirected
    • The detected language is stored as their preference for future visits
  2. After Preference is Stored: Once a language preference is saved (either from auto-detection or manual selection):
    • Uses the stored preference (no API call)
    • Fast redirect based on localStorage
  3. Manual Selection: User can always override by selecting a different language
Setup Instructions
  1. Sign up for VisitorAPI:

    • Visit visitorapi.com and create a free account
    • Create a new project to get your Project ID
  2. Add the Project ID to your configuration:

[params]
  # VisitorAPI Project ID for automatic language detection
  # Set this to enable auto-detection of visitor's language
  # If not set, language detection will be disabled
  visitorapi_pid = "YOUR_PROJECT_ID_HERE"
  1. That's it! The feature is automatically enabled when the Project ID is configured.
How Language Matching Works

The system uses dynamic language matching based on the first two characters of language codes. This means it automatically works for any language you add to Hugo without requiring any code changes.

Matching Logic:

  • VisitorAPI returns language codes like "en", "zh", "de", "fr", "es", "pt", etc.
  • The system extracts the first 2 characters from the VisitorAPI response
  • It compares these with the first 2 characters of your Hugo language codes
  • When they match, the user is redirected to that language

Examples:

  • VisitorAPI returns "en" → Matches Hugo language "en"
  • VisitorAPI returns "zh" or "zho" → Matches Hugo language "zh-cn" ✓ (both start with "zh")
  • VisitorAPI returns "de" or "deu" → Matches Hugo language "de"
  • VisitorAPI returns "pt" or "por" → Matches Hugo language "pt" or "pt-br"
  • VisitorAPI returns "es" or "spa" → Matches Hugo language "es"
  • VisitorAPI returns "fr" or "fra" → Matches Hugo language "fr" or "fr-ca"

Adding a New Language:

When you add a new language to your Hugo site, VisitorAPI auto-detection will automatically work for it:

  1. Add the language to your hugo.toml:

    [languages.ja]
      languageCode = "ja-jp"
      languageName = "日本語"
      # ... rest of config
    
  2. That's it! The system will automatically detect visitors from Japan and redirect them to /ja/ because:

    • VisitorAPI will return "ja" or "jpn" for Japanese visitors
    • The first 2 characters ("ja") match your Hugo language code "ja"
    • No code changes needed

Technical Details:

The matching happens in language-preference.js:

// Extract first 2 characters from VisitorAPI response (e.g., "zh" from "zh" or "zho")
var langPrefix = visitorLang.substring(0, 2);

// Get all available Hugo languages from the language switcher
var languageLinks = document.querySelectorAll('.language-switch-link');

// Compare first 2 characters with each Hugo language
for (var j = 0; j < languageLinks.length; j++) {
    var hugoLang = languageLinks[j].getAttribute('data-lang');
    var hugoPrefix = hugoLang.substring(0, 2);

    if (langPrefix === hugoPrefix) {
        // Match found! Redirect to this language
        detectedLang = hugoLang;
        break;
    }
}

This approach ensures:

  • No hardcoded language mappings to maintain
  • Automatic support for any language you add
  • Works with both 2-character codes ("en") and extended codes ("zh-cn")
  • Handles VisitorAPI's various language code formats (ISO 639-1 and ISO 639-2)
Privacy & Performance
  • API Call: Called on every page load when no language preference is stored
  • Caching: Once a language is detected and stored, no more API calls are made
  • Efficient: After first detection, uses localStorage for instant redirects
  • Fallback: Works gracefully if API is unavailable
  • No Tracking: VisitorAPI detects language, doesn't track user behavior
  • User Control: Users can clear localStorage to reset and re-detect
Disabling Auto-Detection

To disable automatic language detection while keeping manual preference persistence:

  1. Simply remove or comment out the visitorapi_pid parameter from your hugo.toml
  2. Or set it to an empty string: visitorapi_pid = ""

The language preference system will continue to work; it just won't auto-detect on first visit.

Basic Setup

# Set the default language
defaultContentLanguage = "en"

# Whether to include default language in URL path
# false: English at / (example.com/)
# true: English at /en/ (example.com/en/)
defaultContentLanguageInSubdir = false

Language-Specific Parameters

Each language can have its own configuration for CTAs, headers, etc:

[languages.en.params.cta]
  enable = true
  title = "Ready to Get Started?"
  description = "Join companies using our platform"
  [languages.en.params.cta.primary_button]
    text = "Get Started Free"
    url = "/get-started"

[languages.zh-cn.params.cta]
  enable = true
  title = "准备好开始了吗?"
  description = "加入使用我们平台的公司"
  [languages.zh-cn.params.cta.primary_button]
    text = "免费开始"
    url = "/zh-cn/get-started"

Sidebar Configuration

Important: Do NOT set title/description/text in global sidebar config, as this overrides i18n translations:

# ❌ DON'T: This prevents translation
[params.blog.sidebar.recent]
  enable = true
  title = "Recent Articles"  # This overrides i18n

# ✅ DO: Let i18n handle the translations
[params.blog.sidebar.recent]
  enable = true
  count = 5  # Only set non-text configuration

Translation Keys

Complete List

The theme includes 40+ translation keys. Here are the most important ones:

Blog & Content

  • readMore - "Read More" link text
  • readTime - Reading time suffix
  • categories - Categories heading
  • tags - Tags heading

Blog Post

  • tableOfContents - TOC heading
  • minRead - Reading time text
  • previousPost - Previous navigation
  • nextPost - Next navigation
  • dateFormat - Date format string

Blog Sidebar

  • recentArticles - Recent articles heading
  • popularTags - Popular tags heading
  • subscribeNewsletter - Newsletter heading
  • subscribeDescription - Newsletter description
  • emailPlaceholder - Email input placeholder
  • subscribe - Subscribe button

Navigation

  • previous - Previous page
  • next - Next page
  • documentation - Documentation heading
  • language - Language label

Features & Pricing

  • seeItInAction - Feature demo heading
  • popular - Popular badge
  • mostPopular - Most popular badge
  • perMonth - Pricing suffix

Homepage

  • trustedByCompanies - Company logos heading
  • lovedByTeams - Testimonials heading

For the complete list with default values, see themes/chill-theme/i18n/en.toml

Example Translation File

i18n/en.toml:

[readMore]
other = "Read More"

[minRead]
other = "min read"

[previousPost]
other = "Previous Post"

[nextPost]
other = "Next Post"

[dateFormat]
other = "January 2, 2006"

i18n/zh-cn.toml:

[readMore]
other = "阅读更多"

[minRead]
other = "分钟阅读"

[previousPost]
other = "上一篇"

[nextPost]
other = "下一篇"

[dateFormat]
other = "2006年1月2日"

Content Organization

Directory Structure

content/
├── _index.md                    # English homepage
├── blog/
│   ├── post-1.md               # English blog posts
│   └── post-2.md
├── features/
│   └── performance.md          # English feature page
└── zh-cn/                      # Chinese content
    ├── _index.md               # Chinese homepage
    ├── blog/
    │   ├── post-1.md           # Chinese blog posts
    │   └── post-2.md
    └── features/
        └── performance.md      # Chinese feature page

Creating Translated Content

  1. Duplicate the structure from your default language
  2. Translate the content including front matter
  3. Keep the same file names for better URL consistency

Example - English post:

---
title: "Getting Started with Hugo"
date: 2024-01-15
categories: ["Tutorial"]
tags: ["hugo", "web development"]
---
Content in English...

Example - Chinese post:

---
title: "Hugo入门指南"
date: 2024-01-15
categories: ["教程"]
tags: ["hugo", "网站开发"]
---
中文内容...

Date Formatting

The theme uses the dateFormat i18n key for locale-specific date formatting:

# English format
[dateFormat]
other = "January 2, 2006"  # Output: July 20, 2023

# Chinese format
[dateFormat]
other = "2006年1月2日"  # Output: 2023年7月20日

# French format example
[dateFormat]
other = "2 January 2006"  # Output: 20 juillet 2023

Hugo's date format reference:

  • 2006 - Year (4 digits)
  • 06 - Year (2 digits)
  • January - Month (full name)
  • Jan - Month (abbreviated)
  • 01 - Month (2 digits)
  • 1 - Month (1-2 digits)
  • 02 - Day (2 digits)
  • 2 - Day (1-2 digits)
  • Monday - Weekday (full name)
  • Mon - Weekday (abbreviated)

Common Issues

Issue: Sidebar Still Showing English

Problem: Blog sidebar shows English text even in Chinese version.

Solution: Remove hardcoded titles from global [params.blog.sidebar] configuration. The theme's i18n will handle the translations automatically.

# Remove these from global config:
[params.blog.sidebar.recent]
  enable = true
  # title = "Recent Articles"  ← Remove this
  count = 5

Issue: Language Switcher Not Appearing

Problem: Language switcher doesn't show in header.

Solution: Ensure you have defined multiple languages in your hugo.toml:

[languages]
  [languages.en]
    # ...
  [languages.zh-cn]
    # ...

The language switcher automatically appears when 2+ languages are configured.

Problem: Logo/home link goes to wrong language homepage.

Solution: This is already fixed in the theme. The header uses {{ "/" | relLangURL }} which automatically links to the correct language homepage.

Adding a New Language

Let's add French as an example.

Quick Reference (For Experienced Users)

If you're familiar with Hugo i18n, here's what you need:

  1. Config: hugo.toml - Add languages.fr section with languageCode, languageName, title, weight, contentDir
  2. Translations: i18n/fr.toml - Copy from i18n/en.toml and translate all keys
  3. Content: content/fr/ - Create directory structure and translate content
  4. Menus: languages.fr.menu.main + 3 footer menus
  5. Params: languages.fr.params - Must include: cta, footer (with uppercase column titles), header, blog

Critical: Don't forget the footer configuration - it's required for footer to display correctly!

Detailed Step-by-Step Guide

1. Add Language Configuration

[languages.fr]
  languageCode = "fr-fr"
  languageName = "Français"
  title = "Votre Site"
  weight = 3
  contentDir = "content/fr"

2. Create Translation File

Copy the example and translate:

cp themes/chill-theme/docs/zh-cn.toml.example i18n/fr.toml

Edit i18n/fr.toml:

[readMore]
other = "Lire la suite"

[minRead]
other = "min de lecture"

[previousPost]
other = "Article précédent"

[nextPost]
other = "Article suivant"

[dateFormat]
other = "2 January 2006"

[recentArticles]
other = "Articles récents"

[categories]
other = "Catégories"

[popularTags]
other = "Tags populaires"

# ... translate all keys

3. Create French Content

content/fr/
├── _index.md
├── blog/
└── features/

4. Configure French Menu

[[languages.fr.menu.main]]
  name = "Fonctionnalités"
  url = "/fr/features"
  weight = 1

[[languages.fr.menu.main]]
  name = "Blog"
  url = "/fr/blog"
  weight = 2

5. Language-Specific Parameters (Required)

Configure language-specific parameters for your new language. Important: These parameters are required for proper navigation and UI display.

# French language specific params
[languages.fr.params]
  # Global CTA Configuration
  [languages.fr.params.cta]
    enable = true
    title = "Prêt à commencer?"
    description = "Rejoignez les entreprises qui utilisent notre plateforme"
    gradient_from = "#2563eb"
    gradient_to = "#7c3aed"
    gradient_angle = 30
    [languages.fr.params.cta.primary_button]
      text = "Commencer"
      url = "/fr/get-started"
    [languages.fr.params.cta.secondary_button]
      text = "Démo"
      url = "/fr/demo"

  # Footer Configuration (IMPORTANT - Required for footer to display correctly)
  [languages.fr.params.footer]
    column_1_title = "FONCTIONNALITÉS"
    column_2_title = "ENTREPRISE"
    column_3_title = "LÉGAL"

  # Header Configuration
  [languages.fr.params.header]
    [languages.fr.params.header.buttons]
      [languages.fr.params.header.buttons.signIn]
        text = "Connexion"
        url = "/fr/signin"
      [languages.fr.params.header.buttons.getStarted]
        text = "Commencer"
        url = "/fr/get-started"

  # Blog configuration
  [languages.fr.params.blog]
    enable = true
    title = "Derniers Articles"
    subtitle = "Apprenez-en plus sur le développement web et les meilleures pratiques"

Important Notes:

  • Footer Configuration: The footer section is required. Without it, footer column titles may not display correctly in the new language.
  • Use uppercase for footer column titles to maintain consistency with other languages
  • URL paths: Include the language prefix (e.g., /fr/) in all button URLs
  • All parameters should mirror the structure used in other languages (check English or Chinese examples)

6. Complete Checklist

Use this checklist to ensure you've configured everything correctly:

  • Language Config: Added language configuration in hugo.toml with languageCode, languageName, title, weight, and contentDir
  • Translation File: Created i18n/[lang].toml with all UI translations (50+ keys)
  • Content Directory: Created content/[lang]/ directory structure
  • Homepage: Created content/[lang]/_index.md with translated homepage content
  • Navigation Menus: Configured languages.[lang].menu.main, footer_column_1, footer_column_2, footer_column_3
  • Language Params - CTA: Added languages.[lang].params.cta configuration
  • Language Params - Footer: Added languages.[lang].params.footer with all three column titles (UPPERCASE)
  • Language Params - Header: Added languages.[lang].params.header.buttons configuration
  • Language Params - Blog: Added languages.[lang].params.blog configuration
  • URL Prefixes: All menu URLs include the language prefix (e.g., /fr/features)
  • Test: Verified language switcher appears in header with new language
  • Test: Verified homepage displays correctly in new language
  • Test: Verified top navigation menu shows translated items
  • Test: Verified footer navigation shows translated items
  • Test: Verified footer column titles display in new language

Common Mistakes to Avoid

  1. Missing Footer Config: Forgetting to add languages.[lang].params.footer will cause footer titles to not display
  2. Inconsistent Capitalization: Footer column titles should be UPPERCASE to match the theme's style
  3. Missing URL Prefixes: All language-specific URLs must include the language code (e.g., /fr/, not just /)
  4. Incomplete Menus: Ensure all three footer menus (footer_column_1, footer_column_2, footer_column_3) are configured
  5. Missing i18n Keys: Make sure all 50+ translation keys are present in your translation file

Best Practices

  1. Consistent Structure: Mirror your content directory structure across all languages
  2. Translation Keys: Use descriptive i18n keys like readMore instead of abbreviations
  3. Avoid Hardcoding: Don't set text parameters in global config; let i18n handle it
  4. Test Switching: Verify language switching works correctly on all pages
  5. Date Formats: Define appropriate date formats for each language
  6. Menus: Configure language-specific menus with translated labels
  7. URLs: Include language code in URLs for non-default languages

Resources

  • Hugo i18n Documentation: https://gohugo.io/content-management/multilingual/
  • Theme i18n Files: themes/chill-theme/i18n/
  • Demo Site Configuration: See complete trilingual setup in demo site's hugo.toml
  • Translation Examples:
    • English: i18n/en.toml (reference for all keys)
    • Chinese: i18n/zh-cn.toml (complete translation)
    • German: i18n/de.toml (complete translation)
  • Content Structure Examples:
    • content/ (English)
    • content/zh-cn/ (Chinese)
    • content/de/ (German)

Support

For issues or questions about i18n:

  1. Check TROUBLESHOOTING.md
  2. Review the demo site's working multilingual setup
  3. Open an issue on GitHub with details about your configuration