From 0f5d6959609923a575291509853c729c1ab36a41 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sat, 11 Apr 2026 00:24:56 +0200 Subject: [PATCH] language fixes --- modules/routes/routeI18n.py | 134 ++++++++++++++++++++---------------- 1 file changed, 73 insertions(+), 61 deletions(-) diff --git a/modules/routes/routeI18n.py b/modules/routes/routeI18n.py index 1b5fdba3..5b806d33 100644 --- a/modules/routes/routeI18n.py +++ b/modules/routes/routeI18n.py @@ -58,12 +58,15 @@ _TRANSLATE_RATE_LIMIT_MAX_RETRIES = 3 _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_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", "cs": "Čeština", "sk": "Slovenčina", "sv": "Svenska", "no": "Norsk", "da": "Dansk", "fi": "Suomi", "hu": "Magyar", "ro": "Română", @@ -487,15 +490,17 @@ async def list_language_codes(): entries = _rowEntries(r) uiCount = sum(1 for e in entries if e.get("context", "ui") == "ui") gatewayCount = len(entries) - uiCount + code = r["id"] out.append( { - "code": r["id"], + "code": code, "label": r.get("label"), "status": r.get("status"), "isDefault": bool(r.get("isDefault")), "entriesCount": len(entries), "uiCount": uiCount, "gatewayCount": gatewayCount, + "updating": code in _UPDATING_CODES, } ) 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: 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( - status_code=400, detail=routeApiMsg("Nur ISO-639-1 Zwei-Buchstaben-Codes erlaubt.") + status_code=400, detail=routeApiMsg("Nur ISO-639 Sprachcodes (2–3 Buchstaben) erlaubt.") ) 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: + _UPDATING_CODES.add(code) try: db = _publicMgmtDb() 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", message=f"Fehler bei «{code}»: {e}", ) + finally: + _UPDATING_CODES.discard(code) @router.post("/sets") @@ -678,71 +686,75 @@ async def _syncLanguageWithXx(db, code: str, userId: Optional[str], adminUser: O """ if code == "xx": raise HTTPException(status_code=400, detail=routeApiMsg("Das xx-Set wird über 'UI-Keys einlesen' aktualisiert.")) - rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code}) - if not rows: - raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden")) - xxEntries = _loadMasterXxEntries(db) - if not xxEntries: - raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden.")) + _UPDATING_CODES.add(code) + try: + rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code}) + if not rows: + raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden")) + xxEntries = _loadMasterXxEntries(db) + if not xxEntries: + raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden.")) - row = dict(rows[0]) - curEntries = _rowEntries(row) - curById = {_entryId(e): e for e in curEntries} - xxById = {_entryId(e): e for e in xxEntries} + row = dict(rows[0]) + curEntries = _rowEntries(row) + curById = {_entryId(e): e for e in curEntries} + xxById = {_entryId(e): e for e in xxEntries} - masterIds = set(xxById.keys()) - currentIds = set(curById.keys()) - removedIds = currentIds - masterIds - addedIds = masterIds - currentIds + masterIds = set(xxById.keys()) + currentIds = set(curById.keys()) + removedIds = currentIds - masterIds + addedIds = masterIds - currentIds - translatedCount = 0 - if addedIds: - toTranslate = {xxById[eid]["key"]: xxById[eid].get("value", "") for eid in addedIds} - langLabel = row.get("label") or code - billingCb = None - if adminUser: - memberIds = _userMemberMandateIds(adminUser) - if memberIds: - billingCb = _makeBillingCallback(adminUser, memberIds[0]) - try: - translated = await _translateBatch(toTranslate, langLabel, code, billingCallback=billingCb) - translatedCount = sum(1 for eid in addedIds if xxById[eid]["key"] in translated) - except Exception as e: - logger.error("AI translation during sync failed for %s: %s", code, e) - translated = {} + translatedCount = 0 + if addedIds: + toTranslate = {xxById[eid]["key"]: xxById[eid].get("value", "") for eid in addedIds} + langLabel = row.get("label") or code + billingCb = None + if adminUser: + memberIds = _userMemberMandateIds(adminUser) + if memberIds: + billingCb = _makeBillingCallback(adminUser, memberIds[0]) + try: + translated = await _translateBatch(toTranslate, langLabel, code, billingCallback=billingCb) + translatedCount = sum(1 for eid in addedIds if xxById[eid]["key"] in translated) + except Exception as e: + logger.error("AI translation during sync failed for %s: %s", code, e) + translated = {} - for eid in addedIds: - xxEntry = xxById[eid] - curById[eid] = { - "context": xxEntry["context"], - "key": xxEntry["key"], - "value": translated.get(xxEntry["key"], f"[{xxEntry['key']}]"), - } + for eid in addedIds: + xxEntry = xxById[eid] + curById[eid] = { + "context": xxEntry["context"], + "key": xxEntry["key"], + "value": translated.get(xxEntry["key"], f"[{xxEntry['key']}]"), + } - for eid in removedIds: - del curById[eid] + for eid in removedIds: + del curById[eid] - for eid in masterIds & currentIds: - curById[eid]["context"] = xxById[eid]["context"] + for eid in masterIds & currentIds: + 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() - untranslated = len(addedIds) - translatedCount - row["entries"] = newEntries - if "keys" in row: - del row["keys"] - row["status"] = "complete" if untranslated == 0 else "incomplete" - row["sysModifiedAt"] = now - row["sysModifiedBy"] = userId - db.recordModify(UiLanguageSet, code, row) - return { - "code": code, - "added": sorted({xxById[eid]["key"] for eid in addedIds}), - "removed": sorted({eid[0] for eid in removedIds}), - "translated": translatedCount, - "entriesCount": len(newEntries), - } + now = getUtcTimestamp() + untranslated = len(addedIds) - translatedCount + row["entries"] = newEntries + if "keys" in row: + del row["keys"] + row["status"] = "complete" if untranslated == 0 else "incomplete" + row["sysModifiedAt"] = now + row["sysModifiedBy"] = userId + db.recordModify(UiLanguageSet, code, row) + return { + "code": code, + "added": sorted({xxById[eid]["key"] for eid in addedIds}), + "removed": sorted({eid[0] for eid in removedIds}), + "translated": translatedCount, + "entriesCount": len(newEntries), + } + finally: + _UPDATING_CODES.discard(code) @router.put("/sets/sync-xx")