-
+
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();