wiki/c-work/4-done/2026-04-ui-i18n-dynamic-language-sets.md
2026-04-08 20:29:18 +02:00

10 KiB

UI-Mehrsprachigkeit: Dynamische Sprachsets (DB-backed i18n)

Beschreibung und Kontext

Das UI nutzt ein eigenes i18n-System (LanguageContext, t()-Hook, useLanguage). Sprachsets werden dynamisch aus der Datenbank via public API geladen — keine statischen TypeScript-Dateien mehr.

Business-Treiber: Multi-Tenant-Plattform mit Kunden in CH/DE/AT und perspektivisch FR/EN — neue Sprachen ohne Code-Änderung + Redeploy; Kunden können selbst neue Sprachen anlegen (AI-generiert).

ISO-Code Deutsch: de (ISO 639-1 Standard).

Ergebnis (Ist-Zustand nach Umsetzung)

Aspekt Umgesetzt
Sprachen Dynamischer string-Code, kein hardcoded Union-Type
Quelle DB via public API (GET /api/i18n/sets/{code})
Neue Sprache anlegen AI-generiert, async, User-ausgelöst via Admin-UI
t() Coverage Bestehende Dateien migriert; jede neue Code-Anpassung muss UI-Texte mit t() erfassen
Key-Schema Deutscher Text = Key (kein Dot-Notation-Schema)
Variable Interpolation Nativ in t(): t('Text mit {variable}', {variable: 'Wert'})
Sprachset-Verwaltung CRUD-API: get codes, add, get, update, delete, download, update-all, export, import
Admin-UI Administration → System → UI-Sprachen (/admin/languages) inkl. Export/Import
AI-Übersetzung Batch-Pipeline via AiObjects.callWithTextContext + Billing
Statische Locale-Files Entfernt (de.ts, en.ts, fr.ts gelöscht); Seed-Daten in DB

Key-Konvention: Deutscher Text = Key

Grundprinzip: Der deutsche Klartext IST der Key. Das de-Set ist trivial (Key = Value). Alle anderen Sets mappen denselben Key auf die jeweilige Übersetzung.

t('Abbrechen')                    // de: "Abbrechen", en: "Cancel", fr: "Annuler"
t('Speichern')                    // de: "Speichern", en: "Save", fr: "Enregistrer"
t('{authority} Verbindung bearbeiten', {authority: 'Google'})
                                  // de: "Google Verbindung bearbeiten"
                                  // en: "Edit Google connection"

DB-Struktur

de-Set: { "Abbrechen": "Abbrechen", "Speichern": "Speichern", ... }
en-Set: { "Abbrechen": "Cancel",    "Speichern": "Save",      ... }
fr-Set: { "Abbrechen": "Annuler",   "Speichern": "Enregistrer", ... }

Entwickler-Pflicht: t()-Tagging bei jeder Code-Änderung

Regel: Jeder neue oder geänderte UI-Text (Label, Button, Placeholder, Tooltip, Fehlermeldung) MUSS mit t('Deutscher Klartext') getaggt werden. Hardcodierte deutsche Strings im JSX sind nicht erlaubt.

Workflow für Entwickler

  1. Neuer Text: t('Mein neuer Text') verwenden — der Key wird automatisch Teil des de-Masters
  2. Text ändern: t('Alter Text')t('Neuer Text') — der alte Key verwaist, der neue fehlt in anderen Sets
  3. Variable Interpolation: t('{count} Einträge gefunden', { count: String(total) }) — Platzhalter {...} werden ersetzt
  4. Kein Plural-Framework: Separate Keys verwenden, z.B. t('1 Eintrag') vs. t('{count} Einträge', { count })
  5. Import: const { t } = useLanguage(); — in jeder Komponente die t() nutzt
  6. Sync: Admin klickt "Update All" in Administration → System → UI-Sprachen → System scannt automatisch die Codebase, synchronisiert das de-Master-Set (neue Keys rein, verwaiste raus), dann AI übersetzt fehlende Keys in allen anderen Sets

Sonderfall: gleicher Text, anderer Kontext

Falls nötig (z.B. "Offen" = "Open" vs. "Outstanding"): t('Offen (Status)') vs. t('Offen (Zustand)'). Die Klammer ist Teil des Keys und dient AI als Kontext-Hinweis.

Architektur-Übersicht

Backend (Gateway)

Datei Zweck
modules/datamodels/datamodelUiLanguage.py Pydantic-Model UiLanguageSet (id, label, keys, status, isDefault)
modules/routes/routeI18n.py API-Routen: public GET, auth POST, SysAdmin PUT/DELETE
modules/interfaces/interfaceDbManagement.py _seedUiLanguageSetsIfEmpty() — initiales DB-Seeding
modules/migration/seedData/ui_language_seed.json Seed-Daten für de, en, fr
modules/system/mainSystem.py Navigationseintrag admin-languages
scripts/build_ui_language_seed_json.py Script zur Seed-JSON-Generierung
scripts/i18n_rekey_plaintext_keys.py Script zur Migration Dot-Notation → Klartext-Keys

Frontend (Nyla)

Datei Zweck
src/locales/index.ts API-basiertes Language-Loading (kein static-import)
src/locales/types.ts Language = string (dynamisch)
src/providers/language/LanguageContext.tsx t() mit Interpolation, Fallback-Kette, availableLanguages
src/pages/admin/AdminLanguagesPage.tsx Admin-Seite mit FormGeneratorTable
src/config/pageRegistry.tsx Icon-Mapping page.admin.languages

API-Endpunkte

Methode Pfad Auth Zweck
GET /api/i18n/codes public Liste aller Sprachcodes + Status
GET /api/i18n/sets/{code} public Sprachset laden
GET /api/i18n/sets/{code}/download auth JSON-Download
POST /api/i18n/sets auth + billing Neue Sprache anlegen (async AI)
PUT /api/i18n/sets/sync-de SysAdmin de-Master aus Codebase synchronisieren (t()-Scan)
PUT /api/i18n/sets/{code} SysAdmin de-Sync + Set synchronisieren (AI für fehlende Keys)
PUT /api/i18n/sets/update-all SysAdmin de-Sync + alle Non-de-Sets synchronisieren
DELETE /api/i18n/sets/{code} SysAdmin Set löschen (nicht de)
GET /api/i18n/export SysAdmin Komplette Sprachdatenbank als JSON exportieren
POST /api/i18n/import SysAdmin JSON-Datei importieren (upsert, kein Löschen)

AI-Pipeline

  • Create: Background-Job übersetzt alle ~928 Keys in Batches à 80 via AiObjects.callWithTextContext
  • Update: Synchron — nur fehlende Keys werden per AI übersetzt, überzählige entfernt
  • Billing: Jeder AI-Call wird via BillingService.recordUsage abgerechnet (Mandats-Pool des auslösenden Users)
  • Fallback: Bei AI-Fehler wird [Deutscher Klartext] als Platzhalter gesetzt (eckige Klammern = erkennbar unübersetzt), Status incomplete
  • de-Master-Sync: Vor jedem Update/Update-All wird automatisch _syncDeMasterFromCodebase() ausgeführt — scannt alle t()-Aufrufe im Frontend, fügt neue Keys hinzu, entfernt verwaiste

de-Master-Sync aus Codebase

Bei Update, Update All und dem dedizierten Endpunkt PUT /api/i18n/sets/sync-de wird automatisch:

  1. Alle .ts/.tsx-Dateien unter frontend_nyla/src/ nach t('...')-Aufrufen gescannt
  2. Neue Keys (in Codebase, nicht in DB) → zum de-Set hinzugefügt (Key = Value = deutscher Klartext)
  3. Verwaiste Keys (in DB, nicht mehr in Codebase) → aus dem de-Set entfernt
  4. Danach erst werden die Non-de-Sets synchronisiert (fehlende Keys per AI übersetzen, überzählige entfernen)

t()-Funktion Fallback-Kette

  1. Ziel-Sprachset (z.B. en)
  2. de-Master-Set (immer geladen)
  3. Zweites Argument als String-Fallback (falls übergeben)
  4. [Key] — eckige Klammern markieren den Key als unübersetzt/fehlend, damit er im UI sofort erkennbar ist

Entscheidungen

Datum Entscheidung Begründung
2026-04-08 ISO-Code de für Deutsch ISO 639-1 Standard
2026-04-08 Eigenes t()-System beibehalten 150+ Dateien integriert; i18next wäre Overhead
2026-04-08 Deutscher Text = Key Selbst-dokumentierend, AI-Kontext eingebaut
2026-04-08 Keine Plural-Logik in t() Separate Keys reichen
2026-04-08 Keine statischen Locale-Files DB ist einzige Quelle; kein Fallback
2026-04-08 Key-Sync über Update-API Dev taggt t(), Admin klickt "Update All"
2026-04-08 AI-Batch-Übersetzung mit Billing AiObjects + BillingService.recordUsage
2026-04-08 de-Master-Sync aus Codebase Automatischer t()-Scan vor jedem Update; kein manuelles Pflegen des de-Sets
2026-04-08 Fallback [Key] statt nackter Key Eckige Klammern machen unübersetzte Texte im UI sofort sichtbar
2026-04-08 Export/Import der kompletten Sprachdatenbank Instanz-übergreifender Transfer (INT → PROD) ohne DB-Zugriff

Umsetzungs-Checkliste (abgeschlossen)

Phase 0 — t()-Tagging: Klartext-Keys + vollständige Coverage

  • AI-Scan: Replacement-Liste erstellt (read-only)
  • Script i18n_rekey_plaintext_keys.py: Replacements mechanisch ausgeführt
  • Script build_ui_language_seed_json.py: de-Master-Set extrahiert
  • en/fr-Sets migriert (mechanisch, Key-Remapping)
  • Seed-Daten als ui_language_seed.json bereitgestellt
  • Statische Locale-Files de.ts, en.ts, fr.ts entfernt

Phase 1 — Gateway: Datamodel + API

  • Datamodel UiLanguageSet (datamodelUiLanguage.py)
  • DB-Tabelle registriert (Auto-Deploy)
  • Routes routeI18n.py (7 Endpunkte)
  • createUiLanguage: Pre-flight Billing → Background-Job → AI-Batch-Übersetzung → Notification
  • updateUiLanguage: de-Master → AI-Übersetzung fehlender Keys → überzählige entfernt
  • Seed: _seedUiLanguageSetsIfEmpty() in interfaceDbManagement.py
  • AI-Pipeline: AiObjects.callWithTextContext + BillingService.recordUsage, Batch-Size 80

Phase 2 — Frontend: LanguageContext + t()

  • Language-Type → string
  • loadLanguage → API statt static-import
  • t() mit {variable}-Interpolation
  • Fallback-Kette: Ziel → de → Fallback-String → Key
  • availableLanguages + refreshAvailableLanguages
  • Sprach-Dropdown in Settings: dynamisch

Phase 3 — Admin-UI: Sprachverwaltung

  • Admin-Seite /admin/languages (AdminLanguagesPage.tsx)
  • FormGeneratorTable: Code, Label, Status, Keys-Count
  • Row-Actions: Update, Delete (nicht de), Download
  • Toolbar-Actions: Add (Billing-Warning), Update All, Export, Import
  • Navigationseintrag in mainSystem.py + pageRegistry.tsx

Querschnitt

  • RBAC: POST → auth; PUT/DELETE → SysAdmin; GET → public
  • Billing: AI-Credits via BillingService.recordUsage
  • Navigation: Admin-Route + Menüeintrag
  • LanguageContext: frontend_nyla/src/providers/language/LanguageContext.tsx
  • API-Route: gateway/modules/routes/routeI18n.py
  • Admin-Seite: frontend_nyla/src/pages/admin/AdminLanguagesPage.tsx
  • Seed-Daten: gateway/modules/migration/seedData/ui_language_seed.json