From 075a5050855840e10155503517a239639f43a68d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Mar 2026 12:13:54 +0000
Subject: [PATCH] feat(profile): expose default_document_language in profile
API and UI
- Add default_document_language to ProfileResponse and ProfileUpdateRequest
- Handle validation in PATCH /api/profile endpoint
- Add dropdown in profile.html template with Alpine.js binding
- Add translation keys for profile UI labels
- Add comprehensive tests for profile default language feature
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
app/api/profile.py | 18 ++
app/api/translation.py | 2 +-
docs/ConfigurationGuide.md | 42 +++
frontend/templates/profile.html | 24 ++
frontend/translations/en.json | 3 +
tests/test_translation.py | 478 ++++++++++++++++++++++++++++++++
6 files changed, 566 insertions(+), 1 deletion(-)
create mode 100644 tests/test_translation.py
diff --git a/app/api/profile.py b/app/api/profile.py
index 90369f73..b3482e3c 100644
--- a/app/api/profile.py
+++ b/app/api/profile.py
@@ -99,6 +99,8 @@ class ProfileResponse(BaseModel):
contact_email: str | None
preferred_language: str | None
preferred_theme: str | None
+ default_document_language: str | None
+ """ISO 639-1 code for the user's preferred document translation target language."""
avatar_url: str
"""Gravatar URL or ``data:`` URI for a custom uploaded avatar."""
is_local_user: bool
@@ -112,6 +114,10 @@ class ProfileUpdateRequest(BaseModel):
contact_email: str | None = Field(default=None, max_length=255, description="Contact / notification e-mail")
preferred_language: str | None = Field(default=None, description="ISO 639-1 language code, e.g. 'en', 'de'")
preferred_theme: str | None = Field(default=None, description="Colour scheme: 'light', 'dark', or 'system'")
+ default_document_language: str | None = Field(
+ default=None,
+ description="ISO 639-1 code for the default document translation target language, e.g. 'en', 'de'",
+ )
class ChangePasswordRequest(BaseModel):
@@ -149,6 +155,7 @@ async def get_profile(request: Request, db: DbSession) -> ProfileResponse:
contact_email=profile.contact_email, # type: ignore[arg-type]
preferred_language=profile.preferred_language, # type: ignore[arg-type]
preferred_theme=profile.preferred_theme, # type: ignore[arg-type]
+ default_document_language=profile.default_document_language, # type: ignore[arg-type]
avatar_url=avatar_url,
is_local_user=is_local,
)
@@ -201,6 +208,16 @@ async def update_profile(
)
profile.preferred_theme = theme or None # type: ignore[assignment]
+ # Validate default document language
+ if body.default_document_language is not None:
+ doc_lang = body.default_document_language.lower().strip()
+ if doc_lang and doc_lang not in SUPPORTED_LANGUAGE_CODES:
+ raise HTTPException(
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+ detail=f"Unsupported language code: {doc_lang}",
+ )
+ profile.default_document_language = doc_lang or None # type: ignore[assignment]
+
if body.display_name is not None:
profile.display_name = body.display_name.strip() or None # type: ignore[assignment]
@@ -225,6 +242,7 @@ async def update_profile(
contact_email=profile.contact_email, # type: ignore[arg-type]
preferred_language=profile.preferred_language, # type: ignore[arg-type]
preferred_theme=profile.preferred_theme, # type: ignore[arg-type]
+ default_document_language=profile.default_document_language, # type: ignore[arg-type]
avatar_url=avatar_url,
is_local_user=is_local,
)
diff --git a/app/api/translation.py b/app/api/translation.py
index 26e65fe6..20c1af46 100644
--- a/app/api/translation.py
+++ b/app/api/translation.py
@@ -32,7 +32,7 @@ _MAX_TRANSLATION_INPUT = 50_000
def _get_file_or_404(db: Session, file_id: int, request: Request) -> FileRecord:
"""Fetch a FileRecord visible to the current user or raise 404."""
query = db.query(FileRecord).filter(FileRecord.id == file_id)
- owner_id = get_current_owner_id(request, db)
+ owner_id = get_current_owner_id(request)
if owner_id:
query = apply_owner_filter(query, owner_id, FileRecord)
record = query.first()
diff --git a/docs/ConfigurationGuide.md b/docs/ConfigurationGuide.md
index 37acf9c2..45f4ba13 100644
--- a/docs/ConfigurationGuide.md
+++ b/docs/ConfigurationGuide.md
@@ -934,6 +934,48 @@ OPENAI_API_KEY=sk-ant-... # passed as the api_key to LiteLLM
---
+### Document Translation
+
+After processing, DocuElevate can automatically translate a document's extracted text into a configurable *default language* (e.g. English). This reference translation is stored alongside the original text so users always have a version in a language they understand.
+
+Other languages are translated **on the fly** via the AI provider and are not persisted.
+
+#### Settings
+
+| **Variable** | **Description** | **Default** |
+|------------------------------|-----------------------------------------------------------------------------------------------------------|-------------|
+| `DEFAULT_DOCUMENT_LANGUAGE` | ISO 639-1 code for the default translation target (e.g. `en`, `de`, `fr`). Documents whose detected language differs are automatically translated into this language after processing. | `en` |
+
+Each user can override this global default in their profile (`UserProfile.default_document_language`).
+
+#### How It Works
+
+1. During metadata extraction the AI detects the document language (stored as `detected_language` on the file record).
+2. If the detected language differs from the default target language, a background Celery task (`translate_to_default_language`) translates the extracted text.
+3. The translated text is persisted in `default_language_text` and the target code in `default_language_code`.
+4. The file detail view shows both the original text and the default-language version.
+5. Users can also request on-the-fly translations to any language via the **Translate** dropdown.
+
+#### API Endpoints
+
+| **Endpoint** | **Method** | **Description** |
+|-----------------------------------------------|------------|------------------------------------------------------------------------|
+| `/api/files/{id}/translation/default` | GET | Returns the persisted default-language translation (404 if unavailable)|
+| `/api/files/{id}/translate?lang=xx` | GET | On-the-fly translation to any ISO 639-1 language code |
+| `/files/{id}/text/default-language` | GET | View endpoint returning the default-language text as JSON |
+
+#### Example
+
+```bash
+# Get the stored English translation of a German document
+curl http://localhost:8000/api/files/42/translation/default
+
+# Translate on the fly to French
+curl "http://localhost:8000/api/files/42/translate?lang=fr"
+```
+
+---
+
### OCR Providers
DocuElevate supports multiple OCR engines that can be used individually or in combination. Configure the list of active providers with `OCR_PROVIDERS` and tune each provider with the settings below.
diff --git a/frontend/templates/profile.html b/frontend/templates/profile.html
index 50b2371e..62973360 100644
--- a/frontend/templates/profile.html
+++ b/frontend/templates/profile.html
@@ -231,6 +231,28 @@
{{ _("profile.theme_hint") }}