Files
markusgraf_ch/openspec/changes/archive/2025-10-30-add-multilingual-support/design.md
T
gurixandClaude adbee2c655 chore: archive completed OpenSpec changes
Archive all 6 deployed changes following OpenSpec workflow:
- add-minimal-hugo-site → hugo-site spec
- add-multilingual-support → internationalization spec
- migrate-site-content → content-migration + image-assets specs
- add-custom-bootstrap-styling → styling spec
- add-cv-page → content-management spec
- simple-deployment → deployment spec

All changes moved to archive/ with 2025-10-30 date prefix.
Created 7 capability specs reflecting deployed functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 17:22:03 +01:00

11 KiB

Multilingual Support Design

Context

The site needs to support both German and English content with seamless language switching. Hugo provides built-in multilingual support that we'll leverage. The current content is in German only, stored directly in /content/.

Goals / Non-Goals

Goals

  • Support German (de) and English (en) languages
  • Automatic browser language detection for first-time visitors
  • Persistent language preference via URL structure
  • Language switcher that maintains page context (e.g., /de/cv/en/cv)
  • All existing content translated to English
  • Clean URL structure using language codes as path prefixes

Non-Goals

  • Translation management system or CMS integration
  • More than 2 languages at this stage
  • Client-side JavaScript for language switching (Hugo server-side only)
  • Automated translation (manual translation required)

Decisions

Decision 1: Hugo Multilingual Mode

Approach: Use Hugo's built-in multilingual mode with language code prefixes in URLs

Rationale:

  • Hugo natively supports multilingual sites with defaultContentLanguage and [languages] config
  • URL structure /de/ and /en/ clearly indicates language and persists preference
  • No cookies or JavaScript needed for language persistence
  • SEO-friendly with proper hreflang tags

Alternatives Considered:

  • Subdomain approach (de.markusgraf.ch, en.markusgraf.ch): Rejected due to complexity and DNS configuration
  • Client-side JavaScript switching: Rejected to keep site static and performant

Decision 2: Content Organization

Approach: Reorganize content into language-specific directories /content/de/ and /content/en/

Structure:

content/
├── de/
│   ├── _index.md
│   └── cv.md
└── en/
    ├── _index.md
    └── cv.md

Rationale:

  • Clear separation of language-specific content
  • Hugo automatically links translations when file paths match
  • Easy to maintain and scale

Decision 3: Language Switcher Implementation

Approach: Add language switcher in navigation partial using Hugo's .Translations function

Implementation:

  • Display language switcher as links (e.g., "DE | EN")
  • Active language styled differently
  • Maintains current page context when switching
  • Falls back to home page if translation doesn't exist

Rationale:

  • Simple, no JavaScript required
  • Hugo provides .Translations to find corresponding pages
  • Consistent with existing navigation patterns

Decision 4: Default Language

Approach: German (de) as default with English (en) as secondary

Configuration:

defaultContentLanguage = "de"
defaultContentLanguageInSubdir = true

Rationale:

  • Existing content is in German
  • Primary audience is German-speaking
  • defaultContentLanguageInSubdir = true ensures consistent URLs (/de/ for German, not just /)

Decision 5: Browser Language Detection

Approach: Hugo serves content based on URL path; Apache/Nginx handles initial redirect based on Accept-Language header

Rationale:

  • Hugo is a static site generator and cannot detect browser language at runtime
  • Server-side redirect is the cleanest approach
  • Falls back to default language (German) if header not present
  • Once user navigates, URL path maintains their choice

Migration Path:

  1. Configure multilingual mode in hugo.toml
  2. Reorganize existing content to /content/de/
  3. Create English translations in /content/en/
  4. Update templates with language switcher
  5. Configure server redirect rules (optional, for initial visit only)

Risks / Trade-offs

Risk 1: Breaking Change in Content Structure

Risk: Moving content files breaks existing local references and workflows Mitigation:

  • Clear documentation of new structure
  • All content moved in single commit
  • Update paths in OpenSpec documentation

Risk 2: Translation Maintenance

Risk: Keeping German and English content in sync over time Mitigation:

  • Document translation process
  • Consider adding translation status tracking in future
  • For now, manual process with clear ownership

Risk 3: SEO Impact

Risk: URL structure changes may affect search rankings Mitigation:

  • Implement proper hreflang tags (Hugo does this automatically)
  • Add redirects from old URLs to new German URLs
  • Submit new sitemap to search engines

Migration Plan

Phase 1: Configure Hugo (No Breaking Changes Yet)

  1. Update hugo.toml with multilingual configuration
  2. Test configuration with existing content structure

Phase 2: Restructure Content

  1. Move existing content to /content/de/
  2. Update any internal references
  3. Build and verify German site works at /de/

Phase 3: Add English Content

  1. Create /content/en/ directory
  2. Translate home page content
  3. Translate CV page content
  4. Translate navigation and UI strings

Phase 4: Update Templates

  1. Add language switcher to navigation
  2. Update partials for multilingual strings
  3. Test language switching functionality

Phase 5: Server Configuration (Optional)

  1. Add .htaccess or nginx rules for browser language detection
  2. Redirect root / to /de/ or /en/ based on Accept-Language header

Open Questions

Q: Should the root URL / redirect to a language version, or show a language selection page? A: Redirect to /de/ as default, optionally detect browser language. No standalone language selection page needed.

Q: How should we handle the menu configuration for different languages? A: Hugo supports language-specific menus using [[languages.de.menu.main]] and [[languages.en.menu.main]] in hugo.toml.

Q: Should we translate the project descriptions on the home page? A: Yes, translate all visible content including project descriptions.

Implementation Bugs and Solutions

During implementation, several critical bugs were discovered and fixed. These are documented here to avoid future pitfalls.

Bug 1: Language Switcher Showing Current Language

Problem: The language switcher initially showed a dropdown with the current language as the toggle button (e.g., showing "Deutsch" when on German pages). This was confusing because users couldn't easily see which language to switch to.

Solution: Simplified the language switcher to only show the alternative language as a direct link. Removed the dropdown pattern and changed from:

<li class="nav-item dropdown">
  <a class="dropdown-toggle">{{ .Language.LanguageName }}</a>
  <ul class="dropdown-menu">...</ul>
</li>

To:

{{ if .IsTranslated }}
  {{ range .Translations }}
    <li class="nav-item">
      <a class="nav-link" href="{{ .RelPermalink }}">{{ .Language.LanguageName }}</a>
    </li>
  {{ end }}
{{ end }}

Lesson: For a two-language site, showing only the alternative language is clearer than a dropdown showing the current language.

Bug 2: Language Switcher Not Maintaining Page Context

Problem: When switching languages from /de/cv/ to English, it would redirect to /en/ (home) instead of /en/cv/ (maintaining the CV page context).

Solution: Added translationKey to the frontmatter of both language versions of each content file to explicitly link them as translations:

---
title: "Curriculum Vitae"
translationKey: "cv"
---

This allows Hugo's .IsTranslated and .Translations functions to correctly find corresponding pages.

Lesson: Hugo doesn't automatically recognize translations by filename alone when using language subdirectories. Always add translationKey to explicitly link translated pages.

Bug 3: CV Pages Rendering with Wrong Language Context

Problem: The most critical bug - CV pages would render with the wrong language:

  • /en/cv/ would show lang="de", German menu ("Lebenslauf"), and German language context
  • Both German and English CV pages would show incorrect language switcher labels

Root Cause: Using language subdirectories (content/de/cv.md and content/en/cv.md) caused Hugo to incorrectly resolve the language context for these pages, even though hugo list all showed them with the correct language tags.

Solution: Reorganized content files to use Hugo's language suffix naming convention instead of subdirectories:

  • Changed from: content/de/cv.md and content/en/cv.md
  • Changed to: content/cv.de.md and content/cv.en.md

After this change, all pages rendered with the correct language context.

Lesson: With Hugo's multilingual mode and defaultContentLanguageInSubdir = true, use the .{lang}.md suffix pattern (e.g., filename.de.md, filename.en.md) rather than language subdirectories (de/filename.md, en/filename.md). The subdirectory approach can cause template rendering issues where the wrong language context is used.

Bug 4: Home and Navbar Brand Not Respecting Language

Problem: Clicking the "Home" link or navbar brand from /en/cv/ would always redirect to / (German home) instead of /en/ (English home), breaking language persistence.

Root Cause: Both links used {{ .Site.BaseURL }} which always points to the absolute root URL without language awareness.

Solution: Changed both to use Hugo's relLangURL function:

<!-- Before -->
<a href="{{ .Site.BaseURL }}">Home</a>

<!-- After -->
<a href="{{ "/" | relLangURL }}">Home</a>

This makes the links language-aware, so:

  • On German pages: {{ "/" | relLangURL }}/de/
  • On English pages: {{ "/" | relLangURL }}/en/

Lesson: Never use {{ .Site.BaseURL }} for internal navigation in multilingual sites. Always use relLangURL or absLangURL to maintain language context.

Bug 5: Menu URLs Not Language-Aware

Problem: The German "Lebenslauf" menu link pointed to /cv/ which resulted in a 404 error because with defaultContentLanguageInSubdir = true, German pages are at /de/cv/, not /cv/.

Root Cause: Menu URLs in hugo.toml were hardcoded:

[[languages.de.menu.main]]
  url = '/cv/'  # Wrong - page doesn't exist here

Solution: Updated menu URLs to include the language prefix:

[[languages.de.menu.main]]
  name = 'Lebenslauf'
  url = '/de/cv/'  # Correct
  weight = 10

[[languages.en.menu.main]]
  name = 'Curriculum Vitae'
  url = '/en/cv/'  # Correct
  weight = 10

Lesson: When defaultContentLanguageInSubdir = true, even the default language requires the language prefix in all URLs. Don't assume the default language is accessible at the root paths.

Best Practices Derived from Bugs

  1. Content Organization: Use filename.{lang}.md pattern instead of {lang}/filename.md subdirectories to avoid template context issues.

  2. Translation Linking: Always add translationKey to frontmatter for explicit translation relationships.

  3. URL Generation: Use Hugo's language-aware functions:

    • relLangURL for relative URLs
    • absLangURL for absolute URLs
    • .RelPermalink for page permalinks
    • Never use {{ .Site.BaseURL }} for internal links
  4. Menu Configuration: When defaultContentLanguageInSubdir = true, all menu URLs must include the language prefix, even for the default language.

  5. Testing: Always test all pages in all languages, not just home pages. CV/subpages may render with different template contexts than home pages.