From 517143fe6073393b8a9851c7f5051e133699d340 Mon Sep 17 00:00:00 2001 From: isUnknown Date: Thu, 12 Mar 2026 11:57:59 +0100 Subject: [PATCH] =?UTF-8?q?Feat:=20int=C3=A9gration=20multilingue=20FR/EN?= =?UTF-8?q?=20(i18n)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout de src/i18n/index.js : dictionnaire centralisé + fonction t(key, vars) - Ajout de LanguageSwitcher.svelte : toggle FR/EN avec persistance localStorage - Router : normalizePath strip /en/, apiPrefix() pour les fetches, détection langue (URL > localStorage > navigator) - Tous les composants (Header, Menu, Footer, Article, Blog, Play) migrent vers t() depuis @i18n - Blog : navigation interne (fetch, history, getSlugFromUrl) locale-aware Co-Authored-By: Claude Sonnet 4.6 --- src/App.svelte | 2 + src/components/layout/Footer.svelte | 27 ++++++------ src/components/layout/Header.svelte | 3 +- src/components/layout/Menu.svelte | 13 +----- src/components/ui/LanguageSwitcher.svelte | 28 +++++++++++++ src/i18n/index.js | 51 +++++++++++++++++++++++ src/router/index.js | 31 ++++++++++++-- src/views/Article.svelte | 33 ++++++++------- src/views/Blog.svelte | 23 ++++++---- src/views/Play.svelte | 7 ++-- vite.config.js | 3 +- 11 files changed, 162 insertions(+), 59 deletions(-) create mode 100644 src/components/ui/LanguageSwitcher.svelte create mode 100644 src/i18n/index.js diff --git a/src/App.svelte b/src/App.svelte index fa512e0..7118944 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -5,6 +5,7 @@ import Header from '@components/layout/Header.svelte' import Cursor from '@components/layout/Cursor.svelte' + import LanguageSwitcher from '@components/ui/LanguageSwitcher.svelte' import Home from '@views/Home.svelte' import About from '@views/About.svelte' @@ -94,6 +95,7 @@ {/each} + diff --git a/src/i18n/index.js b/src/i18n/index.js new file mode 100644 index 0000000..aa73357 --- /dev/null +++ b/src/i18n/index.js @@ -0,0 +1,51 @@ +import { locale } from '@state/locale.svelte' + +const dict = { + // Article + 'published_on': { fr: 'Publié le', en: 'Published on' }, + 'link_copied': { fr: 'Lien copié !', en: 'Link copied!' }, + 'copy_link': { fr: 'Copier le lien', en: 'Copy link' }, + 'share_article': { fr: 'Partager cet article', en: 'Share this article' }, + 'related': { fr: 'Nos recommandations', en: 'Our recommendations' }, + 'share_whatsapp': { fr: 'Partager sur WhatsApp', en: 'Share on WhatsApp' }, + 'share_x': { fr: 'Partager sur X', en: 'Share on X' }, + 'share_facebook': { fr: 'Partager sur Facebook', en: 'Share on Facebook' }, + 'share_linkedin': { fr: 'Partager sur LinkedIn', en: 'Share on LinkedIn' }, + // Blog + 'loading': { fr: 'Chargement…', en: 'Loading…' }, + 'read_article': { fr: "Lire l'article", en: 'Read article' }, + // Play + 'play': { fr: 'Jouer', en: 'Play' }, + 'coming_soon': { fr: 'Coming soon', en: 'Coming soon' }, + // Header + 'close_menu': { fr: 'Fermer le menu', en: 'Close menu' }, + 'open_menu': { fr: 'Ouvrir le menu', en: 'Open menu' }, + // Footer + 'location': { fr: 'Adresse', en: 'Location' }, + 'contact': { fr: 'Contact', en: 'Contact' }, + 'follow_us': { fr: 'Réseaux', en: 'Follow us' }, + 'newsletter_heading': { fr: 'Inscrivez-vous à notre newsletter !', en: 'Subscribe to our newsletter!' }, + 'newsletter_placeholder': { fr: 'Votre email', en: 'Enter your email' }, + 'newsletter_submit': { fr: "S'inscrire", en: 'Subscribe' }, + 'newsletter_success': { fr: 'Merci pour votre inscription !', en: 'Thank you for subscribing!' }, + 'newsletter_error': { fr: 'Une erreur est survenue.', en: 'An error occurred.' }, + 'copyright': { fr: 'World Game © {year}. Tous droits réservés.', en: 'World Game © {year}. All rights reserved.' }, + 'legal': { fr: 'Mentions légales', en: 'Legal notice' }, + 'cookies': { fr: 'Préférences cookies', en: 'Cookie preferences' }, + 'privacy': { fr: 'Confidentialité', en: 'Privacy' }, + // Menu + 'menu': { fr: 'MENU', en: 'MENU' }, + 'connect': { fr: 'CONNECT', en: 'CONNECT' }, + 'address': { fr: 'ADRESSE', en: 'LOCATION' }, + 'mail': { fr: 'MAIL', en: 'MAIL' }, + 'socials': { fr: 'RÉSEAUX', en: 'SOCIALS' }, +} + +export function t(key, vars = {}) { + const lang = locale.current + let str = dict[key]?.[lang] ?? dict[key]?.fr ?? key + for (const [k, v] of Object.entries(vars)) { + str = str.replace(`{${k}}`, v) + } + return str +} diff --git a/src/router/index.js b/src/router/index.js index d035676..cb54766 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -5,7 +5,12 @@ import { locale } from "@state/locale.svelte"; let siteInitialized = false; function normalizePath(path) { - return path === "/" ? "/home" : path; + const stripped = path.replace(/^\/en(\/|$)/, '$1') || '/'; + return stripped === '/' ? '/home' : stripped; +} + +function apiPrefix() { + return locale.current === 'en' ? '/en' : ''; } /** @@ -47,8 +52,7 @@ async function loadSlide(path) { } try { - // Fetch the actual slide path (parent), not the sub-page - const response = await fetch(`${slidePath}.json`); + const response = await fetch(`${apiPrefix()}${slidePath}.json`); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); @@ -76,7 +80,10 @@ export function slideTo(path, { skipHistory = false } = {}) { path = normalizePath(path); if (!skipHistory) { - history.pushState({}, "", path === "/home" ? "/" : path); + const historyPath = locale.current === 'en' + ? (path === '/home' ? '/en' : `/en${path}`) + : (path === '/home' ? '/' : path); + history.pushState({}, '', historyPath); } const idx = findSlideIndex(path); @@ -94,6 +101,22 @@ export function slideTo(path, { skipHistory = false } = {}) { } export async function initRouter() { + // Language detection: URL prefix > localStorage > navigator + const hasEnPrefix = window.location.pathname.startsWith('/en'); + if (hasEnPrefix) { + locale.setLanguage('en'); + localStorage.setItem('wg_lang', 'en'); + } else if (!localStorage.getItem('wg_lang')) { + const navLang = navigator.language || navigator.languages?.[0] || 'fr'; + if (navLang.startsWith('en')) { + window.location.replace('/en' + window.location.pathname); + return; + } + } else if (localStorage.getItem('wg_lang') === 'en') { + window.location.replace('/en' + window.location.pathname); + return; + } + const initialPath = normalizePath(window.location.pathname); await loadSlide(initialPath); diff --git a/src/views/Article.svelte b/src/views/Article.svelte index 3abeee5..79b7d29 100644 --- a/src/views/Article.svelte +++ b/src/views/Article.svelte @@ -4,6 +4,7 @@ * Reçoit les données article via props, pas via le slide system. */ import Footer from '@components/layout/Footer.svelte' + import { t } from '@i18n' let { data, onBack } = $props() @@ -25,27 +26,27 @@
- +
{#if copySuccess} - Lien copié ! + {t('link_copied')} {/if}
@@ -74,23 +75,23 @@
- +
@@ -98,7 +99,7 @@ {#if data.related?.length}
-

Nos recommandations

+

{t('related')}

{#each data.related as rec} diff --git a/src/views/Blog.svelte b/src/views/Blog.svelte index ebaf637..92161c0 100644 --- a/src/views/Blog.svelte +++ b/src/views/Blog.svelte @@ -1,8 +1,10 @@