fixed sysuser and removed redundant fallbacks
This commit is contained in:
parent
4dfc0afd06
commit
3adbd1da29
31 changed files with 276 additions and 292 deletions
4
app.py
4
app.py
|
|
@ -491,12 +491,12 @@ from modules.auth import (
|
|||
)
|
||||
|
||||
# i18n language detection middleware (sets per-request language from Accept-Language header)
|
||||
from modules.shared.i18nRegistry import _setLanguage
|
||||
from modules.shared.i18nRegistry import _setLanguage, normalizePrimaryLanguageTag
|
||||
|
||||
@app.middleware("http")
|
||||
async def _i18nMiddleware(request: Request, call_next):
|
||||
acceptLang = request.headers.get("Accept-Language", "")
|
||||
lang = acceptLang[:2].lower() if len(acceptLang) >= 2 and acceptLang[:2].isalpha() else "de"
|
||||
lang = normalizePrimaryLanguageTag(acceptLang, "de")
|
||||
_setLanguage(lang)
|
||||
return await call_next(request)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from typing import Optional, List, Dict, Any
|
|||
from enum import Enum
|
||||
from pydantic import BaseModel, Field, EmailStr, field_validator, computed_field
|
||||
from modules.datamodels.datamodelBase import PowerOnModel
|
||||
from modules.shared.i18nRegistry import i18nModel
|
||||
from modules.shared.i18nRegistry import i18nModel, normalizePrimaryLanguageTag
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
|
||||
|
||||
|
|
@ -243,17 +243,12 @@ class User(PowerOnModel):
|
|||
)
|
||||
language: str = Field(
|
||||
default="de",
|
||||
description="Preferred language of the user (ISO 639-1 code: de, en, fr, it)",
|
||||
description="Preferred UI language code (must exist as UiLanguageSet; loaded from /api/i18n/user-language-options).",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": [
|
||||
{"value": "de", "label": "Deutsch"},
|
||||
{"value": "en", "label": "Englisch"},
|
||||
{"value": "fr", "label": "Französisch"},
|
||||
{"value": "it", "label": "Italienisch"},
|
||||
],
|
||||
"frontend_options": "/api/i18n/user-language-options",
|
||||
"label": "Sprache",
|
||||
},
|
||||
)
|
||||
|
|
@ -261,10 +256,9 @@ class User(PowerOnModel):
|
|||
@field_validator('language', mode='before')
|
||||
@classmethod
|
||||
def _normalizeLanguage(cls, v):
|
||||
"""Normalize language to valid ISO 639-1 code."""
|
||||
"""Normalize to primary language subtag (2–8 letters); default remains ``de``."""
|
||||
if v is None:
|
||||
return "de"
|
||||
# Map common variations to standard codes
|
||||
langMap = {
|
||||
'english': 'en', 'englisch': 'en',
|
||||
'german': 'de', 'deutsch': 'de',
|
||||
|
|
@ -274,11 +268,7 @@ class User(PowerOnModel):
|
|||
normalized = str(v).lower().strip()
|
||||
if normalized in langMap:
|
||||
return langMap[normalized]
|
||||
# If already a valid code, return as-is
|
||||
if normalized in ['de', 'en', 'fr', 'it']:
|
||||
return normalized
|
||||
# Default fallback
|
||||
return "de"
|
||||
return normalizePrimaryLanguageTag(normalized, "de")
|
||||
|
||||
enabled: bool = Field(
|
||||
default=True,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
# All rights reserved.
|
||||
"""Utility datamodels: Prompt, TextMultilingual."""
|
||||
|
||||
import re as _re
|
||||
from typing import Any, Dict, Optional
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from modules.datamodels.datamodelBase import PowerOnModel
|
||||
from modules.shared.i18nRegistry import i18nModel
|
||||
|
|
@ -98,21 +99,6 @@ class TextMultilingual(BaseModel):
|
|||
return cls(xx=t)
|
||||
|
||||
|
||||
_REPR_PATTERN = _re.compile(r"(\w+)='([^']*)'")
|
||||
|
||||
|
||||
def _parseReprString(s: str) -> Optional[Dict[str, str]]:
|
||||
"""Parse a Pydantic repr string like "en='text' de=None fr=" into a dict."""
|
||||
matches = _REPR_PATTERN.findall(s)
|
||||
if not matches:
|
||||
return None
|
||||
result = {}
|
||||
for code, text in matches:
|
||||
if len(code) <= 5 and text:
|
||||
result[code] = text
|
||||
return result if result else None
|
||||
|
||||
|
||||
def coerce_text_multilingual(val: Any) -> TextMultilingual:
|
||||
"""Normalize str, dict, or TextMultilingual into a valid TextMultilingual instance."""
|
||||
if isinstance(val, TextMultilingual):
|
||||
|
|
@ -125,10 +111,13 @@ def coerce_text_multilingual(val: Any) -> TextMultilingual:
|
|||
cleaned["xx"] = cleaned.get("de") or next((v for v in cleaned.values() if v), "—")
|
||||
return TextMultilingual(**cleaned)
|
||||
if isinstance(val, str) and val.strip():
|
||||
parsed = _parseReprString(val)
|
||||
if parsed:
|
||||
if not parsed.get("xx"):
|
||||
parsed["xx"] = parsed.get("de") or next((v for v in parsed.values() if v), val.strip())
|
||||
return TextMultilingual(**parsed)
|
||||
return TextMultilingual.fromUniform(val)
|
||||
s = val.strip()
|
||||
if s.startswith("{") and s.endswith("}"):
|
||||
try:
|
||||
parsed = json.loads(s)
|
||||
if isinstance(parsed, dict):
|
||||
return coerce_text_multilingual(parsed)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return TextMultilingual.fromUniform(s)
|
||||
return TextMultilingual.fromUniform("—")
|
||||
|
|
|
|||
|
|
@ -191,8 +191,8 @@ def getChatStreamingHelper():
|
|||
|
||||
def __get_placeholder_user():
|
||||
"""Placeholder user for contexts that only need service resolution (e.g. ChatStreamingHelper)."""
|
||||
from modules.datamodels.datamodelUam import User
|
||||
return User(id="system", username="system", email=None, fullName="System Placeholder")
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
return getRootInterface().currentUser
|
||||
|
||||
|
||||
def getEventManager(user, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from modules.datamodels.datamodelUam import User
|
|||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||
from modules.shared.timeUtils import getIsoTimestamp
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.shared.i18nRegistry import t
|
||||
from modules.shared.i18nRegistry import resolveText, t
|
||||
|
||||
from .datamodelCommcoach import (
|
||||
CoachingContext, CoachingContextStatus,
|
||||
|
|
@ -414,16 +414,20 @@ def _calcGoalProgress(goalsRaw) -> Optional[int]:
|
|||
|
||||
|
||||
_LEVELS = [
|
||||
(50, 5, "master", t("Meister")),
|
||||
(25, 4, "expert", t("Experte")),
|
||||
(10, 3, "advanced", t("Fortgeschritten")),
|
||||
(3, 2, "engaged", t("Engagiert")),
|
||||
(50, 5, "master", "Meister"),
|
||||
(25, 4, "expert", "Experte"),
|
||||
(10, 3, "advanced", "Fortgeschritten"),
|
||||
(3, 2, "engaged", "Engagiert"),
|
||||
]
|
||||
t("Meister")
|
||||
t("Experte")
|
||||
t("Fortgeschritten")
|
||||
t("Engagiert")
|
||||
t("Einsteiger")
|
||||
|
||||
|
||||
def _calcLevel(totalSessions: int) -> Dict[str, Any]:
|
||||
for threshold, number, code, _label in _LEVELS:
|
||||
for threshold, number, code, labelKey in _LEVELS:
|
||||
if totalSessions >= threshold:
|
||||
return {"number": number, "code": code, "label": t(_label), "totalSessions": totalSessions}
|
||||
return {"number": 1, "code": "beginner", "label": t("Einsteiger"), "totalSessions": totalSessions}
|
||||
return {"number": number, "code": code, "label": resolveText(labelKey), "totalSessions": totalSessions}
|
||||
return {"number": 1, "code": "beginner", "label": resolveText("Einsteiger"), "totalSessions": totalSessions}
|
||||
|
|
|
|||
|
|
@ -236,9 +236,9 @@ def _seedBuiltinPersonas():
|
|||
try:
|
||||
from .serviceCommcoachPersonas import seedBuiltinPersonas
|
||||
from .interfaceFeatureCommcoach import getInterface
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
|
||||
systemUser = User(id="system", username="system", email="system@poweron.swiss")
|
||||
systemUser = getRootInterface().currentUser
|
||||
interface = getInterface(systemUser)
|
||||
seedBuiltinPersonas(interface)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Checks and awards badges after each session completion.
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from modules.shared.i18nRegistry import t
|
||||
from modules.shared.i18nRegistry import resolveText, t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -80,9 +80,32 @@ BADGE_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
|||
}
|
||||
|
||||
# Register all badge labels/descriptions at import time for i18n xx base set
|
||||
for _bd in BADGE_DEFINITIONS.values():
|
||||
t(_bd["label"])
|
||||
t(_bd["description"])
|
||||
t("Erste Session")
|
||||
t("Deine erste Coaching-Session abgeschlossen")
|
||||
t("3-Tage-Serie")
|
||||
t("3 Tage in Folge eine Session absolviert")
|
||||
t("Wochenserie")
|
||||
t("7 Tage in Folge eine Session absolviert")
|
||||
t("Monatsserie")
|
||||
t("30 Tage in Folge eine Session absolviert")
|
||||
t("Engagiert")
|
||||
t("5 Sessions abgeschlossen")
|
||||
t("Fortgeschritten")
|
||||
t("10 Sessions abgeschlossen")
|
||||
t("Experte")
|
||||
t("25 Sessions abgeschlossen")
|
||||
t("Meister")
|
||||
t("50 Sessions abgeschlossen")
|
||||
t("Bestleistung")
|
||||
t("Durchschnittsscore über 80 in einer Session")
|
||||
t("Vielseitig")
|
||||
t("3 verschiedene Coaching-Themen aktiv")
|
||||
t("Rollenspieler")
|
||||
t("Erste Roleplay-Session mit einer Persona abgeschlossen")
|
||||
t("Ganzheitlich")
|
||||
t("In allen 5 Kompetenz-Dimensionen bewertet")
|
||||
t("Umsetzer")
|
||||
t("10 Coaching-Aufgaben erledigt")
|
||||
|
||||
|
||||
async def checkAndAwardBadges(interface, userId: str, mandateId: str, instanceId: str,
|
||||
|
|
@ -141,8 +164,8 @@ async def checkAndAwardBadges(interface, userId: str, mandateId: str, instanceId
|
|||
}
|
||||
newBadge = interface.awardBadge(badgeData)
|
||||
definition = BADGE_DEFINITIONS.get(badgeKey, {})
|
||||
newBadge["label"] = t(definition.get("label", badgeKey))
|
||||
newBadge["description"] = t(definition.get("description", ""))
|
||||
newBadge["label"] = resolveText(definition.get("label", badgeKey))
|
||||
newBadge["description"] = resolveText(definition.get("description", ""))
|
||||
newBadge["icon"] = definition.get("icon", "star")
|
||||
awarded.append(newBadge)
|
||||
logger.info(f"Badge '{badgeKey}' awarded to user {userId}")
|
||||
|
|
@ -154,5 +177,9 @@ def getBadgeDefinitions() -> Dict[str, Dict[str, Any]]:
|
|||
"""Return all badge definitions for the frontend (labels resolved via i18n)."""
|
||||
resolved = {}
|
||||
for key, defn in BADGE_DEFINITIONS.items():
|
||||
resolved[key] = {**defn, "label": t(defn["label"]), "description": t(defn["description"])}
|
||||
resolved[key] = {
|
||||
**defn,
|
||||
"label": resolveText(defn["label"]),
|
||||
"description": resolveText(defn["description"]),
|
||||
}
|
||||
return resolved
|
||||
|
|
|
|||
|
|
@ -36,10 +36,11 @@ def default_manual_entry_point() -> Dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def _normalize_title(title: Any, preferredLang: Optional[str] = None) -> str:
|
||||
def _normalize_title(title: Any) -> str:
|
||||
"""Extract a plain string from a title value for storage (not display)."""
|
||||
if isinstance(title, dict):
|
||||
picked = (preferredLang and title.get(preferredLang)) or title.get("xx") or next(iter(title.values()), "")
|
||||
return str(picked).strip() if picked is not None else ""
|
||||
picked = title.get("xx") or next((v for v in title.values() if v), None)
|
||||
return str(picked).strip() if picked else "Start"
|
||||
if isinstance(title, str) and title.strip():
|
||||
return title.strip()
|
||||
return "Start"
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ from typing import Dict, List, Any
|
|||
|
||||
from modules.features.graphicalEditor.nodeDefinitions import STATIC_NODE_TYPES
|
||||
from modules.features.graphicalEditor.portTypes import PORT_TYPE_CATALOG, SYSTEM_VARIABLES
|
||||
from modules.shared.i18nRegistry import normalizePrimaryLanguageTag
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def getNodeTypes(
|
||||
services: Any = None,
|
||||
language: str = "en",
|
||||
language: str = "de",
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Return static node types. No dynamic I/O derivation from methodDiscovery.
|
||||
|
|
@ -25,27 +26,42 @@ def getNodeTypes(
|
|||
return list(STATIC_NODE_TYPES)
|
||||
|
||||
|
||||
def _pickFromLangMap(d: Any, lang: str) -> Any:
|
||||
"""Resolve multilingual dict: ``lang`` → ``xx`` → ``de`` → ``en`` → first non-empty value."""
|
||||
if not isinstance(d, dict) or not d:
|
||||
return None
|
||||
for k in (lang, "xx", "de", "en"):
|
||||
v = d.get(k)
|
||||
if v is not None and v != "":
|
||||
return v
|
||||
for v in d.values():
|
||||
if v is not None and v != "":
|
||||
return v
|
||||
return None
|
||||
|
||||
|
||||
def _localizeNode(node: Dict[str, Any], language: str) -> Dict[str, Any]:
|
||||
"""Apply language to label/description/parameters. Keep inputPorts/outputPorts."""
|
||||
lang = language if language in ("en", "de", "fr") else "en"
|
||||
lang = normalizePrimaryLanguageTag(language, "en")
|
||||
out = dict(node)
|
||||
for key in list(out.keys()):
|
||||
if key.startswith("_"):
|
||||
del out[key]
|
||||
if isinstance(node.get("label"), dict):
|
||||
out["label"] = node["label"].get(lang, node["label"].get("en", str(node["label"])))
|
||||
out["label"] = _pickFromLangMap(node["label"], lang) or node.get("id", "")
|
||||
if isinstance(node.get("description"), dict):
|
||||
out["description"] = node["description"].get(lang, node["description"].get("en", str(node["description"])))
|
||||
out["description"] = _pickFromLangMap(node["description"], lang) or ""
|
||||
ol = node.get("outputLabels")
|
||||
if isinstance(ol, dict) and ol:
|
||||
first = next(iter(ol.values()), None)
|
||||
if isinstance(first, (list, tuple)):
|
||||
out["outputLabels"] = ol.get(lang, ol.get("en", list(first)))
|
||||
picked = _pickFromLangMap(ol, lang)
|
||||
out["outputLabels"] = picked if picked is not None else list(first)
|
||||
params = []
|
||||
for p in node.get("parameters", []):
|
||||
pc = dict(p)
|
||||
if isinstance(p.get("description"), dict):
|
||||
pc["description"] = p["description"].get(lang, p["description"].get("en", str(p.get("description", ""))))
|
||||
pc["description"] = _pickFromLangMap(p["description"], lang) or str(p.get("description", ""))
|
||||
params.append(pc)
|
||||
out["parameters"] = params
|
||||
return out
|
||||
|
|
@ -53,7 +69,7 @@ def _localizeNode(node: Dict[str, Any], language: str) -> Dict[str, Any]:
|
|||
|
||||
def getNodeTypesForApi(
|
||||
services: Any,
|
||||
language: str = "en",
|
||||
language: str = "de",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
API-ready response: nodeTypes with localized strings, plus categories, portTypeCatalog, systemVariables.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ from typing import Any, Callable, Dict, List, Optional
|
|||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -480,11 +482,9 @@ def _deriveFormPayloadSchema(node: Dict[str, Any]) -> Optional[PortSchema]:
|
|||
for f in fields_param:
|
||||
if isinstance(f, dict) and f.get("name"):
|
||||
_lab = f.get("label")
|
||||
_desc = (
|
||||
str(_lab.get("xx") or next(iter(_lab.values()), "") or f["name"])
|
||||
if isinstance(_lab, dict)
|
||||
else str(_lab if _lab is not None else f["name"])
|
||||
)
|
||||
_desc = resolveText(_lab) if _lab is not None else f["name"]
|
||||
if not _desc.strip():
|
||||
_desc = f["name"]
|
||||
portFields.append(PortField(
|
||||
name=f["name"],
|
||||
type=f.get("type", "str"),
|
||||
|
|
|
|||
|
|
@ -26,21 +26,12 @@ from modules.workflows.automation2.runEnvelope import (
|
|||
normalize_run_envelope,
|
||||
)
|
||||
from modules.features.graphicalEditor.entryPoints import find_invocation
|
||||
from modules.shared.i18nRegistry import apiRouteContext
|
||||
from modules.shared.i18nRegistry import apiRouteContext, resolveText
|
||||
routeApiMsg = apiRouteContext("routeFeatureGraphicalEditor")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _pickInvocationTitleLabel(title: Any, requestLang: Optional[str]) -> str:
|
||||
if isinstance(title, str):
|
||||
return title
|
||||
if isinstance(title, dict) and title:
|
||||
picked = (requestLang and title.get(requestLang)) or title.get("xx") or next(iter(title.values()), "")
|
||||
return str(picked) if picked else ""
|
||||
return ""
|
||||
|
||||
|
||||
def _build_execute_run_envelope(
|
||||
body: Dict[str, Any],
|
||||
workflow: Optional[Dict[str, Any]],
|
||||
|
|
@ -80,7 +71,7 @@ def _build_execute_run_envelope(
|
|||
}
|
||||
trig = trig_map.get(kind, "manual")
|
||||
title = inv.get("title") or {}
|
||||
label = _pickInvocationTitleLabel(title, requestLang)
|
||||
label = resolveText(title)
|
||||
base = default_run_envelope(
|
||||
trig,
|
||||
entry_point_id=inv.get("id"),
|
||||
|
|
@ -1052,10 +1043,7 @@ async def post_workflow_webhook(
|
|||
discoverMethods(services)
|
||||
|
||||
title = inv.get("title") or {}
|
||||
label = _pickInvocationTitleLabel(
|
||||
title,
|
||||
getattr(context.user, "language", None) if context.user else None,
|
||||
)
|
||||
label = resolveText(title)
|
||||
pl = body if isinstance(body, dict) else {}
|
||||
base = default_run_envelope(
|
||||
"webhook",
|
||||
|
|
@ -1113,10 +1101,7 @@ async def post_workflow_form_submit(
|
|||
discoverMethods(services)
|
||||
|
||||
title = inv.get("title") or {}
|
||||
label = _pickInvocationTitleLabel(
|
||||
title,
|
||||
getattr(context.user, "language", None) if context.user else None,
|
||||
)
|
||||
label = resolveText(title)
|
||||
pl = body if isinstance(body, dict) else {}
|
||||
base = default_run_envelope(
|
||||
"form",
|
||||
|
|
|
|||
|
|
@ -1252,18 +1252,16 @@ async def postTranscript(
|
|||
config = _getInstanceConfig(instanceId)
|
||||
|
||||
# Load original user context from session
|
||||
from modules.datamodels.datamodelUam import User
|
||||
|
||||
systemUser = User(id="system", username="system", email="system@poweron.swiss")
|
||||
sessionInterface = interfaceDb.getInterface(systemUser, featureInstanceId=instanceId)
|
||||
rootInterface = getRootInterface()
|
||||
rootUser = rootInterface.currentUser
|
||||
sessionInterface = interfaceDb.getInterface(rootUser, featureInstanceId=instanceId)
|
||||
session = sessionInterface.getSession(sessionId)
|
||||
mandateId = session.get("mandateId") if session else None
|
||||
startedByUserId = session.get("startedByUserId") if session else None
|
||||
|
||||
rootInterface = getRootInterface()
|
||||
originalUser = rootInterface.getUser(startedByUserId) if startedByUserId else None
|
||||
if not originalUser:
|
||||
originalUser = systemUser
|
||||
originalUser = rootUser
|
||||
|
||||
# Process transcript through the service pipeline
|
||||
from .service import TeamsbotService
|
||||
|
|
@ -1308,18 +1306,16 @@ async def postBotStatus(
|
|||
try:
|
||||
config = _getInstanceConfig(instanceId)
|
||||
|
||||
from modules.datamodels.datamodelUam import User
|
||||
|
||||
systemUser = User(id="system", username="system", email="system@poweron.swiss")
|
||||
sessionInterface = interfaceDb.getInterface(systemUser, featureInstanceId=instanceId)
|
||||
rootInterface = getRootInterface()
|
||||
rootUser = rootInterface.currentUser
|
||||
sessionInterface = interfaceDb.getInterface(rootUser, featureInstanceId=instanceId)
|
||||
session = sessionInterface.getSession(sessionId)
|
||||
mandateId = session.get("mandateId") if session else None
|
||||
startedByUserId = session.get("startedByUserId") if session else None
|
||||
|
||||
rootInterface = getRootInterface()
|
||||
originalUser = rootInterface.getUser(startedByUserId) if startedByUserId else None
|
||||
if not originalUser:
|
||||
originalUser = systemUser
|
||||
originalUser = rootUser
|
||||
|
||||
from .service import TeamsbotService
|
||||
service = TeamsbotService(originalUser, mandateId, instanceId, config)
|
||||
|
|
@ -1361,22 +1357,20 @@ async def botWebsocket(
|
|||
|
||||
# Load the original user who started the session (has RBAC roles in mandate)
|
||||
# Bot callbacks have no HTTP auth, so we reconstruct the user context from the session record.
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
|
||||
systemUser = User(id="system", username="system", email="system@poweron.swiss")
|
||||
sessionInterface = interfaceDb.getInterface(systemUser, featureInstanceId=instanceId)
|
||||
rootInterface = getRootInterface()
|
||||
rootUser = rootInterface.currentUser
|
||||
sessionInterface = interfaceDb.getInterface(rootUser, featureInstanceId=instanceId)
|
||||
session = sessionInterface.getSession(sessionId)
|
||||
mandateId = session.get("mandateId") if session else None
|
||||
startedByUserId = session.get("startedByUserId") if session else None
|
||||
|
||||
# Look up the original user (getRootInterface uses admin context, can load any user)
|
||||
rootInterface = getRootInterface()
|
||||
originalUser = rootInterface.getUser(startedByUserId) if startedByUserId else None
|
||||
|
||||
if not originalUser:
|
||||
logger.warning(f"Could not load original user {startedByUserId}, falling back to system user")
|
||||
originalUser = systemUser
|
||||
logger.warning(f"Could not load original user {startedByUserId}, falling back to root user")
|
||||
originalUser = rootUser
|
||||
|
||||
# Build effective config with the session's actual bot name.
|
||||
# The session stores the resolved bot name (from system bot or user override).
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import os
|
|||
from typing import Dict, List, Optional
|
||||
|
||||
from .accountingConnectorBase import BaseAccountingConnector
|
||||
from modules.shared.i18nRegistry import t
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -62,11 +62,11 @@ class AccountingRegistry:
|
|||
fields = []
|
||||
for f in connector.getRequiredConfigFields():
|
||||
fd = f.model_dump()
|
||||
fd["label"] = t(f.label)
|
||||
fd["label"] = resolveText(f.label)
|
||||
fields.append(fd)
|
||||
result.append({
|
||||
"connectorType": connectorType,
|
||||
"label": t(connector.getConnectorLabel()),
|
||||
"label": resolveText(connector.getConnectorLabel()),
|
||||
"configFields": fields,
|
||||
})
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -151,12 +151,7 @@ def getQuickActions(
|
|||
if role and role.roleLabel:
|
||||
userRoleLabels.add(role.roleLabel)
|
||||
|
||||
def _resolveText(multilingual, lang: str) -> str:
|
||||
if isinstance(multilingual, str):
|
||||
return multilingual
|
||||
if isinstance(multilingual, dict):
|
||||
return multilingual.get(lang) or multilingual.get("xx") or next(iter(multilingual.values()), "")
|
||||
return ""
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
filteredActions = []
|
||||
for action in QUICK_ACTIONS:
|
||||
|
|
@ -166,8 +161,8 @@ def getQuickActions(
|
|||
if context.hasSysAdminRole or required.intersection(userRoleLabels):
|
||||
resolved = {
|
||||
"id": action["id"],
|
||||
"label": _resolveText(action.get("label", {}), language),
|
||||
"description": _resolveText(action.get("description", {}), language),
|
||||
"label": resolveText(action.get("label", {})),
|
||||
"description": resolveText(action.get("description", {})),
|
||||
"icon": action.get("icon", ""),
|
||||
"color": action.get("color", ""),
|
||||
"category": action.get("category", ""),
|
||||
|
|
@ -178,14 +173,14 @@ def getQuickActions(
|
|||
if resolved["actionType"] == "agentPrompt" and "config" in resolved:
|
||||
cfg = dict(resolved["config"])
|
||||
if "uploadHint" in cfg:
|
||||
cfg["uploadHint"] = _resolveText(cfg["uploadHint"], language)
|
||||
cfg["uploadHint"] = resolveText(cfg["uploadHint"])
|
||||
resolved["config"] = cfg
|
||||
filteredActions.append(resolved)
|
||||
|
||||
filteredActions.sort(key=lambda a: a["sortOrder"])
|
||||
|
||||
resolvedCategories = [
|
||||
{"id": c["id"], "label": _resolveText(c.get("label", {}), language), "sortOrder": c.get("sortOrder", 99)}
|
||||
{"id": c["id"], "label": resolveText(c.get("label", {})), "sortOrder": c.get("sortOrder", 99)}
|
||||
for c in QUICK_ACTION_CATEGORIES
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ from modules.interfaces.interfaceAiObjects import AiObjects
|
|||
from modules.serviceCenter.core.serviceStreaming import get_event_manager
|
||||
from modules.serviceCenter.services.serviceAgent.datamodelAgent import AgentEventTypeEnum, PendingFileEdit
|
||||
from modules.shared.timeUtils import parseTimestamp
|
||||
from modules.shared.i18nRegistry import apiRouteContext, t
|
||||
from modules.shared.i18nRegistry import apiRouteContext, resolveText
|
||||
routeApiMsg = apiRouteContext("routeFeatureWorkspace")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -1467,7 +1467,7 @@ async def listFeatureConnectionTables(
|
|||
node = {
|
||||
"objectKey": obj.get("objectKey", ""),
|
||||
"tableName": meta.get("table", ""),
|
||||
"label": t(obj.get("label", "")),
|
||||
"label": resolveText(obj.get("label", "")),
|
||||
"fields": meta.get("fields", []),
|
||||
}
|
||||
if meta.get("isParent"):
|
||||
|
|
|
|||
|
|
@ -666,7 +666,7 @@ class AppObjects:
|
|||
password: str = None,
|
||||
email: str = None,
|
||||
fullName: str = None,
|
||||
language: str = "en",
|
||||
language: str = "de",
|
||||
enabled: bool = True,
|
||||
authenticationAuthority: AuthAuthority = AuthAuthority.LOCAL,
|
||||
externalId: str = None,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from modules.datamodels.datamodelFeatures import Feature, FeatureInstance
|
|||
from modules.datamodels.datamodelRbac import Role, AccessRule
|
||||
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -241,9 +242,9 @@ class FeatureInterface:
|
|||
return 0
|
||||
|
||||
from modules.features.graphicalEditor.interfaceFeatureGraphicalEditor import getGraphicalEditorInterface
|
||||
from modules.auth.authModels import SystemUser
|
||||
systemUser = SystemUser()
|
||||
geInterface = getGraphicalEditorInterface(systemUser, mandateId, instanceId)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
rootUser = getRootInterface().currentUser
|
||||
geInterface = getGraphicalEditorInterface(rootUser, mandateId, instanceId)
|
||||
|
||||
copied = 0
|
||||
for template in templateWorkflows:
|
||||
|
|
@ -251,11 +252,7 @@ class FeatureInterface:
|
|||
graphJson = graphJson.replace("{{featureInstanceId}}", instanceId)
|
||||
graph = json.loads(graphJson)
|
||||
|
||||
labelDict = template.get("label", {})
|
||||
if isinstance(labelDict, dict):
|
||||
label = labelDict.get("xx") or next(iter(labelDict.values()), "") or str(labelDict)
|
||||
else:
|
||||
label = str(labelDict)
|
||||
label = resolveText(template.get("label"))
|
||||
|
||||
geInterface.createWorkflow({
|
||||
"label": label,
|
||||
|
|
|
|||
|
|
@ -27,24 +27,12 @@ from modules.interfaces.interfaceDbApp import getRootInterface
|
|||
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
||||
from modules.security.rbacCatalog import getCatalogService
|
||||
from modules.routes.routeNotifications import create_access_change_notification
|
||||
from modules.shared.i18nRegistry import apiRouteContext
|
||||
from modules.shared.i18nRegistry import apiRouteContext, resolveText
|
||||
routeApiMsg = apiRouteContext("routeAdminFeatures")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _featureLabelPlain(label: Union[str, Dict[str, str], None], fallback: str, requestLang: Optional[str] = None) -> str:
|
||||
"""Catalog feature label as a single display/i18n key string."""
|
||||
if isinstance(label, str) and label.strip():
|
||||
return label
|
||||
if isinstance(label, dict):
|
||||
picked = (requestLang and label.get(requestLang)) or label.get("xx") or next(iter(label.values()), "")
|
||||
if picked:
|
||||
return str(picked)
|
||||
return fallback
|
||||
return fallback
|
||||
|
||||
|
||||
def _feature_instance_display_name(instance: Any) -> str:
|
||||
if instance is None:
|
||||
return ""
|
||||
|
|
@ -197,11 +185,7 @@ def get_my_feature_instances(
|
|||
featureDef = catalogService.getFeatureDefinition(instance.featureCode)
|
||||
featuresMap[featureKey] = {
|
||||
"code": instance.featureCode,
|
||||
"label": _featureLabelPlain(
|
||||
featureDef.get("label") if featureDef else None,
|
||||
instance.featureCode,
|
||||
getattr(context.user, "language", None),
|
||||
),
|
||||
"label": resolveText(featureDef.get("label") if featureDef else None),
|
||||
"icon": featureDef.get("icon", "folder") if featureDef else "folder",
|
||||
"instances": [],
|
||||
"_mandateId": mandateId # Temporary for grouping
|
||||
|
|
|
|||
|
|
@ -23,23 +23,12 @@ from modules.datamodels.datamodelRbac import AccessRuleContext, AccessRule, Role
|
|||
from modules.datamodels.datamodelMembership import UserMandate
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
|
||||
from modules.shared.i18nRegistry import apiRouteContext, t, _getLanguage
|
||||
from modules.shared.i18nRegistry import apiRouteContext, t, resolveText
|
||||
routeApiMsg = apiRouteContext("routeAdminRbacRules")
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolveTextMultilingual(value) -> str:
|
||||
"""Resolve a TextMultilingual dict to a single string for the current request language.
|
||||
Falls back to xx (source text), then any available value."""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
lang = _getLanguage()
|
||||
return value.get(lang) or value.get("xx") or next(iter(value.values()), "")
|
||||
return str(value) if value else ""
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/rbac",
|
||||
tags=["RBAC"],
|
||||
|
|
@ -922,7 +911,7 @@ def list_roles(
|
|||
result.append({
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"description": resolveText(role.description),
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"featureCode": role.featureCode,
|
||||
|
|
@ -1051,7 +1040,7 @@ def get_roles_filter_values(
|
|||
result.append({
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"description": resolveText(role.description),
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"featureCode": role.featureCode,
|
||||
|
|
@ -1168,7 +1157,7 @@ def get_role(
|
|||
return {
|
||||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"description": resolveText(role.description),
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"featureCode": role.featureCode,
|
||||
|
|
@ -1369,12 +1358,8 @@ def getCatalogObjects(
|
|||
|
||||
def _resolveLabels(objects: list) -> list:
|
||||
for obj in objects:
|
||||
raw = obj.get("label")
|
||||
if isinstance(raw, str):
|
||||
obj["label"] = t(raw)
|
||||
elif isinstance(raw, dict):
|
||||
key = raw.get("xx") or next(iter(raw.values()), "") or obj.get("objectKey", "")
|
||||
obj["label"] = t(key) if key else f"[{obj.get('objectKey', '?')}]"
|
||||
resolved = resolveText(obj.get("label"))
|
||||
obj["label"] = resolved if resolved else f"[{obj.get('objectKey', '?')}]"
|
||||
return objects
|
||||
|
||||
if context:
|
||||
|
|
|
|||
|
|
@ -24,24 +24,13 @@ from modules.datamodels.datamodelMembership import (
|
|||
)
|
||||
from modules.datamodels.datamodelFeatures import FeatureInstance, Feature
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.shared.i18nRegistry import apiRouteContext, t, _getLanguage
|
||||
from modules.shared.i18nRegistry import apiRouteContext, t, resolveText
|
||||
routeApiMsg = apiRouteContext("routeAdminUserAccessOverview")
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolveTextMultilingual(value) -> str:
|
||||
"""Resolve a TextMultilingual dict to a single string for the current request language.
|
||||
Falls back to xx (source text), then any available value."""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
lang = _getLanguage()
|
||||
return value.get(lang) or value.get("xx") or next(iter(value.values()), "")
|
||||
return str(value) if value else ""
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/admin/user-access-overview",
|
||||
tags=["Admin User Access Overview"],
|
||||
|
|
@ -309,7 +298,7 @@ def getUserAccessOverview(
|
|||
roleInfo = {
|
||||
"id": roleId,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"description": resolveText(role.description),
|
||||
"scope": scope,
|
||||
"scopePriority": _getRoleScopePriority(scope),
|
||||
"mandateId": role.mandateId,
|
||||
|
|
@ -345,7 +334,7 @@ def getUserAccessOverview(
|
|||
# Get feature info using interface method
|
||||
featureCode = instance.featureCode
|
||||
feature = interface.getFeatureByCode(featureCode)
|
||||
featureLabel = t(feature.label) if feature and feature.label else ""
|
||||
featureLabel = resolveText(feature.label) if feature and feature.label else ""
|
||||
|
||||
# Get roles for this FeatureAccess using interface method
|
||||
instanceRoleIds = interface.getRoleIdsForFeatureAccess(faId)
|
||||
|
|
@ -359,7 +348,7 @@ def getUserAccessOverview(
|
|||
roleInfo = {
|
||||
"id": roleId,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": _resolveTextMultilingual(role.description),
|
||||
"description": resolveText(role.description),
|
||||
"scope": scope,
|
||||
"scopePriority": _getRoleScopePriority(scope),
|
||||
"mandateId": role.mandateId,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from modules.auth import limiter, getRequestContext, RequestContext
|
|||
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
from modules.shared.i18nRegistry import apiRouteContext
|
||||
from modules.shared.i18nRegistry import apiRouteContext, resolveText
|
||||
routeApiMsg = apiRouteContext("routeDataUsers")
|
||||
|
||||
# Configure logger
|
||||
|
|
@ -87,9 +87,9 @@ def _extractDistinctValues(
|
|||
elif isinstance(val, (int, float)):
|
||||
values.add(str(val))
|
||||
elif isinstance(val, dict):
|
||||
text = (requestLang and val.get(requestLang)) or val.get("xx") or next(iter(val.values()), None)
|
||||
text = resolveText(val, requestLang)
|
||||
if text:
|
||||
values.add(str(text))
|
||||
values.add(text)
|
||||
else:
|
||||
values.add(str(val))
|
||||
return sorted(values, key=lambda v: v.lower())
|
||||
|
|
@ -606,7 +606,7 @@ class CreateUserRequest(BaseModel):
|
|||
username: str
|
||||
email: Optional[str] = None
|
||||
fullName: Optional[str] = None
|
||||
language: str = "en"
|
||||
language: str = "de"
|
||||
enabled: bool = True
|
||||
isSysAdmin: bool = False
|
||||
password: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -508,6 +508,24 @@ async def list_language_codes():
|
|||
return sorted(out, key=lambda x: (not x.get("isDefault"), x["code"]))
|
||||
|
||||
|
||||
@router.get("/user-language-options")
|
||||
async def list_user_language_options():
|
||||
"""Select options for User.language: all UiLanguageSets except ``xx`` (basis set).
|
||||
|
||||
Returns ``[{ \"value\": code, \"label\": name }, ...]`` for FormGenerator ``frontend_options`` URL.
|
||||
"""
|
||||
db = _publicMgmtDb()
|
||||
rows = db.getRecordset(UiLanguageSet)
|
||||
out: List[Dict[str, str]] = []
|
||||
for r in rows:
|
||||
code = r.get("id")
|
||||
if not code or code == "xx":
|
||||
continue
|
||||
lbl = (r.get("label") or "").strip() or code
|
||||
out.append({"value": code, "label": lbl})
|
||||
return sorted(out, key=lambda x: (x.get("label") or x["value"]).lower())
|
||||
|
||||
|
||||
@router.get("/sets/{code}")
|
||||
async def get_language_set(code: str):
|
||||
db = _publicMgmtDb()
|
||||
|
|
|
|||
|
|
@ -23,23 +23,11 @@ from modules.interfaces.interfaceFeatures import getFeatureInterface
|
|||
from modules.security.rbacCatalog import getCatalogService
|
||||
from modules.security.rbac import RbacClass
|
||||
from modules.security.rootAccess import getRootDbAppConnector
|
||||
from modules.shared.i18nRegistry import apiRouteContext
|
||||
from modules.shared.i18nRegistry import apiRouteContext, resolveText
|
||||
routeApiMsg = apiRouteContext("routeStore")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _storeLabelText(label: Union[str, Dict[str, str], None], fallback: str, requestLang: Optional[str] = None) -> str:
|
||||
"""Normalize catalog label to a single display/i18n key string."""
|
||||
if isinstance(label, str) and label.strip():
|
||||
return label
|
||||
if isinstance(label, dict):
|
||||
picked = (requestLang and label.get(requestLang)) or label.get("xx") or next(iter(label.values()), "")
|
||||
if picked:
|
||||
return str(picked)
|
||||
return fallback
|
||||
return fallback
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/store",
|
||||
tags=["Store"],
|
||||
|
|
@ -301,9 +289,9 @@ def listStoreFeatures(
|
|||
instances = _getUserInstancesForFeature(db, userId, featureCode, userMandateIds)
|
||||
result.append(StoreFeatureResponse(
|
||||
featureCode=featureCode,
|
||||
label=_storeLabelText(featureDef.get("label"), featureCode, getattr(context.user, "language", None)),
|
||||
label=resolveText(featureDef.get("label")),
|
||||
icon=featureDef.get("icon", "mdi-puzzle"),
|
||||
description=_storeLabelText(featureDef.get("description"), "", getattr(context.user, "language", None)),
|
||||
description=resolveText(featureDef.get("description")),
|
||||
instances=instances,
|
||||
canActivate=True,
|
||||
))
|
||||
|
|
@ -391,7 +379,7 @@ def activateStoreFeature(
|
|||
|
||||
# ── 3. Provision instance ───────────────────────────────────────
|
||||
featureInterface = getFeatureInterface(db)
|
||||
featureLabel = _storeLabelText(featureDef.get("label"), featureCode, getattr(context.user, "language", None))
|
||||
featureLabel = resolveText(featureDef.get("label"))
|
||||
instance = featureInterface.createFeatureInstance(
|
||||
featureCode=featureCode,
|
||||
mandateId=mandateId,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from modules.serviceCenter.services.serviceAgent.datamodelAgent import (
|
|||
)
|
||||
from modules.serviceCenter.services.serviceAgent.toolRegistry import ToolRegistry
|
||||
from modules.serviceCenter.services.serviceAgent.featureDataProvider import FeatureDataProvider
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -305,12 +306,7 @@ def _buildSchemaContext(
|
|||
meta = obj.get("meta", {})
|
||||
tbl = meta.get("table", "?")
|
||||
fields = meta.get("fields", [])
|
||||
label = obj.get("label", {})
|
||||
if isinstance(label, dict):
|
||||
picked = (requestLang and label.get(requestLang)) or label.get("xx") or next(iter(label.values()), "")
|
||||
labelStr = str(picked) if picked else tbl
|
||||
else:
|
||||
labelStr = str(label).strip() if isinstance(label, str) and str(label).strip() else tbl
|
||||
labelStr = resolveText(obj.get("label"), requestLang)
|
||||
tableNames.append(tbl)
|
||||
block = f" Table: {tbl} ({labelStr})"
|
||||
if fields:
|
||||
|
|
|
|||
|
|
@ -2051,7 +2051,7 @@ class StructureFiller:
|
|||
contentPartInstructions: Dict[str, Any],
|
||||
contentParts: List[ContentPart],
|
||||
userPrompt: str,
|
||||
language: str = "en",
|
||||
language: str = "de",
|
||||
outputFormat: str = "txt"
|
||||
) -> str:
|
||||
"""Baue Prompt für Chapter-Sections-Struktur-Generierung, querying renderer for accepted section types."""
|
||||
|
|
@ -2187,7 +2187,7 @@ Return only valid JSON. Do not include any explanatory text outside the JSON.
|
|||
allSections: Optional[List[Dict[str, Any]]] = None,
|
||||
sectionIndex: Optional[int] = None,
|
||||
isAggregation: bool = False,
|
||||
language: str = "en",
|
||||
language: str = "de",
|
||||
outputFormat: str = "txt",
|
||||
preExtractedText: Optional[str] = None
|
||||
) -> tuple[str, str]:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from typing import Dict, Any, List, Optional
|
|||
from modules.datamodels.datamodelExtraction import ContentPart
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||
from modules.shared.i18nRegistry import normalizePrimaryLanguageTag
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -262,18 +263,17 @@ CRITICAL:
|
|||
# Validation 3.5 & 3.6: Document language
|
||||
# Use validated currentUserLanguage (always valid, validated during user intention analysis)
|
||||
# Access via _getUserLanguage() which uses self.services.currentUserLanguage
|
||||
userPromptLanguage = self._getUserLanguage() # Uses validated currentUserLanguage infrastructure
|
||||
userPromptLanguage = normalizePrimaryLanguageTag(self._getUserLanguage(), "de")
|
||||
|
||||
if "language" not in doc or not isinstance(doc["language"], str) or len(doc["language"]) != 2:
|
||||
# AI didn't return language or invalid format - use validated currentUserLanguage
|
||||
raw_lang = doc.get("language")
|
||||
if not isinstance(raw_lang, str) or not str(raw_lang).strip():
|
||||
doc["language"] = userPromptLanguage
|
||||
if "language" not in doc:
|
||||
logger.warning(f"Document {doc.get('id')} missing language - using currentUserLanguage: {userPromptLanguage}")
|
||||
else:
|
||||
logger.warning(f"Document {doc.get('id')} has invalid language format from AI: {doc['language']}, using currentUserLanguage")
|
||||
logger.warning(f"Document {doc.get('id')} has invalid language format from AI: {doc.get('language')}, using currentUserLanguage")
|
||||
else:
|
||||
# AI returned valid language format - normalize
|
||||
doc["language"] = doc["language"].lower().strip()[:2]
|
||||
doc["language"] = normalizePrimaryLanguageTag(str(raw_lang), userPromptLanguage)
|
||||
logger.debug(f"Document {doc.get('id')} using AI-determined language: {doc['language']}")
|
||||
|
||||
# Validation 3.7: Document missing 'chapters' field
|
||||
|
|
|
|||
|
|
@ -45,77 +45,53 @@ def _getModelLabelEntry(modelName: str) -> Dict[str, Any]:
|
|||
return i18nModelLabels.get(modelName) or {}
|
||||
|
||||
|
||||
def getModelLabels(modelName: str, language: str = "en") -> Dict[str, str]:
|
||||
def getModelLabels(modelName: str) -> Dict[str, str]:
|
||||
"""Get labels for a model's attributes in the specified language.
|
||||
|
||||
Reads @i18nModel registration (German base strings); non-German languages use the i18n cache.
|
||||
Attribute values are strings; dict-shaped entries are still accepted for unusual callers.
|
||||
Reads @i18nModel registration (German base strings); resolves via resolveText().
|
||||
"""
|
||||
modelData = _getModelLabelEntry(modelName)
|
||||
attributeLabels = modelData.get("attributes", {})
|
||||
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
result: Dict[str, str] = {}
|
||||
for attr, translations in attributeLabels.items():
|
||||
if isinstance(translations, dict):
|
||||
germanKey = translations.get("xx") or next(iter(translations.values()), attr)
|
||||
result[attr] = _resolveLabel(germanKey, language)
|
||||
elif isinstance(translations, str):
|
||||
result[attr] = _resolveLabel(translations, language)
|
||||
else:
|
||||
result[attr] = f"[{attr}]"
|
||||
resolved = resolveText(translations)
|
||||
result[attr] = resolved if resolved else f"[{attr}]"
|
||||
return result
|
||||
|
||||
|
||||
def _resolveLabel(germanText: str, language: str) -> str:
|
||||
"""Resolve a German base label to the requested language via i18n cache.
|
||||
Returns [germanText] when no translation exists so missing keys are visible in the UI."""
|
||||
if language == "de":
|
||||
return germanText
|
||||
try:
|
||||
from modules.shared.i18nRegistry import _CACHE
|
||||
return _CACHE.get(language, {}).get(germanText, f"[{germanText}]")
|
||||
except ImportError:
|
||||
return germanText
|
||||
|
||||
|
||||
def _resolveOptionLabels(options, userLanguage: str):
|
||||
"""Resolve frontend_options label values to the requested language."""
|
||||
def _resolveOptionLabels(options):
|
||||
"""Resolve frontend_options label values via resolveText()."""
|
||||
if not isinstance(options, list):
|
||||
return options
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
for opt in options:
|
||||
if not isinstance(opt, dict) or "label" not in opt:
|
||||
continue
|
||||
raw = opt["label"]
|
||||
if isinstance(raw, dict):
|
||||
germanKey = raw.get("xx") or next(iter(raw.values()), str(opt.get("value", "")))
|
||||
opt["label"] = _resolveLabel(germanKey, userLanguage)
|
||||
elif isinstance(raw, str):
|
||||
opt["label"] = _resolveLabel(raw, userLanguage)
|
||||
opt["label"] = resolveText(opt["label"])
|
||||
return options
|
||||
|
||||
|
||||
def _mergedAttributeLabels(modelClass: Type[BaseModel], userLanguage: str) -> Dict[str, str]:
|
||||
def _mergedAttributeLabels(modelClass: Type[BaseModel]) -> Dict[str, str]:
|
||||
"""Merge attribute labels from model MRO (base classes first, subclass overrides)."""
|
||||
try:
|
||||
baseIdx = modelClass.__mro__.index(BaseModel)
|
||||
except ValueError:
|
||||
return getModelLabels(modelClass.__name__, userLanguage)
|
||||
return getModelLabels(modelClass.__name__)
|
||||
merged: Dict[str, str] = {}
|
||||
for cls in reversed(modelClass.__mro__[:baseIdx]):
|
||||
merged.update(getModelLabels(cls.__name__, userLanguage))
|
||||
merged.update(getModelLabels(cls.__name__))
|
||||
return merged
|
||||
|
||||
|
||||
def getModelLabel(modelName: str, language: str = "de") -> str:
|
||||
"""Get the label for a model in the specified language (see getModelLabels)."""
|
||||
def getModelLabel(modelName: str) -> str:
|
||||
"""Get the label for a model via resolveText()."""
|
||||
modelData = _getModelLabelEntry(modelName)
|
||||
modelLabel = modelData.get("model", {})
|
||||
if isinstance(modelLabel, dict):
|
||||
germanKey = modelLabel.get("xx") or next(iter(modelLabel.values()), modelName)
|
||||
return _resolveLabel(germanKey, language)
|
||||
elif isinstance(modelLabel, str):
|
||||
return _resolveLabel(modelLabel, language)
|
||||
return f"[{modelName}]"
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
resolved = resolveText(modelLabel)
|
||||
return resolved if resolved else f"[{modelName}]"
|
||||
|
||||
|
||||
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
|
||||
|
|
@ -134,8 +110,8 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
|
|||
|
||||
attributes = []
|
||||
model_name = modelClass.__name__
|
||||
labels = _mergedAttributeLabels(modelClass, userLanguage)
|
||||
model_label = getModelLabel(model_name, userLanguage)
|
||||
labels = _mergedAttributeLabels(modelClass)
|
||||
model_label = getModelLabel(model_name)
|
||||
|
||||
# Pydantic v2 only
|
||||
fields = modelClass.model_fields
|
||||
|
|
@ -273,7 +249,7 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
|
|||
"visible": frontend_visible,
|
||||
"order": len(attributes),
|
||||
"readonly": frontend_readonly,
|
||||
"options": _resolveOptionLabels(frontend_options, userLanguage),
|
||||
"options": _resolveOptionLabels(frontend_options),
|
||||
"default": field_default,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,50 @@ def t(key: str, context: str = "api", value: str = "") -> str:
|
|||
return _CACHE.get(lang, {}).get(key, f"[{key}]")
|
||||
|
||||
|
||||
def resolveText(value: Any, lang: Optional[str] = None) -> str:
|
||||
"""Resolve any value to a translated string for the current request language.
|
||||
|
||||
Accepts str, dict, TextMultilingual, or None.
|
||||
- str: translate via t() (treats as i18n key / German plaintext key)
|
||||
- dict: multilingual user content — pick ``lang`` (or current context), then ``xx``, then first value
|
||||
- object with model_dump(): convert to dict first (TextMultilingual)
|
||||
- None/empty: return ""
|
||||
|
||||
If ``lang`` is given, it temporarily overrides the context language for this call
|
||||
(used by schedulers that have an explicit user language).
|
||||
|
||||
Missing i18n translations for string keys use t()'s ``[key]`` fallback.
|
||||
"""
|
||||
if lang is not None:
|
||||
token = _CURRENT_LANGUAGE.set(lang)
|
||||
try:
|
||||
return _resolveTextImpl(value)
|
||||
finally:
|
||||
_CURRENT_LANGUAGE.reset(token)
|
||||
return _resolveTextImpl(value)
|
||||
|
||||
|
||||
def _resolveTextImpl(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, str):
|
||||
if not value.strip():
|
||||
return ""
|
||||
return t(value)
|
||||
if hasattr(value, "model_dump"):
|
||||
value = value.model_dump()
|
||||
if isinstance(value, dict):
|
||||
if not value:
|
||||
return ""
|
||||
lang = _CURRENT_LANGUAGE.get()
|
||||
text = value.get(lang) or value.get("xx")
|
||||
if text:
|
||||
return str(text)
|
||||
first = next((v for v in value.values() if v), None)
|
||||
return str(first) if first else ""
|
||||
return str(value)
|
||||
|
||||
|
||||
def apiRouteContext(routeModuleName: str):
|
||||
"""Return a callable that registers + translates HTTPException details.
|
||||
|
||||
|
|
@ -155,6 +199,23 @@ def _getLanguage() -> str:
|
|||
return _CURRENT_LANGUAGE.get()
|
||||
|
||||
|
||||
def normalizePrimaryLanguageTag(tag: str, fallback: str = "de") -> str:
|
||||
"""Primary language subtag from ``Accept-Language`` or a single BCP47 tag.
|
||||
|
||||
Supports 2-letter (ISO 639-1) and 3-letter (ISO 639-2/3) primaries such as ``gsw``.
|
||||
Strips region/variant: ``de-CH`` → ``de``, ``zh-Hans-CN`` → ``zh``.
|
||||
"""
|
||||
if not tag or not isinstance(tag, str):
|
||||
return fallback
|
||||
first = tag.split(",")[0].split(";")[0].strip()
|
||||
if not first:
|
||||
return fallback
|
||||
primary = first.split("-")[0].split("_")[0].lower()
|
||||
if primary.isalpha() and 2 <= len(primary) <= 8:
|
||||
return primary
|
||||
return fallback
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Boot: scan route files for routeApiMsg("…") calls → register eagerly
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -270,12 +331,7 @@ def _registerFeatureUiLabels():
|
|||
_REGISTRY[fl] = _I18nRegistryEntry(context="nav", value="")
|
||||
added += 1
|
||||
for uiObj in getattr(mod, "UI_OBJECTS", []) or []:
|
||||
lab = uiObj.get("label")
|
||||
if isinstance(lab, str) and lab and lab not in _REGISTRY:
|
||||
_REGISTRY[lab] = _I18nRegistryEntry(context="nav", value="")
|
||||
added += 1
|
||||
elif isinstance(lab, dict):
|
||||
base = lab.get("xx") or next(iter(lab.values()), "")
|
||||
base = _extractRegistrySourceText(uiObj.get("label"))
|
||||
if base and base not in _REGISTRY:
|
||||
_REGISTRY[base] = _I18nRegistryEntry(context="nav", value="")
|
||||
added += 1
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import logging
|
|||
from typing import Any, Dict, Optional
|
||||
|
||||
from modules.shared.eventManagement import eventManager
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
# Main loop reference for scheduling async work from job executor (may run in thread)
|
||||
_main_loop = None
|
||||
|
|
@ -214,13 +215,7 @@ def _create_schedule_handler(
|
|||
|
||||
title = (inv or {}).get("title") or {}
|
||||
requestLang: Optional[str] = getattr(event_user, "language", None)
|
||||
if isinstance(title, dict):
|
||||
picked = (requestLang and title.get(requestLang)) or title.get("xx") or next(iter(title.values()), "")
|
||||
label = str(picked) if picked else ""
|
||||
elif isinstance(title, str):
|
||||
label = title
|
||||
else:
|
||||
label = ""
|
||||
label = resolveText(title, requestLang) if title else ""
|
||||
|
||||
run_env = default_run_envelope(
|
||||
"schedule",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from typing import Dict, Any
|
|||
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.serviceCenter.services.serviceGeneration.subDocumentUtility import markdownToDocumentJson
|
||||
from modules.shared.i18nRegistry import normalizePrimaryLanguageTag
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -84,7 +85,10 @@ async def create(self, parameters: Dict[str, Any]) -> ActionResult:
|
|||
outputFormat = (parameters.get("outputFormat") or "docx").strip().lower().lstrip(".")
|
||||
title = (parameters.get("title") or "Document").strip()
|
||||
templateName = parameters.get("templateName")
|
||||
language = (parameters.get("language") or "de").strip()[:2]
|
||||
language = normalizePrimaryLanguageTag(
|
||||
str(parameters.get("language") or "de"),
|
||||
"de",
|
||||
)
|
||||
|
||||
try:
|
||||
structured_content = markdownToDocumentJson(context, title, language)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import logging
|
|||
from typing import Any, Dict, Optional
|
||||
|
||||
from modules.shared.eventManagement import eventManager
|
||||
from modules.shared.i18nRegistry import resolveText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -232,13 +233,7 @@ class WorkflowScheduler:
|
|||
|
||||
title = (inv or {}).get("title") or {}
|
||||
requestLang: Optional[str] = getattr(eventUser, "language", None)
|
||||
if isinstance(title, dict):
|
||||
picked = (requestLang and title.get(requestLang)) or title.get("xx") or next(iter(title.values()), "")
|
||||
label = str(picked) if picked else ""
|
||||
elif isinstance(title, str):
|
||||
label = title
|
||||
else:
|
||||
label = ""
|
||||
label = resolveText(title, requestLang) if title else ""
|
||||
|
||||
runEnv = default_run_envelope(
|
||||
"schedule",
|
||||
|
|
|
|||
Loading…
Reference in a new issue