fix(i18n): persist language preference server-side, fix dropdown rendering and avatar 404

- Fix language dropdown in base.html by moving tojson data out of HTML
  attribute into a script tag (prevents quote conflicts breaking Alpine.js)
- Fix avatar placeholder 404 by correcting filename reference from
  avatar-placeholder.svg to default-avatar.svg
- Add session hydration from DB in _inject_global_context() so
  detect_language() uses the stored preference on every request
- Sync session and cookie in PATCH /api/profile when language changes
- Reload page after language change in profile to reflect new locale
- Add tests for session/cookie sync and DB hydration

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-14 10:30:50 +00:00
parent c3e389d957
commit 1416d901d0
5 changed files with 178 additions and 11 deletions
+8 -2
View File
@@ -216,12 +216,18 @@
aria-live="polite"></span>
</a>
<!-- Language selector data (kept outside the HTML attribute to avoid quote conflicts with tojson) -->
<script>
window.__langSuggested = {{ suggested_languages | tojson }};
window.__langAll = {{ supported_languages | tojson }};
</script>
<!-- Language selector dropdown -->
<div x-data="{
langOpen: false,
search: '',
suggested: {{ suggested_languages | tojson }},
all: {{ supported_languages | tojson }},
suggested: window.__langSuggested,
all: window.__langAll,
get filtered() {
if (!this.search.trim()) return this.suggested;
const q = this.search.trim().toLowerCase();
+10 -1
View File
@@ -327,7 +327,7 @@
function profileSettings() {
return {
// ── State ──────────────────────────────────────────────────────────────
avatarUrl: '/static/images/avatar-placeholder.svg',
avatarUrl: '/static/images/default-avatar.svg',
hasCustomAvatar: false,
isLocalUser: false,
saving: false,
@@ -349,6 +349,8 @@ function profileSettings() {
},
// ── Init ───────────────────────────────────────────────────────────────
_initialLanguage: '',
async init() {
try {
const res = await fetch('/api/profile');
@@ -361,6 +363,7 @@ function profileSettings() {
this.form.contact_email = data.contact_email || '';
this.form.preferred_language = data.preferred_language || '';
this.form.preferred_theme = data.preferred_theme || 'system';
this._initialLanguage = this.form.preferred_language;
} catch (_e) {
// Silently ignore — user might not be logged in (rare for this page)
}
@@ -378,6 +381,7 @@ function profileSettings() {
async saveProfile() {
this.saving = true;
this._hideBanner();
const previousLang = this._initialLanguage || '';
try {
const csrfToken = this._getCSRFToken();
@@ -394,6 +398,11 @@ function profileSettings() {
this._showBanner(data.detail || 'Failed to save settings.', true);
return;
}
// If the UI language changed, reload so the page renders in the new locale
if (this.form.preferred_language !== previousLang) {
window.location.reload();
return;
}
this._showBanner('Profile settings saved successfully.', false);
// Update display name across the nav (refresh the avatar/name)
if (data.avatar_url) {