Add language switcher and language detection

This commit is contained in:
2026-08-11 12:25:14 +02:00
parent 3e7c831dac
commit df489c6c79
11 changed files with 526 additions and 384 deletions
+226 -162
View File
@@ -3,26 +3,7 @@ baseURL = "/"
title = ""
theme = "chill-theme"
defaultContentLanguage = "fr"
disableLanguages = ["en", "nl"]
# Language configuration
[languages]
[languages.fr]
languageCode = "fr-fr"
languageName = "Français"
weight = 1
contentDir = "content"
[languages.en]
languageCode = "en-us"
languageName = "English"
weight = 2
contentDir = "content/en"
[languages.nl]
languageCode = "nl-nl"
languageName = "Nederlands"
weight = 3
contentDir = "content/nl"
defaultContentLanguageInSubdir = false
# Required Features
enableEmoji = true # Enable emoji support
@@ -69,14 +50,13 @@ pagination = { pagerSize = 6, path = "page" }
category = 'categories'
tag = 'tags'
# Theme Parameters
# Theme Parameters shared by every language.
# Anything language specific lives under [languages.<lang>.params] below.
[params]
description = "Logiciel libre d'accompagnement social"
author = "Champs-Libres"
logo = "/images/logo-darkmode.png" # Path to your logo
customCSS = ["css/custom.css"]
# Header Configuration
[params.header]
background = "header backdrop-blur-sm"
@@ -84,7 +64,6 @@ pagination = { pagerSize = 6, path = "page" }
menu.linkClass = "text-white hover:text-primary font-bold transition duration-200"
[params.header.buttons.demo]
text = "Démo"
url = "http://demo.chill.social/"
mobileClass = "block text-center px-6 py-3 rounded-lg font-bold transition duration-200 ease-in-out bg-primary-400 text-white hover:bg-primary-400 hover:scale-105"
open_tab = true
@@ -93,154 +72,239 @@ pagination = { pagerSize = 6, path = "page" }
[params.header.logo]
src = "/images/logo-darkmode.png"
[params.pdfFolder]
name = "Voir la brochure CHILL"
filename = "Brochure_Chill.pdf"
url = "/pdf/Brochure_Chill.pdf"
# Global CTA Configuration (optional)
[params.cta]
enable = true
title = "Prêt·e à commencer ?"
description = "Testez Chill dès aujourd'hui et découvrez comment il peut transformer votre gestion sociale."
enable = true
# Gradient background
gradient_from = "#0d242e" # Blue-600
gradient_to = "#23627e" # Purple-600
gradient_angle = 30
# Gradient background
gradient_from = "#0d242e" # Blue-600
gradient_to = "#23627e" # Purple-600
gradient_angle = 30
# Primary button
[params.cta.primary_button]
text = "Réserver une démo"
url = "/contact?subject=demo"
open_tab = false
# Secondary button
[params.cta.secondary_button]
text = "Essayer la démo"
url = "http://demo.chill.social/"
open_tab = true
# Social Media Links (optional)
[params.social]
gitlab = "https://gitlab.com/Chill-Projet/chill-bundles"
# Navigation Menu
[menu]
[[menu.main]]
name = "Fonctionnalités"
url = "/features"
weight = 1
[[menu.main]]
name = "Secteur d'activités"
weight = 2
url = "/sector"
[menu.main.params]
has_submenu = true
submenu = [
{ name = "Grandes Collectivités", url = "/sector/collectivities" },
{ name = "Services et associations", url = "/sector/associations" },
]
[[menu.main]]
name = "Tarification"
url = "/pricing"
weight = 3
[[menu.main]]
name = "Ressources"
weight = 4
[menu.main.params]
has_submenu = true
submenu = [
# { name = "Blog", url = "/resources/blog" },
{ name = "Manuels", url = "/resources/manuals" },
{ name = "Tutoriels", url = "/resources/tutorials" },
{ name = "Installation", url = "/resources/installation" },
{ name = "Documentation technique", url = "/resources/technical-documentation" },
{ name = "FAQ", url = "/resources/faq" }
]
[[menu.main]]
name = "Nous contacter"
url = "/contact"
weight = 5
[[menu.footer_column]]
name = "Champs-Libres"
url = "https://www.champs-libres.coop/"
weight = 1
# ---------------------------------------------------------------------------
# Languages
# French is the default language and is served from the site root ("/"),
# English lives under "/en/" and Dutch under "/nl/".
# ---------------------------------------------------------------------------
[languages]
# ------------------------------- Français ----------------------------------
[languages.fr]
languageCode = "fr-fr"
languageName = "Français"
weight = 1
contentDir = "content/fr"
[languages.fr.params]
description = "Logiciel libre d'accompagnement social"
[languages.fr.params.header.buttons.demo]
text = "Démo"
[languages.fr.params.pdfFolder]
name = "Voir la brochure CHILL"
filename = "Brochure_Chill.pdf"
url = "/pdf/Brochure_Chill.pdf"
[languages.fr.params.cta]
title = "Prêt·e à commencer ?"
description = "Testez Chill dès aujourd'hui et découvrez comment il peut transformer votre gestion sociale."
[languages.fr.params.cta.primary_button]
text = "Réserver une démo"
url = "/contact?subject=demo"
open_tab = false
[languages.fr.params.cta.secondary_button]
text = "Essayer la démo"
url = "http://demo.chill.social/"
open_tab = true
[languages.fr.menu]
[[languages.fr.menu.main]]
name = "Fonctionnalités"
url = "/features"
weight = 1
[[languages.fr.menu.main]]
name = "Secteur d'activités"
url = "/sector"
weight = 2
[languages.fr.menu.main.params]
has_submenu = true
submenu = [
{ name = "Grandes Collectivités", url = "/sector/collectivities" },
{ name = "Services et associations", url = "/sector/associations" },
]
[[languages.fr.menu.main]]
name = "Tarification"
url = "/pricing"
weight = 3
[[languages.fr.menu.main]]
name = "Ressources"
weight = 4
[languages.fr.menu.main.params]
has_submenu = true
submenu = [
# { name = "Blog", url = "/resources/blog" },
{ name = "Manuels", url = "/resources/manuals" },
{ name = "Tutoriels", url = "/resources/tutorials" },
{ name = "Installation", url = "/resources/installation" },
{ name = "Documentation technique", url = "/resources/technical-documentation" },
{ name = "FAQ", url = "/resources/faq" }
]
[[languages.fr.menu.main]]
name = "Nous contacter"
url = "/contact"
weight = 5
[[languages.fr.menu.footer_column]]
name = "Champs-Libres"
url = "https://www.champs-libres.coop/"
weight = 1
# ------------------------------- English -----------------------------------
[languages.en]
languageCode = "en-us"
languageName = "English"
weight = 2
contentDir = "content/en"
[languages.en.params]
description = "Free and open source software for social work"
[languages.en.params.header.buttons.demo]
text = "Demo"
[languages.en.params.pdfFolder]
name = "See the CHILL brochure"
filename = "Brochure_Chill.pdf"
url = "/pdf/Brochure_Chill.pdf"
[languages.en.params.cta]
title = "Ready to get started?"
description = "Try Chill today and discover how it can transform the way you manage social support."
[languages.en.params.cta.primary_button]
text = "Book a demo"
url = "/en/contact?subject=demo"
open_tab = false
[languages.en.params.cta.secondary_button]
text = "Try the demo"
url = "http://demo.chill.social/"
open_tab = true
[languages.en.menu]
[[languages.en.menu.main]]
name = "Features"
url = "/features"
weight = 1
[[languages.en.menu.main]]
name = "Installation"
weight = 2
[languages.en.menu.main.params]
has_submenu = true
submenu = [
{ name = "By Champs Libres", url = "/install/hosted" },
{ name = "Self-hosted", url = "/install/on-premise" }
]
[[languages.en.menu.main]]
name = "Pricing"
url = "/pricing"
weight = 3
[[languages.en.menu.main]]
name = "Resources"
weight = 4
[languages.en.menu.main.params]
has_submenu = true
submenu = [
# { name = "Blog", url = "/resources/blog" },
{ name = "Technical Documentation", url = "/resources/technical-documentation" },
{ name = "Manuals", url = "/resources/manuals" },
{ name = "Tutorials", url = "/resources/tutorials" },
{ name = "FAQ", url = "/resources/faq" }
]
[[languages.en.menu.main]]
name = "Contact Us"
url = "/contact"
weight = 5
[[languages.en.menu.main]]
name = "Features"
url = "/en/features"
weight = 1
[[languages.en.menu.main]]
name = "Sectors"
url = "/en/sector"
weight = 2
[languages.en.menu.main.params]
has_submenu = true
submenu = [
{ name = "Large local authorities", url = "/en/sector/collectivities" },
{ name = "Services and non-profits", url = "/en/sector/associations" },
]
[[languages.en.menu.main]]
name = "Pricing"
url = "/en/pricing"
weight = 3
[[languages.en.menu.main]]
name = "Resources"
weight = 4
[languages.en.menu.main.params]
has_submenu = true
submenu = [
# { name = "Blog", url = "/en/resources/blog" },
{ name = "Manuals", url = "/en/resources/manuals" },
{ name = "Tutorials", url = "/en/resources/tutorials" },
{ name = "Installation", url = "/en/resources/installation" },
{ name = "Technical documentation", url = "/en/resources/technical-documentation" },
{ name = "FAQ", url = "/en/resources/faq" }
]
[[languages.en.menu.main]]
name = "Contact us"
url = "/en/contact"
weight = 5
[[languages.en.menu.footer_column_1]]
name = "Download the CHILL brochure"
url = "/pdf/Brochure_Chill.pdf"
weight = 1
[[languages.en.menu.footer_column]]
name = "Champs-Libres"
url = "https://www.champs-libres.coop/"
weight = 1
# ------------------------------ Nederlands ---------------------------------
[languages.nl]
languageCode = "nl-nl"
languageName = "Nederlands"
weight = 3
contentDir = "content/nl"
[languages.nl.params]
description = "Vrije en opensource software voor maatschappelijk werk"
[languages.nl.params.header.buttons.demo]
text = "Demo"
[languages.nl.params.pdfFolder]
name = "Bekijk de CHILL-brochure"
filename = "Brochure_Chill.pdf"
url = "/pdf/Brochure_Chill.pdf"
[languages.nl.params.cta]
title = "Klaar om te starten?"
description = "Probeer Chill vandaag nog en ontdek hoe het uw sociale begeleiding kan transformeren."
[languages.nl.params.cta.primary_button]
text = "Een demo boeken"
url = "/nl/contact?subject=demo"
open_tab = false
[languages.nl.params.cta.secondary_button]
text = "De demo uitproberen"
url = "http://demo.chill.social/"
open_tab = true
[languages.nl.menu]
[[languages.nl.menu.main]]
name = "Functies"
url = "/features"
weight = 1
[[languages.nl.menu.main]]
name = "Installatie"
weight = 2
[languages.nl.menu.main.params]
has_submenu = true
submenu = [
{ name = "Door Champs Libres", url = "/install/hosted" },
{ name = "Zelf gehost", url = "/install/on-premise" }
]
[[languages.nl.menu.main]]
name = "Prijsstelling"
url = "/pricing"
weight = 3
[[languages.nl.menu.main]]
name = "Bronnen"
weight = 4
[languages.nl.menu.main.params]
has_submenu = true
submenu = [
{ name = "Technische Documentatie", url = "/resources/technical-documentation" },
{ name = "Handleidingen", url = "/resources/manuals" },
{ name = "Tutorials", url = "/resources/tutorials" },
{ name = "FAQ", url = "/resources/faq" },
# { name = "Blog", url = "/resources/blog" }
]
[[languages.nl.menu.main]]
name = "Contacteer Ons"
url = "/contact"
weight = 5
[[languages.nl.menu.main]]
name = "Functionaliteiten"
url = "/nl/features"
weight = 1
[[languages.nl.menu.main]]
name = "Sectoren"
url = "/nl/sector"
weight = 2
[languages.nl.menu.main.params]
has_submenu = true
submenu = [
{ name = "Grote lokale besturen", url = "/nl/sector/collectivities" },
{ name = "Diensten en verenigingen", url = "/nl/sector/associations" },
]
[[languages.nl.menu.main]]
name = "Tarieven"
url = "/nl/pricing"
weight = 3
[[languages.nl.menu.main]]
name = "Hulpbronnen"
weight = 4
[languages.nl.menu.main.params]
has_submenu = true
submenu = [
# { name = "Blog", url = "/nl/resources/blog" },
{ name = "Handleidingen", url = "/nl/resources/manuals" },
{ name = "Tutorials", url = "/nl/resources/tutorials" },
{ name = "Installatie", url = "/nl/resources/installation" },
{ name = "Technische documentatie", url = "/nl/resources/technical-documentation" },
{ name = "FAQ", url = "/nl/resources/faq" }
]
[[languages.nl.menu.main]]
name = "Contacteer ons"
url = "/nl/contact"
weight = 5
[[languages.nl.menu.footer_column_1]]
name = "Download de CHILL-brochure"
url = "/pdf/Brochure_Chill.pdf"
weight = 1
[[languages.nl.menu.footer_column]]
name = "Champs-Libres"
url = "https://www.champs-libres.coop/"
weight = 1
+13 -11
View File
@@ -53,6 +53,19 @@
<!-- Canonical URL -->
<link rel="canonical" href="{{ .Permalink }}">
<!-- Alternate language versions of this page -->
{{ if hugo.IsMultilingual }}
{{ range .AllTranslations }}
<link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
{{ end }}
{{ with hugo.Sites.Default.Home }}
<link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
{{ end }}
{{ end }}
<!-- Language preference / browser language detection (must run before paint) -->
{{ partial "language-detection" . }}
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -91,17 +104,6 @@
<!-- Footer -->
{{ partial "footer" . }}
<!-- Language Preference Script -->
{{ if hugo.IsMultilingual }}
{{ if .Site.Params.visitorapi_pid }}
<script>
// Inject VisitorAPI project ID from Hugo config
window.HUGO_VISITOR_API_PID = "{{ .Site.Params.visitorapi_pid }}";
</script>
{{ end }}
<script src="{{ "js/language-preference.js" | relURL }}"></script>
{{ end }}
<!-- Mobile Menu Script -->
<script>
const mobileMenuButton = document.getElementById('mobile-menu-button');
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode | default "en" }}" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Google Tag Manager -->
{{ partial "google-tag-manager" . }}
<!-- Google Analytics -->
{{ partial "google-analytics" . }}
<!-- Title -->
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
<!-- Meta Tags -->
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}">
<meta name="author" content="{{ .Site.Params.author | default .Site.Title }}">
<meta name="robots" content="{{ with .Params.robots }}{{ . }}{{ else }}index, follow{{ end }}">
<!-- Open Graph -->
<meta property="og:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}">
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}">
<meta property="og:type" content="{{ if .IsHome }}website{{ else }}article{{ end }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:site_name" content="{{ .Site.Title }}">
{{ with .Params.image }}
<meta property="og:image" content="{{ . | absURL }}">
{{ else }}
{{ with .Site.Params.image }}
<meta property="og:image" content="{{ . | absURL }}">
{{ end }}
{{ end }}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}">
<meta name="twitter:description" content="{{ with .Description }}{{ . }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}">
{{ with .Site.Params.twitter }}
<meta name="twitter:site" content="@{{ . }}">
{{ end }}
{{ with .Params.image }}
<meta name="twitter:image" content="{{ . | absURL }}">
{{ else }}
{{ with .Site.Params.image }}
<meta property="twitter:image" content="{{ . | absURL }}">
{{ end }}
{{ end }}
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="{{ "images/favicon.ico" | relURL }}">
<!-- Canonical URL -->
<link rel="canonical" href="{{ .Permalink }}">
<!-- Alternate languages -->
{{ if hugo.IsMultilingual }}
{{ range .AllTranslations }}
<link rel="alternate" hreflang="{{ .Lang }}" href="{{ .Permalink }}">
{{ end }}
{{ with .Sites.Default.Home }}
<link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
{{ end }}
{{ end }}
<!-- Language preference / browser language detection (must run before paint) -->
{{ partial "language-detection" . }}
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@600;700;800&display=swap" rel="stylesheet">
<!-- Main CSS -->
<link rel="stylesheet" href="{{ "css/style.css" | relURL }}">
<!-- Additional Meta Tags from Front Matter -->
{{ with .Params.customMeta }}
{{ range . }}
<meta {{ range $key, $value := . }} {{ $key }}="{{ $value }}"{{ end }}>
{{ end }}
{{ end }}
<!-- Custom Head Content -->
{{ partial "custom-head" . }}
</head>
<body class="min-h-screen flex flex-col">
<!-- Google Tag Manager (noscript) -->
{{ if and hugo.IsProduction .Site.Params.googleTagManager }}
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{ .Site.Params.googleTagManager }}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
{{ end }}
<!-- End Google Tag Manager (noscript) -->
<!-- Header -->
<div class="fixed top-0 left-0 right-0 z-50">
{{ partial "header" . }}
</div>
<!-- Main Content -->
<div class="pt-20">
{{ block "main" . }}{{ end }}
</div>
<!-- Footer -->
{{ partial "footer" . }}
<!-- Language Preference Script -->
{{ if hugo.IsMultilingual }}
{{ if .Site.Params.visitorapi_pid }}
<script>
// Inject VisitorAPI project ID from Hugo config
window.HUGO_VISITOR_API_PID = "{{ .Site.Params.visitorapi_pid }}";
</script>
{{ end }}
<script src="{{ "js/language-preference.js" | relURL }}"></script>
{{ end }}
<!-- Mobile Menu Script -->
<script>
const mobileMenuButton = document.getElementById('mobile-menu-button');
if (mobileMenuButton) {
mobileMenuButton.addEventListener('click', function() {
const mobileMenu = document.getElementById('mobile-menu');
if (mobileMenu) {
mobileMenu.classList.toggle('hidden');
}
});
}
</script>
</body>
</html>
@@ -130,10 +130,10 @@
<div class="mt-6 pt-8 border-t border-gray-600">
<div class="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<p class="text-gray-600 text-sm">
© {{ now.Format "2006" }} {{ .Site.Title }}. All rights reserved.
© {{ now.Format "2006" }} {{ with .Site.Title }}{{ . }}. {{ end }}{{ i18n "allRightsReserved" }}
</p>
<a href="https://www.champs-libres.coop/" class="text-sm text-gray-600 hover:text-primary-600" target="_blank" rel="noopener noreferrer">
Designed by Champs-Libres
{{ i18n "designedBy" }} Champs-Libres
</a>
</div>
</div>
@@ -89,21 +89,7 @@
{{ end }}
<!-- Mobile Language Switcher -->
{{ if hugo.IsMultilingual }}
<div class="py-4 border-t border-gray-200 mt-4">
<div class="text-lg text-gray-600 font-semibold mb-2">{{ i18n "language" }}</div>
{{ range .Site.Languages }}
{{ if eq $.Site.Language.Lang .Lang }}
<span class="block py-2 text-primary-600 font-bold">{{ .LanguageName }}</span>
{{ else }}
{{ $langURL := printf "/%s/" .Lang }}
<a href="{{ $langURL }}" class="language-switch-link block py-2 text-gray-700 hover:text-primary-600" data-lang="{{ .Lang }}">
{{ .LanguageName }}
</a>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ partial "language-switcher-mobile" . }}
{{ if not $headerConfig.hideButtons }}
<div class="pt-4 space-y-4">
@@ -0,0 +1,106 @@
{{/*
Language preference + browser language detection.
Rendered inline in <head> on purpose: it must run before the page paints so
a visitor is never shown a flash of the wrong language, and inlining avoids
an extra HTTP request for ~1 KB of script.
Hugo hands the script everything it needs, so the script never has to guess
URLs: the home page of every language and the translations of *this* page.
That keeps it correct even though French is served from the site root ("/")
while the other languages sit under a prefix ("/en/", "/nl/").
*/}}
{{ if hugo.IsMultilingual }}
{{ $homes := dict }}
{{ range hugo.Sites }}
{{ $homes = merge $homes (dict .Language.Lang .Home.RelPermalink) }}
{{ end }}
{{ $translations := dict }}
{{ range .AllTranslations }}
{{ $translations = merge $translations (dict .Lang .RelPermalink) }}
{{ end }}
<script>
(function () {
'use strict';
var STORAGE_KEY = 'hugo_preferred_language';
var SESSION_KEY = 'language_redirected';
var CURRENT = {{ .Site.Language.Lang }};
var HOMES = {{ $homes | jsonify | safeJS }};
var TRANSLATIONS = {{ $translations | jsonify | safeJS }};
var AVAILABLE = Object.keys(HOMES);
function read(store, key) {
try { return window[store].getItem(key); } catch (e) { return null; }
}
function write(store, key, value) {
try { window[store].setItem(key, value); } catch (e) { /* private mode */ }
}
/* Match a browser language tag ("nl-BE", "en") against the languages this
site actually publishes, comparing on the primary subtag. */
function matchLanguage(tag) {
var primary = String(tag).toLowerCase().split('-')[0];
for (var i = 0; i < AVAILABLE.length; i++) {
if (AVAILABLE[i].toLowerCase().split('-')[0] === primary) {
return AVAILABLE[i];
}
}
return null;
}
function detectFromBrowser() {
var tags = navigator.languages || (navigator.language ? [navigator.language] : []);
for (var i = 0; i < tags.length; i++) {
var match = matchLanguage(tags[i]);
if (match) { return match; }
}
return null;
}
function targetFor(lang) {
/* Prefer the translation of the page being viewed, so a deep link keeps
the visitor on the same page in their language. */
return TRANSLATIONS[lang] || HOMES[lang] || null;
}
function resolveAndRedirect() {
/* Auto-redirect at most once per browsing session: after that the
visitor is free to navigate to any language by hand. */
if (read('sessionStorage', SESSION_KEY)) { return; }
write('sessionStorage', SESSION_KEY, '1');
var stored = read('localStorage', STORAGE_KEY);
var wanted = (stored && HOMES[stored]) ? stored : detectFromBrowser();
if (!wanted || wanted === CURRENT) { return; }
var target = targetFor(wanted);
if (target) {
window.location.replace(target + window.location.search + window.location.hash);
}
}
/* Remember an explicit choice made through the language switcher. */
function rememberChoices() {
var links = document.querySelectorAll('.language-switch-link');
for (var i = 0; i < links.length; i++) {
links[i].addEventListener('click', function () {
var lang = this.getAttribute('data-lang');
if (lang) { write('localStorage', STORAGE_KEY, lang); }
});
}
}
resolveAndRedirect();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', rememberChoices);
} else {
rememberChoices();
}
})();
</script>
{{ end }}
@@ -0,0 +1,24 @@
{{/*
Mobile language switcher, used inside the collapsed navigation menu.
Same translation-aware linking as the desktop switcher.
*/}}
{{ if hugo.IsMultilingual }}
{{ $page := . }}
<div class="py-4 border-t border-gray-200 mt-4">
<div class="text-lg text-gray-600 font-semibold mb-2">{{ i18n "language" }}</div>
{{ range hugo.Sites }}
{{ $lang := .Language.Lang }}
{{ if eq $.Site.Language.Lang $lang }}
<span class="block py-2 text-primary-600 font-bold">{{ .Language.LanguageName }}</span>
{{ else }}
{{ $target := .Home }}
{{ range $page.AllTranslations }}
{{ if eq .Lang $lang }}{{ $target = . }}{{ end }}
{{ end }}
<a href="{{ $target.RelPermalink }}" hreflang="{{ $lang }}" lang="{{ $lang }}" class="language-switch-link block py-2 text-gray-700 hover:text-primary-600" data-lang="{{ $lang }}">
{{ .Language.LanguageName }}
</a>
{{ end }}
{{ end }}
</div>
{{ end }}
@@ -1,8 +1,16 @@
{{/*
Desktop language switcher.
Links point at the translation of the *current* page when one exists, so
switching language keeps the visitor where they are. When the current page
has no translation we fall back to the home page of the target language.
*/}}
{{ if hugo.IsMultilingual }}
{{ $page := . }}
{{ $headerConfig := .Site.Params.header }}
{{ $dropdownConfig := $headerConfig.menu.dropdown }}
<div class="relative group">
<button class="flex items-center {{ with $headerConfig.menu.linkClass }}{{ . }}{{ else }}text-base text-gray-900 hover:text-primary-600 font-bold transition duration-200{{ end }}">
<button type="button" aria-label="{{ i18n "switchLanguage" }}" class="flex items-center {{ with $headerConfig.menu.linkClass }}{{ . }}{{ else }}text-base text-gray-900 hover:text-primary-600 font-bold transition duration-200{{ end }}">
{{ .Site.Language.LanguageName }}
<svg class="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
@@ -10,13 +18,17 @@
</button>
<div class="absolute right-0 mt-2 {{ $dropdownConfig.width | default "w-72" }} opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 ease-in-out">
<div class="{{ $dropdownConfig.container_padding | default "py-6" }} {{ $dropdownConfig.background | default "bg-white" }} {{ $dropdownConfig.border | default "border border-gray-100" }} {{ $dropdownConfig.shadow | default "shadow-xl" }} {{ $dropdownConfig.radius | default "rounded-lg" }}">
{{ range .Site.Languages }}
{{ if ne $.Site.Language.Lang .Lang }}
{{ $langURL := printf "/%s/" .Lang }}
<a href="{{ $langURL }}" class="language-switch-link block {{ $dropdownConfig.item_padding | default "px-8 py-3" }} {{ $dropdownConfig.text_size | default "text-sm" }} {{ $dropdownConfig.text_color | default "text-gray-700" }} {{ $dropdownConfig.hover_background | default "hover:bg-gray-50" }}" data-lang="{{ .Lang }}">
{{ .LanguageName }}
{{ range hugo.Sites }}
{{ $lang := .Language.Lang }}
{{ if ne $.Site.Language.Lang $lang }}
{{ $target := .Home }}
{{ range $page.AllTranslations }}
{{ if eq .Lang $lang }}{{ $target = . }}{{ end }}
{{ end }}
<a href="{{ $target.RelPermalink }}" hreflang="{{ $lang }}" lang="{{ $lang }}" class="language-switch-link block {{ $dropdownConfig.item_padding | default "px-8 py-3" }} {{ $dropdownConfig.text_size | default "text-sm" }} {{ $dropdownConfig.text_color | default "text-gray-700" }} {{ $dropdownConfig.hover_background | default "hover:bg-gray-50" }}" data-lang="{{ $lang }}">
{{ .Language.LanguageName }}
</a>
{{ end }}
{{ end }}
{{ end }}
</div>
</div>
@@ -14,8 +14,8 @@ Author: Jeroen van der Schee
{{ end }}
</div>
{{ if gt (len .images) 1 }}
<button class="carousel-prev" aria-label="Précédent">&#10094;</button>
<button class="carousel-next" aria-label="Suivant">&#10095;</button>
<button class="carousel-prev" aria-label="{{ i18n "previous" }}">&#10094;</button>
<button class="carousel-next" aria-label="{{ i18n "next" }}">&#10095;</button>
<div class="carousel-dots">
{{ range $i, $_ := .images }}
@@ -84,7 +84,7 @@
}
})
.catch(() => {
showResult('Erreur réseau, veuillez ré-essayer.', false);
showResult({{ i18n "networkError" }}, false);
});
});
});
@@ -1,183 +0,0 @@
/**
* Language Preference Manager
* Stores user's language selection in localStorage and auto-redirects on subsequent visits
* Supports automatic language detection via VisitorAPI
*/
(function() {
'use strict';
// Configuration
const STORAGE_KEY = 'hugo_preferred_language';
const SESSION_KEY = 'language_redirected';
// Get VisitorAPI project ID from Hugo config (injected via template)
const VISITOR_API_PID = window.HUGO_VISITOR_API_PID || null;
/**
* Initialize the language preference system
*/
function init() {
// Check and redirect on page load (before DOM ready for faster redirect)
checkAndRedirect();
// Attach click handlers after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', attachLanguageSwitchers);
} else {
attachLanguageSwitchers();
}
}
/**
* Check if we should redirect to user's preferred language
*/
function checkAndRedirect() {
try {
const preferredLang = getStoredLanguage();
const currentLang = getCurrentLanguage();
// If user has a stored preference and it matches current language, we're good
if (preferredLang && preferredLang === currentLang) {
return;
}
// If user has a stored preference, use it
if (preferredLang && preferredLang !== currentLang && isValidLanguage(preferredLang)) {
console.log('Language preference: Redirecting to stored preference:', preferredLang);
redirectToLanguage(preferredLang);
return;
}
} catch (e) {
// Silently fail if localStorage/sessionStorage is unavailable
console.warn('Language preference: Storage unavailable', e);
}
}
/**
* Attach click event listeners to all language switch links
*/
function attachLanguageSwitchers() {
const links = document.querySelectorAll('.language-switch-link');
links.forEach(function(link) {
link.addEventListener('click', handleLanguageClick);
});
}
/**
* Handle click on language switch link
* @param {Event} e - Click event
*/
function handleLanguageClick(e) {
const lang = e.currentTarget.getAttribute('data-lang');
if (lang) {
try {
setStoredLanguage(lang);
console.log('Language preference: Stored user preference:', lang);
} catch (e) {
console.warn('Language preference: Could not save preference', e);
}
}
// Let the link navigate normally (don't prevent default)
}
/**
* Get the stored language preference from localStorage
* @returns {string|null} Language code or null
*/
function getStoredLanguage() {
try {
return localStorage.getItem(STORAGE_KEY);
} catch (e) {
return null;
}
}
/**
* Store the language preference in localStorage
* @param {string} lang - Language code to store
*/
function setStoredLanguage(lang) {
try {
localStorage.setItem(STORAGE_KEY, lang);
} catch (e) {
throw e;
}
}
/**
* Get the current language from the URL path
* @returns {string} Current language code (e.g., 'en', 'zh-cn')
*/
function getCurrentLanguage() {
const path = window.location.pathname;
// Match language code pattern: /en/ or /zh-cn/ at start of path
const match = path.match(/^\/([a-z]{2}(-[a-z]{2})?)\//i);
if (match) {
return match[1].toLowerCase();
}
// If no language in path, assume default language (root path)
// This handles cases like "/" which might be the default language
return getDefaultLanguage();
}
/**
* Get default language from HTML lang attribute or fallback
* @returns {string} Default language code
*/
function getDefaultLanguage() {
const htmlLang = document.documentElement.getAttribute('lang');
if (htmlLang) {
// Extract primary language code (e.g., 'en-US' -> 'en')
return htmlLang.split('-')[0].toLowerCase();
}
return 'en'; // Fallback
}
/**
* Check if a language code is valid by looking for available language links
* @param {string} lang - Language code to validate
* @returns {boolean} True if valid
*/
function isValidLanguage(lang) {
// Check if any language switch link exists for this language
const link = document.querySelector('.language-switch-link[data-lang="' + lang + '"]');
return link !== null;
}
/**
* Redirect to a specific language, preserving the current path
* @param {string} targetLang - Target language code
*/
function redirectToLanguage(targetLang) {
const currentPath = window.location.pathname;
const search = window.location.search;
const hash = window.location.hash;
let newPath;
// Check if current path has a language prefix
if (currentPath.match(/^\/[a-z]{2}(-[a-z]{2})?\//i)) {
// Replace existing language: /en/about -> /zh-cn/about
newPath = currentPath.replace(
/^\/[a-z]{2}(-[a-z]{2})?\//i,
'/' + targetLang + '/'
);
} else {
// Add language prefix: /about -> /zh-cn/about
newPath = '/' + targetLang + currentPath;
}
// Perform redirect
window.location.href = newPath + search + hash;
}
// Initialize when script loads
init();
})();