language fixes

This commit is contained in:
ValueOn AG 2026-04-11 00:24:56 +02:00
parent fc17f51752
commit 0f5d695960

View file

@ -58,12 +58,15 @@ _TRANSLATE_RATE_LIMIT_MAX_RETRIES = 3
_PROTECTED_CODES = frozenset({"xx"}) _PROTECTED_CODES = frozenset({"xx"})
# In-memory set of language codes currently being updated (sync / create).
_UPDATING_CODES: Set[str] = set()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ISO 639-1 label map (used when creating a language without explicit label) # ISO 639-1 label map (used when creating a language without explicit label)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_ISO_LABELS: Dict[str, str] = { _ISO_LABELS: Dict[str, str] = {
"de": "Deutsch", "en": "English", "fr": "Français", "it": "Italiano", "de": "Deutsch", "gsw": "Schweizerdeutsch", "en": "English", "fr": "Français", "it": "Italiano",
"es": "Español", "pt": "Português", "nl": "Nederlands", "pl": "Polski", "es": "Español", "pt": "Português", "nl": "Nederlands", "pl": "Polski",
"cs": "Čeština", "sk": "Slovenčina", "sv": "Svenska", "no": "Norsk", "cs": "Čeština", "sk": "Slovenčina", "sv": "Svenska", "no": "Norsk",
"da": "Dansk", "fi": "Suomi", "hu": "Magyar", "ro": "Română", "da": "Dansk", "fi": "Suomi", "hu": "Magyar", "ro": "Română",
@ -487,15 +490,17 @@ async def list_language_codes():
entries = _rowEntries(r) entries = _rowEntries(r)
uiCount = sum(1 for e in entries if e.get("context", "ui") == "ui") uiCount = sum(1 for e in entries if e.get("context", "ui") == "ui")
gatewayCount = len(entries) - uiCount gatewayCount = len(entries) - uiCount
code = r["id"]
out.append( out.append(
{ {
"code": r["id"], "code": code,
"label": r.get("label"), "label": r.get("label"),
"status": r.get("status"), "status": r.get("status"),
"isDefault": bool(r.get("isDefault")), "isDefault": bool(r.get("isDefault")),
"entriesCount": len(entries), "entriesCount": len(entries),
"uiCount": uiCount, "uiCount": uiCount,
"gatewayCount": gatewayCount, "gatewayCount": gatewayCount,
"updating": code in _UPDATING_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"]))
@ -520,9 +525,9 @@ class CreateLanguageBody(BaseModel):
def _validate_iso2_code(code: str) -> str: def _validate_iso2_code(code: str) -> str:
c = code.strip().lower() c = code.strip().lower()
if not re.fullmatch(r"[a-z]{2}", c): if not re.fullmatch(r"[a-z]{2,3}", c):
raise HTTPException( raise HTTPException(
status_code=400, detail=routeApiMsg("Nur ISO-639-1 Zwei-Buchstaben-Codes erlaubt.") status_code=400, detail=routeApiMsg("Nur ISO-639 Sprachcodes (23 Buchstaben) erlaubt.")
) )
return c return c
@ -536,6 +541,7 @@ def _run_create_language_job(userId: str, code: str, label: str, currentUser: Us
async def _run_create_language_job_async(userId: str, code: str, label: str, currentUser: User, mandateId: str) -> None: async def _run_create_language_job_async(userId: str, code: str, label: str, currentUser: User, mandateId: str) -> None:
_UPDATING_CODES.add(code)
try: try:
db = _publicMgmtDb() db = _publicMgmtDb()
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code}) rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
@ -590,6 +596,8 @@ async def _run_create_language_job_async(userId: str, code: str, label: str, cur
title="Sprachset fehlgeschlagen", title="Sprachset fehlgeschlagen",
message=f"Fehler bei «{code}»: {e}", message=f"Fehler bei «{code}»: {e}",
) )
finally:
_UPDATING_CODES.discard(code)
@router.post("/sets") @router.post("/sets")
@ -678,71 +686,75 @@ async def _syncLanguageWithXx(db, code: str, userId: Optional[str], adminUser: O
""" """
if code == "xx": if code == "xx":
raise HTTPException(status_code=400, detail=routeApiMsg("Das xx-Set wird über 'UI-Keys einlesen' aktualisiert.")) raise HTTPException(status_code=400, detail=routeApiMsg("Das xx-Set wird über 'UI-Keys einlesen' aktualisiert."))
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code}) _UPDATING_CODES.add(code)
if not rows: try:
raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden")) rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
xxEntries = _loadMasterXxEntries(db) if not rows:
if not xxEntries: raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden.")) xxEntries = _loadMasterXxEntries(db)
if not xxEntries:
raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden."))
row = dict(rows[0]) row = dict(rows[0])
curEntries = _rowEntries(row) curEntries = _rowEntries(row)
curById = {_entryId(e): e for e in curEntries} curById = {_entryId(e): e for e in curEntries}
xxById = {_entryId(e): e for e in xxEntries} xxById = {_entryId(e): e for e in xxEntries}
masterIds = set(xxById.keys()) masterIds = set(xxById.keys())
currentIds = set(curById.keys()) currentIds = set(curById.keys())
removedIds = currentIds - masterIds removedIds = currentIds - masterIds
addedIds = masterIds - currentIds addedIds = masterIds - currentIds
translatedCount = 0 translatedCount = 0
if addedIds: if addedIds:
toTranslate = {xxById[eid]["key"]: xxById[eid].get("value", "") for eid in addedIds} toTranslate = {xxById[eid]["key"]: xxById[eid].get("value", "") for eid in addedIds}
langLabel = row.get("label") or code langLabel = row.get("label") or code
billingCb = None billingCb = None
if adminUser: if adminUser:
memberIds = _userMemberMandateIds(adminUser) memberIds = _userMemberMandateIds(adminUser)
if memberIds: if memberIds:
billingCb = _makeBillingCallback(adminUser, memberIds[0]) billingCb = _makeBillingCallback(adminUser, memberIds[0])
try: try:
translated = await _translateBatch(toTranslate, langLabel, code, billingCallback=billingCb) translated = await _translateBatch(toTranslate, langLabel, code, billingCallback=billingCb)
translatedCount = sum(1 for eid in addedIds if xxById[eid]["key"] in translated) translatedCount = sum(1 for eid in addedIds if xxById[eid]["key"] in translated)
except Exception as e: except Exception as e:
logger.error("AI translation during sync failed for %s: %s", code, e) logger.error("AI translation during sync failed for %s: %s", code, e)
translated = {} translated = {}
for eid in addedIds: for eid in addedIds:
xxEntry = xxById[eid] xxEntry = xxById[eid]
curById[eid] = { curById[eid] = {
"context": xxEntry["context"], "context": xxEntry["context"],
"key": xxEntry["key"], "key": xxEntry["key"],
"value": translated.get(xxEntry["key"], f"[{xxEntry['key']}]"), "value": translated.get(xxEntry["key"], f"[{xxEntry['key']}]"),
} }
for eid in removedIds: for eid in removedIds:
del curById[eid] del curById[eid]
for eid in masterIds & currentIds: for eid in masterIds & currentIds:
curById[eid]["context"] = xxById[eid]["context"] curById[eid]["context"] = xxById[eid]["context"]
newEntries = sorted(curById.values(), key=lambda e: (e["key"].lower(), e.get("context", ""))) newEntries = sorted(curById.values(), key=lambda e: (e["key"].lower(), e.get("context", "")))
now = getUtcTimestamp() now = getUtcTimestamp()
untranslated = len(addedIds) - translatedCount untranslated = len(addedIds) - translatedCount
row["entries"] = newEntries row["entries"] = newEntries
if "keys" in row: if "keys" in row:
del row["keys"] del row["keys"]
row["status"] = "complete" if untranslated == 0 else "incomplete" row["status"] = "complete" if untranslated == 0 else "incomplete"
row["sysModifiedAt"] = now row["sysModifiedAt"] = now
row["sysModifiedBy"] = userId row["sysModifiedBy"] = userId
db.recordModify(UiLanguageSet, code, row) db.recordModify(UiLanguageSet, code, row)
return { return {
"code": code, "code": code,
"added": sorted({xxById[eid]["key"] for eid in addedIds}), "added": sorted({xxById[eid]["key"] for eid in addedIds}),
"removed": sorted({eid[0] for eid in removedIds}), "removed": sorted({eid[0] for eid in removedIds}),
"translated": translatedCount, "translated": translatedCount,
"entriesCount": len(newEntries), "entriesCount": len(newEntries),
} }
finally:
_UPDATING_CODES.discard(code)
@router.put("/sets/sync-xx") @router.put("/sets/sync-xx")