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")