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

This commit is contained in:
stanig2106
2026-04-05 21:54:22 +01:00
parent f0cf9a605f
commit bfb1ad9163
6 changed files with 177 additions and 11 deletions
+40 -1
View File
@@ -31,11 +31,50 @@ header {
margin-bottom: 32px; margin-bottom: 32px;
} }
.header-top {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 10px;
}
.lang-switcher {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.8rem;
}
.lang-sep {
color: #4a4a6a;
}
.lang-btn {
background: none;
border: none;
color: #4a4a6a;
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
padding: 2px 4px;
letter-spacing: 0.05em;
transition: color 0.2s ease;
}
.lang-btn:hover {
color: #e94560;
}
.lang-btn.active {
color: #e94560;
}
h1 { h1 {
font-size: 2.2rem; font-size: 2.2rem;
font-weight: 700; font-weight: 700;
color: #e94560; color: #e94560;
margin-bottom: 10px; margin-bottom: 0;
} }
header p { header p {
+19 -8
View File
@@ -10,8 +10,15 @@
<body> <body>
<div class="container"> <div class="container">
<header> <header>
<h1>CV Generator</h1> <div class="header-top">
<p>Remplissez vos informations pour générer votre CV</p> <h1 data-i18n="header.title">CV Generator</h1>
<div class="lang-switcher">
<button class="lang-btn" data-lang="fr">FR</button>
<span class="lang-sep">|</span>
<button class="lang-btn" data-lang="en">EN</button>
</div>
</div>
<p data-i18n="header.subtitle">Remplissez vos informations pour générer votre CV</p>
</header> </header>
<div class="card"> <div class="card">
@@ -20,11 +27,12 @@
<!-- ── Section Identité ── --> <!-- ── Section Identité ── -->
<!-- Voir sections/identite/identite.html pour le découpage complet --> <!-- Voir sections/identite/identite.html pour le découpage complet -->
<div class="form-group"> <div class="form-group">
<label for="nom">Nom</label> <label for="nom" data-i18n="form.nom">Nom</label>
<input <input
type="text" type="text"
id="nom" id="nom"
name="nom" name="nom"
data-i18n-placeholder="form.nomPlaceholder"
placeholder="Dupont" placeholder="Dupont"
autocomplete="family-name" autocomplete="family-name"
required required
@@ -34,11 +42,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="prenom">Prénom</label> <label for="prenom" data-i18n="form.prenom">Prénom</label>
<input <input
type="text" type="text"
id="prenom" id="prenom"
name="prenom" name="prenom"
data-i18n-placeholder="form.prenomPlaceholder"
placeholder="Jean" placeholder="Jean"
autocomplete="given-name" autocomplete="given-name"
required required
@@ -48,11 +57,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email" data-i18n="form.email">Email</label>
<input <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
data-i18n-placeholder="form.emailPlaceholder"
placeholder="jean.dupont@email.com" placeholder="jean.dupont@email.com"
autocomplete="email" autocomplete="email"
required required
@@ -61,11 +71,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="titre">Titre du poste</label> <label for="titre" data-i18n="form.titre">Titre du poste</label>
<input <input
type="text" type="text"
id="titre" id="titre"
name="titre" name="titre"
data-i18n-placeholder="form.titrePlaceholder"
placeholder="Développeur Full Stack" placeholder="Développeur Full Stack"
autocomplete="organization-title" autocomplete="organization-title"
required required
@@ -81,14 +92,14 @@
- Hobbies → sections/hobbies/ - Hobbies → sections/hobbies/
--> -->
<button type="submit" class="btn-generate">Générer mon CV</button> <button type="submit" class="btn-generate" data-i18n="form.submit">Générer mon CV</button>
<p class="success-message" id="success"></p> <p class="success-message" id="success"></p>
</form> </form>
</div> </div>
<footer> <footer>
<p>CV Generator &copy; 2026</p> <p data-i18n="footer">CV Generator &copy; 2026</p>
</footer> </footer>
</div> </div>
+60
View File
@@ -0,0 +1,60 @@
const SUPPORTED = ['fr', 'en'];
const FALLBACK = 'fr';
const STORAGE_KEY = 'cv-lang';
let translations = {};
let currentLang = FALLBACK;
function detectLang() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && SUPPORTED.includes(stored)) return stored;
const browser = (navigator.language || '').slice(0, 2).toLowerCase();
return SUPPORTED.includes(browser) ? browser : FALLBACK;
}
async function loadTranslations(lang) {
const res = await fetch(`js/locales/${lang}.json`);
return res.json();
}
// Resolve a dot-notation key like "form.nom"
function resolve(key) {
return key.split('.').reduce((obj, k) => obj?.[k], translations) ?? key;
}
// Replace {{var}} placeholders
export function t(key, vars = {}) {
let str = resolve(key);
for (const [k, v] of Object.entries(vars)) {
str = str.replaceAll(`{{${k}}}`, v);
}
return str;
}
export function getLang() {
return currentLang;
}
// Apply translations to all elements with [data-i18n] and [data-i18n-placeholder]
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.documentElement.lang = currentLang;
}
export async function changeLanguage(lang) {
if (!SUPPORTED.includes(lang)) return;
translations = await loadTranslations(lang);
currentLang = lang;
localStorage.setItem(STORAGE_KEY, lang);
applyTranslations();
}
export async function initI18n() {
const lang = detectLang();
await changeLanguage(lang);
}
+19
View File
@@ -0,0 +1,19 @@
{
"header": {
"title": "CV Generator",
"subtitle": "Fill in your details to generate your CV"
},
"form": {
"nom": "Last name",
"nomPlaceholder": "Smith",
"prenom": "First name",
"prenomPlaceholder": "John",
"email": "Email",
"emailPlaceholder": "john.smith@email.com",
"titre": "Job title",
"titrePlaceholder": "Full Stack Developer",
"submit": "Generate my CV"
},
"success": "CV ready to be generated for {{prenom}} {{nom}}!",
"footer": "CV Generator © 2026"
}
+19
View File
@@ -0,0 +1,19 @@
{
"header": {
"title": "CV Generator",
"subtitle": "Remplissez vos informations pour générer votre CV"
},
"form": {
"nom": "Nom",
"nomPlaceholder": "Dupont",
"prenom": "Prénom",
"prenomPlaceholder": "Jean",
"email": "Email",
"emailPlaceholder": "jean.dupont@email.com",
"titre": "Titre du poste",
"titrePlaceholder": "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"
}
+20 -2
View File
@@ -3,6 +3,7 @@ import { collectCompetences, renderCompetences } from './sections/competences.js
import { collectFormation, renderFormation } from './sections/formation.js'; import { collectFormation, renderFormation } from './sections/formation.js';
import { collectExperience, renderExperience } from './sections/experience.js'; import { collectExperience, renderExperience } from './sections/experience.js';
import { collectHobbies, renderHobbies } from './sections/hobbies.js'; import { collectHobbies, renderHobbies } from './sections/hobbies.js';
import { initI18n, t, changeLanguage, getLang } from './i18n.js';
function generateCV() { function generateCV() {
const identite = collectIdentite(); const identite = collectIdentite();
@@ -19,12 +20,29 @@ function generateCV() {
const success = document.getElementById('success'); const success = document.getElementById('success');
if (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'; success.style.display = 'block';
} }
} }
document.addEventListener('DOMContentLoaded', () => { function updateActiveLangButton() {
const lang = getLang();
document.querySelectorAll('.lang-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.lang === lang);
});
}
document.addEventListener('DOMContentLoaded', async () => {
await initI18n();
updateActiveLangButton();
document.querySelectorAll('.lang-btn').forEach(btn => {
btn.addEventListener('click', async () => {
await changeLanguage(btn.dataset.lang);
updateActiveLangButton();
});
});
document.getElementById('cv-form')?.addEventListener('submit', (e) => { document.getElementById('cv-form')?.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
generateCV(); generateCV();