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 f61bc81..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() @@ -20,31 +21,32 @@ const shareUrl = $derived(encodeURIComponent(window.location.href)) +
- +
{#if copySuccess} - Lien copié ! + {t('link_copied')} {/if}
@@ -73,23 +75,23 @@
- +
@@ -97,7 +99,7 @@ {#if data.related?.length}
-

Nos recommandations

+

{t('related')}

{#each data.related as rec} @@ -110,14 +112,20 @@
{/if} -
+