195 lines
10 KiB
Markdown
195 lines
10 KiB
Markdown
<!-- status: done -->
|
|
<!-- started: 2026-04-08 -->
|
|
<!-- completed: 2026-04-08 -->
|
|
<!-- component: gateway | frontend-nyla -->
|
|
|
|
# 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.
|
|
|
|
```tsx
|
|
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 ✅
|
|
|
|
- [x] AI-Scan: Replacement-Liste erstellt (read-only)
|
|
- [x] Script `i18n_rekey_plaintext_keys.py`: Replacements mechanisch ausgeführt
|
|
- [x] Script `build_ui_language_seed_json.py`: `de`-Master-Set extrahiert
|
|
- [x] `en`/`fr`-Sets migriert (mechanisch, Key-Remapping)
|
|
- [x] Seed-Daten als `ui_language_seed.json` bereitgestellt
|
|
- [x] Statische Locale-Files `de.ts`, `en.ts`, `fr.ts` entfernt
|
|
|
|
### Phase 1 — Gateway: Datamodel + API ✅
|
|
|
|
- [x] Datamodel `UiLanguageSet` (`datamodelUiLanguage.py`)
|
|
- [x] DB-Tabelle registriert (Auto-Deploy)
|
|
- [x] Routes `routeI18n.py` (7 Endpunkte)
|
|
- [x] `createUiLanguage`: Pre-flight Billing → Background-Job → AI-Batch-Übersetzung → Notification
|
|
- [x] `updateUiLanguage`: `de`-Master → AI-Übersetzung fehlender Keys → überzählige entfernt
|
|
- [x] Seed: `_seedUiLanguageSetsIfEmpty()` in `interfaceDbManagement.py`
|
|
- [x] AI-Pipeline: `AiObjects.callWithTextContext` + `BillingService.recordUsage`, Batch-Size 80
|
|
|
|
### Phase 2 — Frontend: LanguageContext + t() ✅
|
|
|
|
- [x] `Language`-Type → `string`
|
|
- [x] `loadLanguage` → API statt static-import
|
|
- [x] `t()` mit `{variable}`-Interpolation
|
|
- [x] Fallback-Kette: Ziel → `de` → Fallback-String → Key
|
|
- [x] `availableLanguages` + `refreshAvailableLanguages`
|
|
- [x] Sprach-Dropdown in Settings: dynamisch
|
|
|
|
### Phase 3 — Admin-UI: Sprachverwaltung ✅
|
|
|
|
- [x] Admin-Seite `/admin/languages` (`AdminLanguagesPage.tsx`)
|
|
- [x] FormGeneratorTable: Code, Label, Status, Keys-Count
|
|
- [x] Row-Actions: Update, Delete (nicht `de`), Download
|
|
- [x] Toolbar-Actions: Add (Billing-Warning), Update All, Export, Import
|
|
- [x] Navigationseintrag in `mainSystem.py` + `pageRegistry.tsx`
|
|
|
|
### Querschnitt ✅
|
|
|
|
- [x] RBAC: POST → auth; PUT/DELETE → SysAdmin; GET → public
|
|
- [x] Billing: AI-Credits via `BillingService.recordUsage`
|
|
- [x] 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`
|