diff --git a/app/api/profile.py b/app/api/profile.py index a12324d8..90369f73 100644 --- a/app/api/profile.py +++ b/app/api/profile.py @@ -18,7 +18,7 @@ import logging from hashlib import md5 from typing import Annotated -from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status +from fastapi import APIRouter, Depends, File, HTTPException, Request, Response, UploadFile, status from pydantic import BaseModel, Field from sqlalchemy.orm import Session @@ -156,7 +156,9 @@ async def get_profile(request: Request, db: DbSession) -> ProfileResponse: @router.patch("", response_model=ProfileResponse) @require_login -async def update_profile(body: ProfileUpdateRequest, request: Request, db: DbSession) -> ProfileResponse: +async def update_profile( + body: ProfileUpdateRequest, request: Request, response: Response, db: DbSession +) -> ProfileResponse: """Update the current user's editable profile settings.""" user_id = _get_user_id(request) profile = _get_or_create_profile(db, user_id) @@ -171,6 +173,24 @@ async def update_profile(body: ProfileUpdateRequest, request: Request, db: DbSes ) profile.preferred_language = lang or None # type: ignore[assignment] + # Keep session and cookie in sync so detect_language() picks up + # the new preference immediately (without a DB round-trip). + if hasattr(request, "session"): + if lang: + request.session["preferred_language"] = lang + else: + request.session.pop("preferred_language", None) + if lang: + response.set_cookie( + key="docuelevate_lang", + value=lang, + max_age=30 * 24 * 60 * 60, + httponly=False, + samesite="lax", + ) + else: + response.delete_cookie(key="docuelevate_lang") + # Validate theme if body.preferred_theme is not None: theme = body.preferred_theme.lower().strip() diff --git a/app/views/base.py b/app/views/base.py index e00f261b..609ff9fb 100644 --- a/app/views/base.py +++ b/app/views/base.py @@ -11,7 +11,8 @@ from sqlalchemy.orm import Session # noqa: F401 from app.auth import require_login # noqa: F401 from app.config import settings -from app.database import get_db # noqa: F401 +from app.database import SessionLocal, get_db # noqa: F401 +from app.models import UserProfile from app.utils.i18n import ( SUPPORTED_LANGUAGES, detect_language, @@ -47,6 +48,41 @@ templates.env.globals["_"] = lambda key, **kwargs: translate(key, "en", **kwargs original_template_response = templates.TemplateResponse +def _hydrate_language_from_db(request: Request, session_user: object) -> None: + """Load the user's preferred language from the DB into the session. + + Called once per session when ``preferred_language`` is not yet in the + session. A lightweight DB query fetches the stored preference so that + :func:`detect_language` picks it up from the session on all subsequent + requests without further DB access. + """ + from app.utils.i18n import SUPPORTED_LANGUAGE_CODES + + user_id: str | None = None + if isinstance(session_user, dict): + user_id = ( + session_user.get("sub") + or session_user.get("preferred_username") + or session_user.get("email") + or session_user.get("id") + ) + elif isinstance(session_user, str): + user_id = session_user + + if not user_id: + return + + db = SessionLocal() + try: + profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first() + if profile and profile.preferred_language and profile.preferred_language in SUPPORTED_LANGUAGE_CODES: + request.session["preferred_language"] = profile.preferred_language + except Exception: # noqa: BLE001 — intentionally broad; DB may be temporarily unavailable + logger.debug("Could not hydrate language preference for user_id=%s", user_id) + finally: + db.close() + + def _inject_global_context(ctx: dict) -> None: """Inject shared global variables into every template context dict.""" ctx.setdefault("version", settings.version) @@ -71,6 +107,14 @@ def _inject_global_context(ctx: dict) -> None: # When auth is disabled every visitor is effectively "logged in" ctx.setdefault("is_logged_in", not getattr(settings, "auth_enabled", True) or session_user is not None) + # --- Hydrate session language from DB (once per session) --- + # If the session doesn't have a preferred_language yet but the user + # is logged in, load the stored preference from the database so that + # detect_language() picks it up from the session on this and all + # subsequent requests. + if hasattr(req, "session") and "preferred_language" not in req.session and session_user is not None: + _hydrate_language_from_db(req, session_user) + # --- i18n: detect language and register template helpers --- current_locale = detect_language(req) ctx.setdefault("current_locale", current_locale) diff --git a/frontend/templates/base.html b/frontend/templates/base.html index afbe786e..9230423b 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -216,12 +216,18 @@ aria-live="polite"> + + +