# Coding-Konventionen ## Naming - Alle internen Funktionen beginnen mit `_` Prefix (nicht exportierbar) - **camelCase** fuer Variablen und Funktionsnamen (kein snake_case) - **PascalCase** fuer Klassen und Pydantic-Models - Dateien: `camelCase` fuer Module (z.B. `mainServiceAi.py`, `routeBilling.py`) ## i18n Grundprinzip: Uebersetzen an der Quelle Jeder Text wird **an der Quelle** uebersetzt. Backend-Strings werden im Backend uebersetzt, Frontend-Strings im Frontend. Der Empfaenger rendert direkt — keine doppelte Uebersetzung, keine Checks. **Backend liefert IMMER in der korrekten Sprache.** Das Frontend muss nichts pruefen oder nochmals uebersetzen. | Text-Quelle | Wer uebersetzt | Empfaenger | |---|---|---| | Frontend-Komponente (Button, Label, h1) | Frontend: `t('Speichern')` | rendert direkt | | Backend statische Struktur (Navigation, Katalog) | Backend: `t()` registriert Key, `resolveText()` liefert zur Request-Zeit | Frontend: `item.uiLabel` direkt rendern | | Backend API-Response (Fehlermeldung, Erfolg) | Backend: `t("Zugriff verweigert")` | Frontend: `error.detail` direkt rendern | | Backend DB-Wert (TextMultilingual) | Backend: `resolveText(role.description)` | Frontend: `role.description` direkt rendern | **Kernregel:** Strings vom Backend sind **immer bereits uebersetzt**. Das Frontend darf `t()` **nie** auf Backend-Werte anwenden — weder `t(item.label)` noch `t(variable)`. Keine `typeof === 'object'` Checks, keine Fallback-Ketten. Nur eigene Frontend-Literale wie `t('Speichern')`. **Redundanz vermeiden:** Derselbe Text darf nicht an zwei Stellen mit `t()` getaggt werden. ## Frontend (React/TypeScript) - Keine Browser-Dialoge (`alert`, `confirm`, `prompt`) -- stattdessen `useConfirm()` / `usePrompt()` Hooks - CSS Modules fuer Styling - Hooks-Pattern fuer State und API-Zugriffe (`useApiRequest`, `useBilling`, etc.) - Fehler propagieren -- keine stillen Fallbacks bei kritischen Pfaden ### i18n-Pflicht: `t()` fuer alle UI-Texte Jeder sichtbare Text im UI (Labels, Buttons, Placeholders, Tooltips, Fehlermeldungen) **muss** mit `t()` getaggt werden. Hardcodierte deutsche Strings im JSX sind nicht erlaubt. ```tsx import { useLanguage } from '../../providers/language/LanguageContext'; const { t } = useLanguage(); // Einfacher Text t('Speichern') // Mit Variablen-Interpolation t('{count} Eintraege gefunden', { count: String(total) }) // Gleicher Text, anderer Kontext → Klammer als Kontext-Hinweis t('Offen (Status)') // vs. t('Offen (Zustand)') ``` - **Key = deutscher Klartext** (kein Dot-Notation-Schema) - **`t()` NUR mit String-Literalen** — `t(variable)` ist verboten. Backend-Werte (Navigation-Labels, Feature-Labels, Role-Descriptions etc.) sind bereits uebersetzt und werden direkt gerendert: ```tsx // FALSCH - Backend-Wert nochmal durch t() label: t(item.uiLabel) // Backend hat schon uebersetzt!

{t(plan.title)}

// Backend hat schon uebersetzt! // RICHTIG - Backend-Wert direkt rendern label: item.uiLabel

{plan.title}

``` - Fuer statische Frontend-Maps (Wochentage, Monatsnamen, Status-Labels) wird `t()` per `switch`-Funktion mit Literalen aufgerufen, nicht ueber Map-Lookup: ```tsx // FALSCH - t() mit Variable const labels: Record = { budget: 'Budget-Vergleich', kpi: 'KPI-Dashboard' }; {t(labels[tabId])} // RICHTIG - t() mit String-Literal per switch function _tabLabel(tabId: string, t: (k: string) => string): string { switch (tabId) { case 'budget': return t('Budget-Vergleich'); case 'kpi': return t('KPI-Dashboard'); default: return tabId; } } ``` - Kein Plural-Framework -- separate Keys verwenden: `t('1 Eintrag')` vs. `t('{count} Eintraege', { count })` - Fehlende Keys erscheinen als `[Text]` (eckige Klammern = unuebersetzt) - Admin synchronisiert Sprachsets ueber Administration → System → UI-Sprachen → "Alle aktualisieren" #### Platzhalter-Namen sind Code, nicht Text Die Token in `{...}` (z. B. `{konten}`, `{count}`) sind interne Variablen-Namen und werden zur Laufzeit durch das Param-Objekt ersetzt. **Sie duerfen nie uebersetzt werden** — auch wenn sie wie deutsche oder englische Woerter aussehen. ```tsx // Quelle (immer Deutsch, weil Source-Sprache): t('{konten} Konten, {buchungen} Buchungen', { konten: '215', buchungen: '72' }) // Korrekte EN-Uebersetzung in der DB: '{konten} accounts, {buchungen} entries' // ^^^^^^^ ^^^^^^^^^^^ <-- Token bleiben Deutsch! // FALSCH (wuerde zu rohem '{accounts}' im UI fuehren): '{accounts} accounts, {entries} entries' ``` Der AI-Uebersetzer verletzt diese Regel ab und zu, deshalb laeuft in `routeI18n._enforcePlaceholdersOnBatch` und `i18nRegistry._loadCache` ein deterministischer Guard, der bei gleicher Token-Anzahl die Source-Namen positionsweise wiederherstellt. Bei abweichender Anzahl bleibt der Wert unangetastet — das muss der Reviewer manuell pruefen. ## Backend (FastAPI/Python) - **Pydantic-Models** als einzige Quelle fuer UI-Feld-Definitionen - **`PowerOnModel`** als Basis mit System-Audit-Feldern (`sysCreatedAt`, `sysCreatedBy`, `sysModifiedAt`, `sysModifiedBy`) - Fehler propagieren -- Exceptions explizit werfen, nicht schlucken - Config ueber `APP_CONFIG` (aus `modules/shared/configuration.py`) ### Async-Routen / Background-Jobs: niemals den Event-Loop blockieren Alle FastAPI-`async def`-Routen und alle Background-Job-Handler (`registerJobHandler`) laufen auf dem **gemeinsamen** Event-Loop des Workers. Synchroner, blockierender Code (psycopg2, requests, time.sleep, ...) friert damit den ganzen Worker ein -- inklusive Health-Checks. Eine grosse Schleife mit `db.recordCreate(...)` reicht, um die Instanz fuer Minuten unerreichbar zu machen. **Regeln:** - HTTP / IO mit nativem `await` aufrufen (httpx, aiohttp, asyncpg, ...). - Sync-Code (psycopg2, lange CPU-Schleifen) **immer** in `await asyncio.to_thread(_syncFn, ...)` verpacken. - Bei grossen Datenmengen (>100 Zeilen) **niemals** `recordCreate` / `recordDelete` in der Schleife aufrufen. Stattdessen die Bulk-Methoden des Connectors verwenden: - `db.recordCreateBulk(model, rows)` -> ein `execute_values` + ein COMMIT - `db.recordDeleteWhere(model, recordFilter)` -> ein `DELETE WHERE ...` - Bei laengeren Phasen Heartbeat-Logs alle 500-1000 Items, damit Hangs im Log sichtbar sind. Referenz-Implementation: `modules/features/trustee/accounting/accountingDataSync.py` (jede Phase: `await connector.fetch(...)` -> `await asyncio.to_thread(self._persistXxx, ...)`). ### Debug-File-Dumps: nur DEV, niemals INT/PROD Code, der zu Debug-Zwecken raw payloads / Prompts / Sync-Daten auf die Disk schreibt, **muss** ein dediziertes Env-Flag pruefen. Pattern: - `APP_DEBUG__ENABLED` (`True` nur in `env_dev.env`) - `APP_DEBUG__DIR` (gateway-relativer Pfad ist OK; absoluter Pfad nur fuer DEV-Workstations) Beispiele: `APP_DEBUG_CHAT_WORKFLOW_*`, `APP_DEBUG_ACCOUNTING_SYNC_*`. Hardcoded Windows-Pfade wie `D:/...` sind verboten (laufen auf Linux als Relativ-Pfad an und schreiben unkontrolliert ins Dateisystem). ### i18n-Pflicht: `t()` fuer alle UI-sichtbaren Gateway-Texte Jeder Text der im Frontend angezeigt wird (HTTPException-Details, API-Response-Messages, Erfolgs-/Fehlermeldungen) **muss** mit `t()` getaggt werden (nur **String-Literale** in `t()`). **Dynamische Werte** (Feld aus DB, Katalog, verschachteltes Dict): `resolveText(wert)` aus `modules.shared.i18nRegistry` verwenden — nicht `t(variable)`. `resolveText` akzeptiert optional `lang="fr"` fuer Kontexte ohne Request-Middleware (z.B. Scheduler mit expliziter Nutzersprache). ```python from modules.shared.i18nRegistry import t # Fehlermeldung (context automatisch = "api") raise HTTPException(status_code=403, detail=t("Zugriff verweigert", "api.routeSecurity", "Fehlermeldung bei fehlendem Zugriff")) # Erfolgsmeldung return {"message": t("Datei erfolgreich hochgeladen", "api.routeFiles", "Bestaetigung nach Datei-Upload")} ``` **Nicht** mit `t()` taggen: Log-Eintraege, AI-Prompts, interne technische Fehlermeldungen. ### Statische Dicts mit i18n-Keys: t() registriert, resolveText() liefert Statische Dicts (Navigation, Kataloge) haben **zwei Schritte**: 1. **Import-Zeit:** `t()` im Dict **registriert** den Key (und gibt den deutschen String zurueck, der im Dict gespeichert wird) 2. **Request-Zeit:** `resolveText()` in der Route nimmt den deutschen String und liefert die Uebersetzung fuer die aktuelle Sprache ```python # mainSystem.py — t() registriert Keys bei Import-Zeit from modules.shared.i18nRegistry import t NAVIGATION_SECTIONS = [ {"title": t("Meine Sicht"), "items": [ {"label": t("Uebersicht"), "objectKey": "ui.system.home", ...}, ]}, ] ``` ```python # routeSystem.py — resolveText() uebersetzt zur Request-Zeit from modules.shared.i18nRegistry import resolveText def _formatBlockItem(item): return {"uiLabel": resolveText(item["label"]), ...} ``` **Warum zwei Schritte?** `t()` wird bei Import-Zeit ausgewertet — der Rueckgabewert ist immer deutsch (Default-Sprache). Der Wert im Dict ist daher ein fester deutscher String. `resolveText()` nimmt diesen deutschen String zur Request-Zeit und liefert die korrekte Uebersetzung. **Warum Modul-Ebene?** `t()` registriert Keys nur beim ersten Aufruf. Wenn `t()` nur in einer Funktion steht, wird der Key erst beim ersten Request registriert — **nach** dem Boot-Sync. Der Key fehlt dann im `xx`-Set und kann nicht uebersetzt werden. Im UI erscheint er als `[Schluessel]` (eckige Klammern = Cache-Miss). **Pflicht-Hook fuer dynamisch geladene Quellen:** Plugin-/Lazy-Module (z.B. Connector-Plugins, dynamisch entdeckte Klassen) deren `t()`-Aufrufe in einer Methode stecken (nicht auf Modul-Ebene), brauchen einen eigenen `_register…Labels()`-Hook in `_syncRegistryToDb()` (`modules/shared/i18nRegistry.py`). Beispiel: `_registerAccountingConnectorLabels()` importiert die Accounting-Connector-Registry und ruft `getRequiredConfigFields()` einmal beim Boot, damit die Field-Labels im `xx`-Set landen. Analog zu `_registerNodeLabels`, `_registerRbacLabels`, etc. ### TextMultilingual-Felder: Backend loest auf via resolveText() `TextMultilingual`-Felder (User-Content in mehreren Sprachen, z.B. `Role.description`) werden im Backend via `resolveText()` aufgeloest bevor sie ans Frontend geliefert werden. Das Frontend erhaelt immer einen **String**, nie ein Dict. ```python from modules.shared.i18nRegistry import resolveText # In Route-Handlern: "description": resolveText(role.description), "label": resolveText(featureDef.get("label")) or featureCode, ``` **Keine lokalen Resolve-Helfer:** `_resolveTextMultilingual()`, `_resolveLabel()`, `_storeLabelText()`, `_featureLabelPlain()`, `_pickInvocationTitleLabel()` etc. sind verboten. Immer `resolveText()` aus `i18nRegistry` verwenden. ### Frontend: Keine Dict-Fallbacks fuer Labels Das Frontend darf **keine** `label?.de || label?.en || Object.values(...)` Ketten verwenden. Wenn das Backend korrekt arbeitet, sind alle Labels Strings. Defensive `typeof === 'object'` Checks verdecken Backend-Fehler und sind verboten. ```tsx // FALSCH - verdeckt Backend-Fehler const name = typeof m.name === 'object' ? m.name.de || m.name.en : m.name; // RICHTIG - Backend liefert String const name = m.label || m.name || m.id; ``` Fuer Route-Module gibt es den Shorthand `apiRouteContext`: ```python from modules.shared.i18nRegistry import apiRouteContext routeApiMsg = apiRouteContext("routeBilling") raise HTTPException(status_code=403, detail=routeApiMsg("Zugriff verweigert")) ``` ### Pydantic-Models: `@i18nModel` Decorator Pflicht Jedes Pydantic-Model das im UI angezeigt wird (Tabellen, Formulare) **muss** den `@i18nModel` Decorator haben. Feld-Labels werden in `json_schema_extra["label"]` definiert. ```python from modules.shared.i18nRegistry import i18nModel @i18nModel("Benutzer") class User(PowerOnModel): name: str = Field( description="Full name of the user", json_schema_extra={"label": "Name", "frontend_type": "text"} ) email: str = Field( description="Email address for login and notifications", json_schema_extra={"label": "E-Mail-Adresse", "frontend_type": "text"} ) ``` - `@i18nModel("Deutscher Modelname")` -- AI-Kontext kommt automatisch aus dem Class-Docstring - `json_schema_extra={"label": "Deutscher Feldname"}` -- Pflicht fuer jedes UI-sichtbare Feld - `Field(description=...)` -- wird als AI-Kontext fuer die Uebersetzung verwendet - **Kein** `registerModelLabels()` mehr verwenden (entfernt) ### Feature-Module: Labels als deutsche Basis-Strings `DATA_OBJECTS`, `RESOURCE_OBJECTS` und `UI_OBJECTS` Labels verwenden **deutsche Klartext-Strings** als Basis-Key. Labels werden beim Boot automatisch ueber `_registerRbacLabels()` registriert und erscheinen im xx-Basisset. Bei Runtime werden sie via `resolveText()` aufgeloest. ```python DATA_OBJECTS = [ { "objectKey": "data.uam.UserInDB", "label": "Benutzer", "meta": {"table": "UserInDB", "namespace": "uam"} }, ] ``` Legacy `{en, de, fr}`-Dicts werden via `_extractRegistrySourceText()` auf den `xx`-Key reduziert. Neue Eintraege: immer **String**, kein Dict. ## Projektstruktur Gateway ``` gateway/ app.py # FastAPI-App, Middleware, Startup config.ini # Statische Konfiguration modules/ auth/ # JWT, OAuth, CSRF, Authentication datamodels/ # Pydantic-Models (zentrale Quelle) features/ # Feature-Module (workspace, automation, ...) / main.py # FEATURE_CODE, Registrierung routeFeature.py # HTTP-Endpunkte interfaceFeature.py # DB-Interface datamodelFeature.py # Feature-spezifische Models interfaces/ # DB-Interfaces (CRUD, Queries) routes/ # Core-Routes (billing, admin, GDPR, ...) security/ # RBAC-Engine serviceCenter/ core/ # serviceSecurity, serviceUtils, serviceStreaming services/ # serviceAi, serviceChat, serviceAgent, ... registry.py # Service-Registrierung und Dependencies shared/ # configuration.py, Utilities system/ # registry.py (Feature-Discovery) workflows/ methods/ # Unified Action Library processing/ # WorkflowProcessor, Modes automation/ # v1 Runtime automation2/ # v2 Engine connectors/ # Externe Systeme (DB, SharePoint, Jira, ...) aicore/ # AI-Provider-Plugins, Model-Selector ``` ## Anti-Patterns - Keine impliziten Type-Conversions in API-Responses - Keine DB-Queries in Route-Handlern (immer ueber Interfaces) - Kein direkter `os.environ`-Zugriff -- immer `APP_CONFIG` - Keine hartkodierten Secrets -- verschluesselt in Env-Dateien