feat: ajouter le système i18n frontend (FR/EN)
This commit is contained in:
+60
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -3,6 +3,7 @@ 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 { initI18n, t, changeLanguage, getLang } from './i18n.js';
|
||||
|
||||
function generateCV() {
|
||||
const identite = collectIdentite();
|
||||
@@ -19,12 +20,29 @@ 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', () => {
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
generateCV();
|
||||
|
||||
Reference in New Issue
Block a user