diff --git a/backend/auth/router.py b/backend/auth/router.py index 23992b8..09ac891 100644 --- a/backend/auth/router.py +++ b/backend/auth/router.py @@ -242,10 +242,41 @@ async def get_me(current_user=Depends(require_auth)): "display_name": current_user["display_name"], "role": current_user["role"], "vaults": current_user["vaults"], + "language": current_user.get("language", "fr"), "last_login": current_user.get("last_login"), } +class UpdateMeRequest(BaseModel): + """Fields the user can update on their own profile.""" + display_name: Optional[str] = None + language: Optional[str] = None + + +@router.patch("/me") +async def patch_me(req: UpdateMeRequest, current_user=Depends(require_auth)): + """Update current user's profile fields (display_name, language).""" + from .user_store import update_user + updates = {} + if req.display_name is not None: + updates["display_name"] = req.display_name + if req.language is not None: + if req.language not in ("fr", "en"): + raise HTTPException(400, "language must be 'fr' or 'en'") + updates["language"] = req.language + if not updates: + raise HTTPException(400, "No fields to update") + updated = update_user(current_user["username"], updates) + return { + "username": updated["username"], + "display_name": updated["display_name"], + "role": updated["role"], + "vaults": updated["vaults"], + "language": updated.get("language", "fr"), + "last_login": updated.get("last_login"), + } + + @router.post("/change-password") async def change_password( req: ChangePasswordRequest, diff --git a/backend/auth/user_store.py b/backend/auth/user_store.py index bf9d742..8e1b7b3 100644 --- a/backend/auth/user_store.py +++ b/backend/auth/user_store.py @@ -88,6 +88,7 @@ def create_user( "role": role, "vaults": vaults or [], "active": True, + "language": "fr", # default UI language "created_at": datetime.now(timezone.utc).isoformat(), "last_login": None, "failed_attempts": 0, diff --git a/frontend/js/app.js b/frontend/js/app.js index 26cd468..bf6de33 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -23,7 +23,12 @@ import { initMermaid, setupFocusMode } from './mermaid-viewer.js'; // Initialization — mirrors the original app.js init() ordering // ========================================================================= async function init() { - await initI18n(); // Must run before any rendering that uses t() + // 1. Init auth first — needed for server-side language preference + const authOk = await Auth.AuthManager.initAuth(); + + // 2. Init i18n after auth so server language preference is available + await initI18n(); + UI.initTheme(); UI.initHeaderMenu(); UI.initCustomDropdowns(); @@ -54,8 +59,6 @@ async function init() { initCommandPalette(); initMobileToolbar(); - const authOk = await Auth.AuthManager.initAuth(); - if (authOk) { Legacy.initSyncStatus(); diff --git a/frontend/js/config.js b/frontend/js/config.js index 9b58989..72a1375 100644 --- a/frontend/js/config.js +++ b/frontend/js/config.js @@ -1561,6 +1561,17 @@ function initProfile() { var newLang = langSel.value; await setLocale(newLang); // Live-reload all translations if (nameInp) localStorage.setItem('obsigate-name', nameInp.value.trim()); + + // Persist language to server (survives clear site data) + try { + await fetch('/api/auth/me', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ language: newLang }), + }); + } catch (e) { /* non-bloquant */ } + if (savedEl) { savedEl.style.display = 'inline'; setTimeout(function() { savedEl.style.display = 'none'; }, 2000); } } catch(e) {} }); diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js index 8cfe1d7..0e49e4b 100644 --- a/frontend/js/i18n.js +++ b/frontend/js/i18n.js @@ -91,12 +91,28 @@ export async function setLocale(locale) { } /** Initialise the i18n system. Call once, early, before any rendering. - * Reads saved preference from localStorage, falls back to navigator.language, - * then to 'fr'. */ + * Priority: server profile > localStorage > navigator.language > 'fr'. */ export async function initI18n() { - // 1. Determine locale: saved > navigator.language > 'fr' + // 1. Determine locale: server profile > localStorage > navigator.language > 'fr' let locale = null; - try { locale = localStorage.getItem('obsigate-lang'); } catch (e) { /* */ } + + // Try server-side language preference (survives clear site data) + try { + const resp = await fetch('/api/auth/me', { credentials: 'include' }); + if (resp.ok) { + const user = await resp.json(); + if (user.language === 'en' || user.language === 'fr') { + locale = user.language; + } + } + } catch (e) { /* not logged in or network error — fall through */ } + + // Fall back to localStorage + if (!locale) { + try { locale = localStorage.getItem('obsigate-lang'); } catch (e) { /* */ } + } + + // Fall back to navigator.language if (!locale) { const nav = (navigator.language || '').toLowerCase(); locale = nav.startsWith('fr') ? 'fr'