Add language switcher and language detection
This commit is contained in:
@@ -3,26 +3,7 @@ baseURL = "/"
|
|||||||
title = ""
|
title = ""
|
||||||
theme = "chill-theme"
|
theme = "chill-theme"
|
||||||
defaultContentLanguage = "fr"
|
defaultContentLanguage = "fr"
|
||||||
disableLanguages = ["en", "nl"]
|
defaultContentLanguageInSubdir = false
|
||||||
|
|
||||||
# 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"
|
|
||||||
|
|
||||||
# Required Features
|
# Required Features
|
||||||
enableEmoji = true # Enable emoji support
|
enableEmoji = true # Enable emoji support
|
||||||
@@ -69,14 +50,13 @@ pagination = { pagerSize = 6, path = "page" }
|
|||||||
category = 'categories'
|
category = 'categories'
|
||||||
tag = 'tags'
|
tag = 'tags'
|
||||||
|
|
||||||
# Theme Parameters
|
# Theme Parameters shared by every language.
|
||||||
|
# Anything language specific lives under [languages.<lang>.params] below.
|
||||||
[params]
|
[params]
|
||||||
description = "Logiciel libre d'accompagnement social"
|
|
||||||
author = "Champs-Libres"
|
author = "Champs-Libres"
|
||||||
logo = "/images/logo-darkmode.png" # Path to your logo
|
logo = "/images/logo-darkmode.png" # Path to your logo
|
||||||
customCSS = ["css/custom.css"]
|
customCSS = ["css/custom.css"]
|
||||||
|
|
||||||
|
|
||||||
# Header Configuration
|
# Header Configuration
|
||||||
[params.header]
|
[params.header]
|
||||||
background = "header backdrop-blur-sm"
|
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"
|
menu.linkClass = "text-white hover:text-primary font-bold transition duration-200"
|
||||||
|
|
||||||
[params.header.buttons.demo]
|
[params.header.buttons.demo]
|
||||||
text = "Démo"
|
|
||||||
url = "http://demo.chill.social/"
|
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"
|
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
|
open_tab = true
|
||||||
@@ -93,60 +72,80 @@ pagination = { pagerSize = 6, path = "page" }
|
|||||||
[params.header.logo]
|
[params.header.logo]
|
||||||
src = "/images/logo-darkmode.png"
|
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)
|
# Global CTA Configuration (optional)
|
||||||
[params.cta]
|
[params.cta]
|
||||||
enable = true
|
enable = true
|
||||||
title = "Prêt·e à commencer ?"
|
|
||||||
description = "Testez Chill dès aujourd'hui et découvrez comment il peut transformer votre gestion sociale."
|
|
||||||
|
|
||||||
# Gradient background
|
# Gradient background
|
||||||
gradient_from = "#0d242e" # Blue-600
|
gradient_from = "#0d242e" # Blue-600
|
||||||
gradient_to = "#23627e" # Purple-600
|
gradient_to = "#23627e" # Purple-600
|
||||||
gradient_angle = 30
|
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)
|
# Social Media Links (optional)
|
||||||
[params.social]
|
[params.social]
|
||||||
gitlab = "https://gitlab.com/Chill-Projet/chill-bundles"
|
gitlab = "https://gitlab.com/Chill-Projet/chill-bundles"
|
||||||
# Navigation Menu
|
|
||||||
[menu]
|
|
||||||
[[menu.main]]
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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"
|
name = "Fonctionnalités"
|
||||||
url = "/features"
|
url = "/features"
|
||||||
weight = 1
|
weight = 1
|
||||||
[[menu.main]]
|
[[languages.fr.menu.main]]
|
||||||
name = "Secteur d'activités"
|
name = "Secteur d'activités"
|
||||||
weight = 2
|
|
||||||
url = "/sector"
|
url = "/sector"
|
||||||
[menu.main.params]
|
weight = 2
|
||||||
|
[languages.fr.menu.main.params]
|
||||||
has_submenu = true
|
has_submenu = true
|
||||||
submenu = [
|
submenu = [
|
||||||
{ name = "Grandes Collectivités", url = "/sector/collectivities" },
|
{ name = "Grandes Collectivités", url = "/sector/collectivities" },
|
||||||
{ name = "Services et associations", url = "/sector/associations" },
|
{ name = "Services et associations", url = "/sector/associations" },
|
||||||
]
|
]
|
||||||
[[menu.main]]
|
[[languages.fr.menu.main]]
|
||||||
name = "Tarification"
|
name = "Tarification"
|
||||||
url = "/pricing"
|
url = "/pricing"
|
||||||
weight = 3
|
weight = 3
|
||||||
[[menu.main]]
|
[[languages.fr.menu.main]]
|
||||||
name = "Ressources"
|
name = "Ressources"
|
||||||
weight = 4
|
weight = 4
|
||||||
[menu.main.params]
|
[languages.fr.menu.main.params]
|
||||||
has_submenu = true
|
has_submenu = true
|
||||||
submenu = [
|
submenu = [
|
||||||
# { name = "Blog", url = "/resources/blog" },
|
# { name = "Blog", url = "/resources/blog" },
|
||||||
@@ -156,32 +155,64 @@ pagination = { pagerSize = 6, path = "page" }
|
|||||||
{ name = "Documentation technique", url = "/resources/technical-documentation" },
|
{ name = "Documentation technique", url = "/resources/technical-documentation" },
|
||||||
{ name = "FAQ", url = "/resources/faq" }
|
{ name = "FAQ", url = "/resources/faq" }
|
||||||
]
|
]
|
||||||
[[menu.main]]
|
[[languages.fr.menu.main]]
|
||||||
name = "Nous contacter"
|
name = "Nous contacter"
|
||||||
url = "/contact"
|
url = "/contact"
|
||||||
weight = 5
|
weight = 5
|
||||||
[[menu.footer_column]]
|
|
||||||
|
[[languages.fr.menu.footer_column]]
|
||||||
name = "Champs-Libres"
|
name = "Champs-Libres"
|
||||||
url = "https://www.champs-libres.coop/"
|
url = "https://www.champs-libres.coop/"
|
||||||
weight = 1
|
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]
|
||||||
[[languages.en.menu.main]]
|
[[languages.en.menu.main]]
|
||||||
name = "Features"
|
name = "Features"
|
||||||
url = "/features"
|
url = "/en/features"
|
||||||
weight = 1
|
weight = 1
|
||||||
[[languages.en.menu.main]]
|
[[languages.en.menu.main]]
|
||||||
name = "Installation"
|
name = "Sectors"
|
||||||
|
url = "/en/sector"
|
||||||
weight = 2
|
weight = 2
|
||||||
[languages.en.menu.main.params]
|
[languages.en.menu.main.params]
|
||||||
has_submenu = true
|
has_submenu = true
|
||||||
submenu = [
|
submenu = [
|
||||||
{ name = "By Champs Libres", url = "/install/hosted" },
|
{ name = "Large local authorities", url = "/en/sector/collectivities" },
|
||||||
{ name = "Self-hosted", url = "/install/on-premise" }
|
{ name = "Services and non-profits", url = "/en/sector/associations" },
|
||||||
]
|
]
|
||||||
[[languages.en.menu.main]]
|
[[languages.en.menu.main]]
|
||||||
name = "Pricing"
|
name = "Pricing"
|
||||||
url = "/pricing"
|
url = "/en/pricing"
|
||||||
weight = 3
|
weight = 3
|
||||||
[[languages.en.menu.main]]
|
[[languages.en.menu.main]]
|
||||||
name = "Resources"
|
name = "Resources"
|
||||||
@@ -189,58 +220,91 @@ pagination = { pagerSize = 6, path = "page" }
|
|||||||
[languages.en.menu.main.params]
|
[languages.en.menu.main.params]
|
||||||
has_submenu = true
|
has_submenu = true
|
||||||
submenu = [
|
submenu = [
|
||||||
# { name = "Blog", url = "/resources/blog" },
|
# { name = "Blog", url = "/en/resources/blog" },
|
||||||
{ name = "Technical Documentation", url = "/resources/technical-documentation" },
|
{ name = "Manuals", url = "/en/resources/manuals" },
|
||||||
{ name = "Manuals", url = "/resources/manuals" },
|
{ name = "Tutorials", url = "/en/resources/tutorials" },
|
||||||
{ name = "Tutorials", url = "/resources/tutorials" },
|
{ name = "Installation", url = "/en/resources/installation" },
|
||||||
{ name = "FAQ", url = "/resources/faq" }
|
{ name = "Technical documentation", url = "/en/resources/technical-documentation" },
|
||||||
|
{ name = "FAQ", url = "/en/resources/faq" }
|
||||||
]
|
]
|
||||||
[[languages.en.menu.main]]
|
[[languages.en.menu.main]]
|
||||||
name = "Contact Us"
|
name = "Contact us"
|
||||||
url = "/contact"
|
url = "/en/contact"
|
||||||
weight = 5
|
weight = 5
|
||||||
|
|
||||||
[[languages.en.menu.footer_column_1]]
|
[[languages.en.menu.footer_column]]
|
||||||
name = "Download the CHILL brochure"
|
name = "Champs-Libres"
|
||||||
url = "/pdf/Brochure_Chill.pdf"
|
url = "https://www.champs-libres.coop/"
|
||||||
weight = 1
|
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]
|
||||||
[[languages.nl.menu.main]]
|
[[languages.nl.menu.main]]
|
||||||
name = "Functies"
|
name = "Functionaliteiten"
|
||||||
url = "/features"
|
url = "/nl/features"
|
||||||
weight = 1
|
weight = 1
|
||||||
[[languages.nl.menu.main]]
|
[[languages.nl.menu.main]]
|
||||||
name = "Installatie"
|
name = "Sectoren"
|
||||||
|
url = "/nl/sector"
|
||||||
weight = 2
|
weight = 2
|
||||||
[languages.nl.menu.main.params]
|
[languages.nl.menu.main.params]
|
||||||
has_submenu = true
|
has_submenu = true
|
||||||
submenu = [
|
submenu = [
|
||||||
{ name = "Door Champs Libres", url = "/install/hosted" },
|
{ name = "Grote lokale besturen", url = "/nl/sector/collectivities" },
|
||||||
{ name = "Zelf gehost", url = "/install/on-premise" }
|
{ name = "Diensten en verenigingen", url = "/nl/sector/associations" },
|
||||||
]
|
]
|
||||||
[[languages.nl.menu.main]]
|
[[languages.nl.menu.main]]
|
||||||
name = "Prijsstelling"
|
name = "Tarieven"
|
||||||
url = "/pricing"
|
url = "/nl/pricing"
|
||||||
weight = 3
|
weight = 3
|
||||||
[[languages.nl.menu.main]]
|
[[languages.nl.menu.main]]
|
||||||
name = "Bronnen"
|
name = "Hulpbronnen"
|
||||||
weight = 4
|
weight = 4
|
||||||
[languages.nl.menu.main.params]
|
[languages.nl.menu.main.params]
|
||||||
has_submenu = true
|
has_submenu = true
|
||||||
submenu = [
|
submenu = [
|
||||||
{ name = "Technische Documentatie", url = "/resources/technical-documentation" },
|
# { name = "Blog", url = "/nl/resources/blog" },
|
||||||
{ name = "Handleidingen", url = "/resources/manuals" },
|
{ name = "Handleidingen", url = "/nl/resources/manuals" },
|
||||||
{ name = "Tutorials", url = "/resources/tutorials" },
|
{ name = "Tutorials", url = "/nl/resources/tutorials" },
|
||||||
{ name = "FAQ", url = "/resources/faq" },
|
{ name = "Installatie", url = "/nl/resources/installation" },
|
||||||
# { name = "Blog", url = "/resources/blog" }
|
{ name = "Technische documentatie", url = "/nl/resources/technical-documentation" },
|
||||||
|
{ name = "FAQ", url = "/nl/resources/faq" }
|
||||||
]
|
]
|
||||||
[[languages.nl.menu.main]]
|
[[languages.nl.menu.main]]
|
||||||
name = "Contacteer Ons"
|
name = "Contacteer ons"
|
||||||
url = "/contact"
|
url = "/nl/contact"
|
||||||
weight = 5
|
weight = 5
|
||||||
|
|
||||||
[[languages.nl.menu.footer_column_1]]
|
[[languages.nl.menu.footer_column]]
|
||||||
name = "Download de CHILL-brochure"
|
name = "Champs-Libres"
|
||||||
url = "/pdf/Brochure_Chill.pdf"
|
url = "https://www.champs-libres.coop/"
|
||||||
weight = 1
|
weight = 1
|
||||||
@@ -53,6 +53,19 @@
|
|||||||
<!-- Canonical URL -->
|
<!-- Canonical URL -->
|
||||||
<link rel="canonical" href="{{ .Permalink }}">
|
<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 -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
@@ -91,17 +104,6 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
{{ partial "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 -->
|
<!-- Mobile Menu Script -->
|
||||||
<script>
|
<script>
|
||||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
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="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">
|
<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">
|
<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>
|
</p>
|
||||||
<a href="https://www.champs-libres.coop/" class="text-sm text-gray-600 hover:text-primary-600" target="_blank" rel="noopener noreferrer">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -89,21 +89,7 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<!-- Mobile Language Switcher -->
|
<!-- Mobile Language Switcher -->
|
||||||
{{ if hugo.IsMultilingual }}
|
{{ partial "language-switcher-mobile" . }}
|
||||||
<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 }}
|
|
||||||
|
|
||||||
{{ if not $headerConfig.hideButtons }}
|
{{ if not $headerConfig.hideButtons }}
|
||||||
<div class="pt-4 space-y-4">
|
<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 }}
|
{{ if hugo.IsMultilingual }}
|
||||||
|
{{ $page := . }}
|
||||||
{{ $headerConfig := .Site.Params.header }}
|
{{ $headerConfig := .Site.Params.header }}
|
||||||
{{ $dropdownConfig := $headerConfig.menu.dropdown }}
|
{{ $dropdownConfig := $headerConfig.menu.dropdown }}
|
||||||
<div class="relative group">
|
<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 }}
|
{{ .Site.Language.LanguageName }}
|
||||||
<svg class="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
@@ -10,11 +18,15 @@
|
|||||||
</button>
|
</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="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" }}">
|
<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 }}
|
{{ range hugo.Sites }}
|
||||||
{{ if ne $.Site.Language.Lang .Lang }}
|
{{ $lang := .Language.Lang }}
|
||||||
{{ $langURL := printf "/%s/" .Lang }}
|
{{ if ne $.Site.Language.Lang $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 }}">
|
{{ $target := .Home }}
|
||||||
{{ .LanguageName }}
|
{{ 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>
|
</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ Author: Jeroen van der Schee
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ if gt (len .images) 1 }}
|
{{ if gt (len .images) 1 }}
|
||||||
<button class="carousel-prev" aria-label="Précédent">❮</button>
|
<button class="carousel-prev" aria-label="{{ i18n "previous" }}">❮</button>
|
||||||
<button class="carousel-next" aria-label="Suivant">❯</button>
|
<button class="carousel-next" aria-label="{{ i18n "next" }}">❯</button>
|
||||||
|
|
||||||
<div class="carousel-dots">
|
<div class="carousel-dots">
|
||||||
{{ range $i, $_ := .images }}
|
{{ range $i, $_ := .images }}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.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();
|
|
||||||
|
|
||||||
})();
|
|
||||||
Reference in New Issue
Block a user