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": "Profile image",
"Profile": "Profile",
"Profile description": "Profile description",
"About": "About",
"Info": "Info",
"Search results": "Search results",
"Result": "Result",
@@ -470,10 +469,6 @@
"Status & meta": "Status & meta",
"Audit": "Audit",
"Overview": "Overview",
"Quick actions": "Quick actions",
"Send email": "Send email",
"Call phone": "Call phone",
"Call mobile": "Call mobile",
"Choose color": "Choose color",
"Be careful - this resets the tenants color": "Be careful - this resets the tenant color",
"Code": "Code",
@@ -520,13 +515,11 @@
"Department can not be created": "Department can not be created",
"Department can not be updated": "Department can not be updated",
"Delete this department?": "Delete this department?",
"Delete this bookmark?": "Delete this bookmark?",
"Delete department": "Delete department",
"This will permanently delete this department.": "This will permanently delete this department.",
"Assigned departments": "Assigned departments",
"Select departments": "Select departments",
"Departments for tenant": "Departments for tenant",
"No departments": "No departments",
"No departments available for this tenant": "No departments available for this tenant",
"Assign tenants first to select departments": "Assign tenants first to select departments",
"No tenants available": "No tenants available",
@@ -549,7 +542,6 @@
"Delete permission": "Delete permission",
"This will permanently delete this role.": "This will permanently delete this role.",
"This will permanently delete this permission.": "This will permanently delete this permission.",
"This action cannot be undone.": "This action cannot be undone.",
"Description": "Description",
"Description cannot be empty": "Description cannot be empty",
"Status is invalid": "Status is invalid",
@@ -647,7 +639,6 @@
"SMTP security": "SMTP security",
"SMTP from address": "SMTP from address",
"SMTP from name": "SMTP from name",
"Address book": "Address book",
"Mobile": "Mobile",
"Short dial": "Short dial",
"setting.smtp_host": "SMTP server host (e.g. smtp.example.com)",
@@ -659,7 +650,6 @@
"setting.smtp_from_name": "From name for outgoing emails",
"High contrast": "High contrast",
"Toggle contrast": "Toggle contrast",
"View in address book": "View in address book",
"Organization & Quality": "Organization & Quality",
"Email security": "Email security",
"API": "API",
@@ -813,8 +803,6 @@
"Please fill required custom field: %s": "Please fill required custom field: %s",
"Invalid value for %s": "Invalid value for %s",
"Please select": "Please select",
"From": "From",
"To": "To",
"perm.custom_fields.manage": "Manage tenant custom fields",
"perm.custom_fields.edit_values": "Edit user custom field values",
"Label": "Label",
@@ -999,7 +987,6 @@
"Import another file": "Import another file",
"No row errors found": "No row errors found",
"Row": "Row",
"Code": "Code",
"Message": "Message",
"No upload file provided": "No upload file provided",
"Upload exceeds the maximum size": "Upload exceeds the maximum size",
@@ -1212,7 +1199,6 @@
"Actor tenant": "Actor tenant",
"Event details": "Event details",
"Target type": "Target type",
"Request ID": "Request ID",
"IP hash": "IP hash",
"User agent hash": "User agent hash",
"Channel": "Channel",
@@ -1299,45 +1285,5 @@
"Session expiring": "Session expiring",
"Your session will expire in {countdown}. Would you like to continue?": "Your session will expire in {countdown}. Would you like to continue?",
"Extend session": "Extend session",
"Log out": "Log out",
"Bookmarks": "Bookmarks",
"Bookmark this page": "Bookmark this page",
"Save bookmark": "Save bookmark",
"Bookmark name": "Bookmark name",
"Delete bookmark": "Delete bookmark",
"No bookmarks yet": "No bookmarks yet",
"New group": "New group",
"No group": "No group",
"Assign to group": "Assign to group",
"Group name": "Group name",
"Delete group": "Delete group",
"Group": "Group",
"Icon": "Icon",
"Maximum bookmarks reached": "Maximum bookmarks reached",
"Edit bookmark": "Edit bookmark",
"Save changes": "Save changes",
"Bookmark saved": "Bookmark saved",
"Bookmark updated": "Bookmark updated",
"Bookmark deleted": "Bookmark deleted",
"Group created": "Group created",
"Group deleted": "Group deleted",
"Group delete failed": "Group delete failed",
"Delete group and keep bookmarks?": "Delete group and keep bookmarks?",
"Bookmark action failed": "Bookmark action failed",
"Group action failed": "Group action failed",
"Reorder saved": "Reorder saved",
"Reorder failed": "Reorder failed",
"Move group up": "Move group up",
"Move group down": "Move group down",
"Move bookmark up": "Move bookmark up",
"Move bookmark down": "Move bookmark down",
"Group actions": "Group actions",
"Bookmark actions": "Bookmark actions",
"Bookmark name is required": "Bookmark name is required",
"Bookmark URL is invalid": "Bookmark URL is invalid",
"Group not found": "Group not found",
"Edit group": "Edit group",
"Group updated": "Group updated",
"Group name is required": "Group name is required",
"Maximum groups reached": "Maximum groups reached"
"Log out": "Log out"
}