# Internationalization (i18n) Guide Complete guide for implementing multilingual support in Hugo Saasify Theme. ## Table of Contents - [Overview](#overview) - [Quick Start](#quick-start) - [Configuration](#configuration) - [Language Preference Persistence](#language-preference-persistence) - [Translation Keys](#translation-keys) - [Content Organization](#content-organization) - [Date Formatting](#date-formatting) - [Common Issues](#common-issues) - [Adding a New Language](#adding-a-new-language) ## 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`:** ```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. 2. **Copy translation files:** ```bash # 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 ``` 3. **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 ``` 4. **Configure language-specific menus:** ```toml # 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`: ```html {{ if .Site.IsMultiLingual }} {{ end }} ``` 2. 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](https://visitorapi.com/) 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](https://visitorapi.com/) and create a free account - Create a new project to get your Project ID 2. **Add the Project ID to your configuration**: ```toml [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" ``` 3. **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`: ```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`: ```javascript // 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 ```toml # 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: ```toml [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: ```toml # ❌ 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:** ```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:** ```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:** ```yaml --- title: "Getting Started with Hugo" date: 2024-01-15 categories: ["Tutorial"] tags: ["hugo", "web development"] --- Content in English... ``` **Example - Chinese post:** ```yaml --- title: "Hugo入门指南" date: 2024-01-15 categories: ["教程"] tags: ["hugo", "网站开发"] --- 中文内容... ``` ## Date Formatting The theme uses the `dateFormat` i18n key for locale-specific date formatting: ```toml # 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. ```toml # 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`: ```toml [languages] [languages.en] # ... [languages.zh-cn] # ... ``` The language switcher automatically appears when 2+ languages are configured. ### Issue: Wrong Language on Home Link **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 ```toml [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: ```bash cp themes/chill-theme/docs/zh-cn.toml.example i18n/fr.toml ``` Edit `i18n/fr.toml`: ```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 ```toml [[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. ```toml # 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](TROUBLESHOOTING.md) 2. Review the demo site's working multilingual setup 3. Open an issue on GitHub with details about your configuration