Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e9746c308 |
@@ -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;
|
||||
}
|
||||
|
||||
+15
-8
@@ -10,8 +10,11 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>CV Generator</h1>
|
||||
<p>Remplissez vos informations pour générer votre CV</p>
|
||||
<div class="header-top">
|
||||
<h1 data-i18n="header.title">CV Generator</h1>
|
||||
<div id="lang-switcher" class="lang-switcher" aria-label="Language switcher"></div>
|
||||
</div>
|
||||
<p data-i18n="header.subtitle">Remplissez vos informations pour générer votre CV</p>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
@@ -20,11 +23,12 @@
|
||||
<!-- ── Section Identité ── -->
|
||||
<!-- Voir sections/identite/identite.html pour le découpage complet -->
|
||||
<div class="form-group">
|
||||
<label for="nom">Nom</label>
|
||||
<label for="nom" data-i18n="form.nom">Nom</label>
|
||||
<input
|
||||
type="text"
|
||||
id="nom"
|
||||
name="nom"
|
||||
data-i18n-placeholder="form.nom_placeholder"
|
||||
placeholder="Dupont"
|
||||
autocomplete="family-name"
|
||||
required
|
||||
@@ -34,11 +38,12 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="prenom">Prénom</label>
|
||||
<label for="prenom" data-i18n="form.prenom">Prénom</label>
|
||||
<input
|
||||
type="text"
|
||||
id="prenom"
|
||||
name="prenom"
|
||||
data-i18n-placeholder="form.prenom_placeholder"
|
||||
placeholder="Jean"
|
||||
autocomplete="given-name"
|
||||
required
|
||||
@@ -48,11 +53,12 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<label for="email" data-i18n="form.email">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
data-i18n-placeholder="form.email_placeholder"
|
||||
placeholder="jean.dupont@email.com"
|
||||
autocomplete="email"
|
||||
required
|
||||
@@ -61,11 +67,12 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="titre">Titre du poste</label>
|
||||
<label for="titre" data-i18n="form.titre">Titre du poste</label>
|
||||
<input
|
||||
type="text"
|
||||
id="titre"
|
||||
name="titre"
|
||||
data-i18n-placeholder="form.titre_placeholder"
|
||||
placeholder="Développeur Full Stack"
|
||||
autocomplete="organization-title"
|
||||
required
|
||||
@@ -81,14 +88,14 @@
|
||||
- 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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>CV Generator © 2026</p>
|
||||
<p data-i18n="footer">CV Generator © 2026</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user