From 4e9746c308ccc3c1448dfcc904e650248b40bb09 Mon Sep 17 00:00:00 2001 From: stanig2106 Date: Sun, 5 Apr 2026 22:43:44 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20ajoute=20le=20syst=C3=A8me=20i18n=20fro?= =?UTF-8?q?ntend=20(FR/EN)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Crée js/i18n/i18n.js : module central avec t(), getLocale(), setLocale() - Crée js/i18n/locales/fr.json et en.json : traductions FR et EN - Crée js/i18n/applyTranslations.js : applique les data-i18n au DOM - Crée js/i18n/langSwitcher.js : sélecteur de langue (boutons FR/EN) - Modifie index.html : ajoute les attributs data-i18n sur tous les textes - Modifie js/main.js : intègre i18n et relance l'application des traductions - Modifie css/form.css : styles pour le sélecteur de langue - Persistance de la langue dans localStorage (clé cv_lang) --- css/form.css | 38 ++++++++++++++++++++++++++++++++++++ index.html | 23 ++++++++++++++-------- js/i18n/applyTranslations.js | 15 ++++++++++++++ js/i18n/i18n.js | 35 +++++++++++++++++++++++++++++++++ js/i18n/langSwitcher.js | 17 ++++++++++++++++ js/i18n/locales/en.json | 20 +++++++++++++++++++ js/i18n/locales/fr.json | 20 +++++++++++++++++++ js/main.js | 14 ++++++++++++- 8 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 js/i18n/applyTranslations.js create mode 100644 js/i18n/i18n.js create mode 100644 js/i18n/langSwitcher.js create mode 100644 js/i18n/locales/en.json create mode 100644 js/i18n/locales/fr.json diff --git a/css/form.css b/css/form.css index bc9398e..61ae55a 100644 --- a/css/form.css +++ b/css/form.css @@ -121,3 +121,41 @@ input:focus { padding-top: 16px; font-size: 0.95rem; } + +/* ── Header top row ── */ +.header-top { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + margin-bottom: 10px; +} + +/* ── Language switcher ── */ +.lang-switcher { + display: flex; + gap: 6px; +} + +.lang-btn { + background: transparent; + border: 1px solid #2a2a4a; + color: #a0a0b0; + padding: 4px 10px; + border-radius: 4px; + font-size: 0.78rem; + font-weight: 600; + cursor: pointer; + letter-spacing: 0.05em; + transition: border-color 0.2s ease, color 0.2s ease; +} + +.lang-btn:hover { + border-color: #e94560; + color: #e0e0e0; +} + +.lang-btn.active { + border-color: #e94560; + color: #e94560; +} diff --git a/index.html b/index.html index 06d87a8..e1accf7 100644 --- a/index.html +++ b/index.html @@ -10,8 +10,11 @@
-

CV Generator

-

Remplissez vos informations pour générer votre CV

+
+

CV Generator

+
+
+

Remplissez vos informations pour générer votre CV

@@ -20,11 +23,12 @@
- +
- +
- +
- + - +

-

CV Generator © 2026

+

CV Generator © 2026

diff --git a/js/i18n/applyTranslations.js b/js/i18n/applyTranslations.js new file mode 100644 index 0000000..f4d861c --- /dev/null +++ b/js/i18n/applyTranslations.js @@ -0,0 +1,15 @@ +import { t } from './i18n.js'; + +export function applyTranslations() { + document.querySelectorAll('[data-i18n]').forEach(el => { + el.textContent = t(el.dataset.i18n); + }); + + document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { + el.placeholder = t(el.dataset.i18nPlaceholder); + }); + + document.querySelectorAll('[data-i18n-title]').forEach(el => { + el.title = t(el.dataset.i18nTitle); + }); +} diff --git a/js/i18n/i18n.js b/js/i18n/i18n.js new file mode 100644 index 0000000..33ae09c --- /dev/null +++ b/js/i18n/i18n.js @@ -0,0 +1,35 @@ +import fr from './locales/fr.json' assert { type: 'json' }; +import en from './locales/en.json' assert { type: 'json' }; + +const LOCALES = { fr, en }; +const SUPPORTED = Object.keys(LOCALES); +const STORAGE_KEY = 'cv_lang'; + +let currentLocale = localStorage.getItem(STORAGE_KEY) || 'fr'; +if (!SUPPORTED.includes(currentLocale)) currentLocale = 'fr'; + +function resolve(obj, path) { + return path.split('.').reduce((acc, key) => acc?.[key], obj); +} + +export function t(key, params = {}) { + const messages = LOCALES[currentLocale] ?? LOCALES.fr; + let str = resolve(messages, key) ?? resolve(LOCALES.fr, key) ?? key; + return str.replace(/\{(\w+)\}/g, (_, k) => params[k] ?? `{${k}}`); +} + +export function getLocale() { + return currentLocale; +} + +export function setLocale(lang) { + if (!SUPPORTED.includes(lang)) return; + currentLocale = lang; + localStorage.setItem(STORAGE_KEY, lang); + document.documentElement.lang = lang; + document.dispatchEvent(new CustomEvent('localechange', { detail: { lang } })); +} + +export function getSupportedLocales() { + return SUPPORTED; +} diff --git a/js/i18n/langSwitcher.js b/js/i18n/langSwitcher.js new file mode 100644 index 0000000..f0475fe --- /dev/null +++ b/js/i18n/langSwitcher.js @@ -0,0 +1,17 @@ +import { getLocale, setLocale, getSupportedLocales } from './i18n.js'; + +export function initLangSwitcher() { + const switcher = document.getElementById('lang-switcher'); + if (!switcher) return; + + const supported = getSupportedLocales(); + switcher.innerHTML = supported + .map(lang => ``) + .join(''); + + switcher.addEventListener('click', (e) => { + const btn = e.target.closest('.lang-btn'); + if (!btn) return; + setLocale(btn.dataset.lang); + }); +} diff --git a/js/i18n/locales/en.json b/js/i18n/locales/en.json new file mode 100644 index 0000000..cc6b36e --- /dev/null +++ b/js/i18n/locales/en.json @@ -0,0 +1,20 @@ +{ + "header": { + "title": "CV Generator", + "subtitle": "Fill in your details to generate your CV" + }, + "form": { + "nom": "Last name", + "nom_placeholder": "Smith", + "prenom": "First name", + "prenom_placeholder": "John", + "email": "Email", + "email_placeholder": "john.smith@email.com", + "titre": "Job title", + "titre_placeholder": "Full Stack Developer", + "submit": "Generate my CV" + }, + "success": "CV ready to be generated for {prenom} {nom}!", + "footer": "CV Generator © 2026", + "lang_switcher_label": "Language" +} diff --git a/js/i18n/locales/fr.json b/js/i18n/locales/fr.json new file mode 100644 index 0000000..c9665a8 --- /dev/null +++ b/js/i18n/locales/fr.json @@ -0,0 +1,20 @@ +{ + "header": { + "title": "CV Generator", + "subtitle": "Remplissez vos informations pour générer votre CV" + }, + "form": { + "nom": "Nom", + "nom_placeholder": "Dupont", + "prenom": "Prénom", + "prenom_placeholder": "Jean", + "email": "Email", + "email_placeholder": "jean.dupont@email.com", + "titre": "Titre du poste", + "titre_placeholder": "Développeur Full Stack", + "submit": "Générer mon CV" + }, + "success": "CV prêt à être généré pour {prenom} {nom} !", + "footer": "CV Generator © 2026", + "lang_switcher_label": "Langue" +} diff --git a/js/main.js b/js/main.js index de8c496..1a21bf8 100644 --- a/js/main.js +++ b/js/main.js @@ -3,6 +3,9 @@ import { collectCompetences, renderCompetences } from './sections/competences.js import { collectFormation, renderFormation } from './sections/formation.js'; import { collectExperience, renderExperience } from './sections/experience.js'; import { collectHobbies, renderHobbies } from './sections/hobbies.js'; +import { t, getLocale } from './i18n/i18n.js'; +import { applyTranslations } from './i18n/applyTranslations.js'; +import { initLangSwitcher } from './i18n/langSwitcher.js'; function generateCV() { const identite = collectIdentite(); @@ -19,12 +22,21 @@ function generateCV() { const success = document.getElementById('success'); if (success) { - success.textContent = `CV prêt à être généré pour ${identite.prenom} ${identite.nom} !`; + success.textContent = t('success', { prenom: identite.prenom, nom: identite.nom }); success.style.display = 'block'; } } document.addEventListener('DOMContentLoaded', () => { + document.documentElement.lang = getLocale(); + applyTranslations(); + initLangSwitcher(); + + document.addEventListener('localechange', () => { + applyTranslations(); + initLangSwitcher(); + }); + document.getElementById('cv-form')?.addEventListener('submit', (e) => { e.preventDefault(); generateCV();