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