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
- Neuer Text:
t('Mein neuer Text')verwenden — der Key wird automatisch Teil desde-Masters - Text ändern:
t('Alter Text')→t('Neuer Text')— der alte Key verwaist, der neue fehlt in anderen Sets - Variable Interpolation:
t('{count} Einträge gefunden', { count: String(total) })— Platzhalter{...}werden ersetzt - Kein Plural-Framework: Separate Keys verwenden, z.B.
t('1 Eintrag')vs.t('{count} Einträge', { count }) - Import:
const { t } = useLanguage();— in jeder Komponente diet()nutzt - 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.recordUsageabgerechnet (Mandats-Pool des auslösenden Users) - Fallback: Bei AI-Fehler wird
[Deutscher Klartext]als Platzhalter gesetzt (eckige Klammern = erkennbar unübersetzt), Statusincomplete - de-Master-Sync: Vor jedem Update/Update-All wird automatisch
_syncDeMasterFromCodebase()ausgeführt — scannt allet()-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:
- Alle
.ts/.tsx-Dateien unterfrontend_nyla/src/nacht('...')-Aufrufen gescannt - Neue Keys (in Codebase, nicht in DB) → zum
de-Set hinzugefügt (Key = Value = deutscher Klartext) - Verwaiste Keys (in DB, nicht mehr in Codebase) → aus dem
de-Set entfernt - Danach erst werden die Non-
de-Sets synchronisiert (fehlende Keys per AI übersetzen, überzählige entfernen)
t()-Funktion Fallback-Kette
- Ziel-Sprachset (z.B.
en) de-Master-Set (immer geladen)- Zweites Argument als String-Fallback (falls übergeben)
[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.jsonbereitgestellt - Statische Locale-Files
de.ts,en.ts,fr.tsentfernt
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 → NotificationupdateUiLanguage:de-Master → AI-Übersetzung fehlender Keys → überzählige entfernt- Seed:
_seedUiLanguageSetsIfEmpty()ininterfaceDbManagement.py - AI-Pipeline:
AiObjects.callWithTextContext+BillingService.recordUsage, Batch-Size 80
Phase 2 — Frontend: LanguageContext + t() ✅
Language-Type →stringloadLanguage→ API statt static-importt()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
Links
- 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