feat: module-owned i18n with merge-at-boot translation loading

Each module can now ship its own i18n/ directory with translation files
(default_de.json, default_en.json). At boot time, ModuleI18nLoader
preloads core translations then merges module translations on top via
Reflection into I18n::$strings — the t() helper stays unchanged.

- Add i18nPath property to ModuleManifest
- Add ModuleI18nLoader with Reflection-based preload
- Extract 59 module-exclusive keys from core i18n into module files
  (notifications: 8, bookmarks: 41, addressbook: 10)
- Expand TranslationKeysTest to scan modules/ and validate against
  merged core + module translations
- Add ModuleI18nContractTest for per-module de/en parity and completeness

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 22:08:34 +01:00
parent 3147009498
commit 1f0b1caf54
16 changed files with 601 additions and 133 deletions

View File

@@ -295,7 +295,6 @@
"Profile image": "Profilbild",
"Profile": "Profil",
"Profile description": "Profilbeschreibung",
"About": "Über",
"Info": "Info",
"Search results": "Suchergebnisse",
"Result": "Ergebnis",
@@ -470,10 +469,6 @@
"Status & meta": "Status & Meta",
"Audit": "Audit",
"Overview": "Übersicht",
"Quick actions": "Schnellaktionen",
"Send email": "E-Mail senden",
"Call phone": "Telefon anrufen",
"Call mobile": "Mobil anrufen",
"Choose color": "Farbe wählen",
"Be careful - this resets the tenants color": "Achtung das setzt die Primärfarbe des Mandanten zurück",
"Code": "Code",
@@ -520,13 +515,11 @@
"Department can not be created": "Abteilung kann nicht erstellt werden",
"Department can not be updated": "Abteilung kann nicht aktualisiert werden",
"Delete this department?": "Diese Abteilung wirklich löschen?",
"Delete this bookmark?": "Dieses Lesezeichen wirklich löschen?",
"Delete department": "Abteilung löschen",
"This will permanently delete this department.": "Diese Abteilung wird dauerhaft gelöscht.",
"Assigned departments": "Zugewiesene Abteilungen",
"Select departments": "Abteilungen auswählen",
"Departments for tenant": "Abteilungen für Mandant",
"No departments": "Keine Abteilungen",
"No departments available for this tenant": "Für diesen Mandanten sind keine Abteilungen verfügbar",
"Assign tenants first to select departments": "Bitte zuerst Mandanten zuweisen, um Abteilungen auszuwählen",
"No tenants available": "Keine Mandanten verfügbar",
@@ -549,7 +542,6 @@
"Delete permission": "Berechtigung löschen",
"This will permanently delete this role.": "Diese Rolle wird dauerhaft gelöscht.",
"This will permanently delete this permission.": "Diese Berechtigung wird dauerhaft gelöscht.",
"This action cannot be undone.": "Diese Aktion kann nicht rückgängig gemacht werden.",
"Description": "Beschreibung",
"Description cannot be empty": "Beschreibung darf nicht leer sein",
"Status is invalid": "Status ist ungültig",
@@ -647,7 +639,6 @@
"SMTP security": "SMTP Sicherheit",
"SMTP from address": "Absender-Adresse",
"SMTP from name": "Absendername",
"Address book": "Adressbuch",
"Mobile": "Mobil",
"Short dial": "Kurzwahl",
"setting.smtp_host": "SMTP-Serveradresse (z.B. smtp.example.com)",
@@ -659,7 +650,6 @@
"setting.smtp_from_name": "Absendername für ausgehende Nachrichten",
"High contrast": "Hoher Kontrast",
"Toggle contrast": "Kontrast umschalten",
"View in address book": "Im Adressbuch ansehen",
"Organization & Quality": "Organisation & Qualität",
"Email security": "E-Mail Sicherheit",
"API": "API",
@@ -813,8 +803,6 @@
"Please fill required custom field: %s": "Bitte Pflicht-Zusatzfeld ausfüllen: %s",
"Invalid value for %s": "Ungültiger Wert für %s",
"Please select": "Bitte auswählen",
"From": "Von",
"To": "Bis",
"perm.custom_fields.manage": "Mandanten-Zusatzfelder verwalten",
"perm.custom_fields.edit_values": "Benutzer-Zusatzfelder bearbeiten",
"Label": "Bezeichnung",
@@ -999,7 +987,6 @@
"Import another file": "Weitere Datei importieren",
"No row errors found": "Keine Zeilenfehler gefunden",
"Row": "Zeile",
"Code": "Code",
"Message": "Nachricht",
"No upload file provided": "Keine Upload-Datei übergeben",
"Upload exceeds the maximum size": "Upload überschreitet die maximale Dateigröße",
@@ -1212,7 +1199,6 @@
"Actor tenant": "Akteur-Mandant",
"Event details": "Ereignisdetails",
"Target type": "Zieltyp",
"Request ID": "Request-ID",
"IP hash": "IP-Hash",
"User agent hash": "User-Agent-Hash",
"Channel": "Kanal",
@@ -1299,45 +1285,5 @@
"Session expiring": "Sitzung läuft ab",
"Your session will expire in {countdown}. Would you like to continue?": "Ihre Sitzung läuft in {countdown} ab. Möchten Sie fortfahren?",
"Extend session": "Sitzung verlängern",
"Log out": "Abmelden",
"Bookmarks": "Lesezeichen",
"Bookmark this page": "Seite als Lesezeichen speichern",
"Save bookmark": "Lesezeichen speichern",
"Bookmark name": "Lesezeichen-Name",
"Delete bookmark": "Lesezeichen löschen",
"No bookmarks yet": "Noch keine Lesezeichen",
"New group": "Neue Gruppe",
"No group": "Keine Gruppe",
"Assign to group": "Gruppe zuweisen",
"Group name": "Gruppenname",
"Delete group": "Gruppe löschen",
"Group": "Gruppe",
"Icon": "Symbol",
"Maximum bookmarks reached": "Maximale Anzahl an Lesezeichen erreicht",
"Edit bookmark": "Lesezeichen bearbeiten",
"Save changes": "Änderungen speichern",
"Bookmark saved": "Lesezeichen gespeichert",
"Bookmark updated": "Lesezeichen aktualisiert",
"Bookmark deleted": "Lesezeichen gelöscht",
"Group created": "Gruppe erstellt",
"Group deleted": "Gruppe gelöscht",
"Group delete failed": "Gruppe konnte nicht gelöscht werden",
"Delete group and keep bookmarks?": "Gruppe löschen und enthaltene Lesezeichen behalten?",
"Bookmark action failed": "Lesezeichen-Aktion fehlgeschlagen",
"Group action failed": "Gruppen-Aktion fehlgeschlagen",
"Reorder saved": "Sortierung gespeichert",
"Reorder failed": "Sortierung fehlgeschlagen",
"Move group up": "Gruppe nach oben",
"Move group down": "Gruppe nach unten",
"Move bookmark up": "Lesezeichen nach oben",
"Move bookmark down": "Lesezeichen nach unten",
"Group actions": "Gruppenaktionen",
"Bookmark actions": "Lesezeichenaktionen",
"Bookmark name is required": "Lesezeichen-Name ist erforderlich",
"Bookmark URL is invalid": "Lesezeichen-URL ist ungültig",
"Group not found": "Gruppe nicht gefunden",
"Edit group": "Gruppe bearbeiten",
"Group updated": "Gruppe aktualisiert",
"Group name is required": "Gruppenname ist erforderlich",
"Maximum groups reached": "Maximale Anzahl an Gruppen erreicht"
"Log out": "Abmelden"
}