feat: server-side language preference — survives clear site data
CI / lint (push) Failing after 24s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Failing after 10s

Problem: language preference stored only in localStorage, lost on
'clear site data'. French users with browser lang=fr always got French
UI even when profile was English.

Solution:
- Backend: add 'language' field to user profile (default 'fr')
- Backend: PATCH /api/auth/me to update language preference
- Frontend: initI18n() priority: server > localStorage > navigator > 'fr'
- Frontend: initAuth() runs before initI18n() so server check works
- Frontend: profile Save persists language to server via PATCH

Verified: tabs show English after server profile set to 'en'
This commit is contained in:
2026-06-21 10:29:51 -04:00
parent e49fb46be7
commit 2de9f91671
5 changed files with 69 additions and 7 deletions
+6 -3
View File
@@ -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();
+11
View File
@@ -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) {}
});
+20 -4
View File
@@ -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'