feat: ajoute le système i18n frontend (FR/EN)

- 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)
This commit is contained in:
stanig2106
2026-04-05 22:43:44 +01:00
parent f0cf9a605f
commit 4e9746c308
8 changed files with 173 additions and 9 deletions
+15
View File
@@ -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);
});
}
+35
View File
@@ -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;
}
+17
View File
@@ -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 => `<button class="lang-btn${lang === getLocale() ? ' active' : ''}" data-lang="${lang}">${lang.toUpperCase()}</button>`)
.join('');
switcher.addEventListener('click', (e) => {
const btn = e.target.closest('.lang-btn');
if (!btn) return;
setLocale(btn.dataset.lang);
});
}
+20
View File
@@ -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"
}
+20
View File
@@ -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"
}
+13 -1
View File
@@ -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();