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

@@ -0,0 +1,12 @@
{
"About": "Über",
"Address book": "Adressbuch",
"Call mobile": "Mobil anrufen",
"Call phone": "Telefon anrufen",
"From": "Von",
"No departments": "Keine Abteilungen",
"Quick actions": "Schnellaktionen",
"Send email": "E-Mail senden",
"To": "Bis",
"View in address book": "Im Adressbuch ansehen"
}

View File

@@ -0,0 +1,12 @@
{
"About": "About",
"Address book": "Address book",
"Call mobile": "Call mobile",
"Call phone": "Call phone",
"From": "From",
"No departments": "No departments",
"Quick actions": "Quick actions",
"Send email": "Send email",
"To": "To",
"View in address book": "View in address book"
}

View File

@@ -94,4 +94,5 @@ return [
],
'migrations_path' => 'db/updates',
'i18n_path' => 'i18n',
];

View File

@@ -0,0 +1,43 @@
{
"Bookmarks": "Lesezeichen",
"Bookmark this page": "Seite als Lesezeichen speichern",
"Save bookmark": "Lesezeichen speichern",
"Bookmark name": "Lesezeichen-Name",
"Delete bookmark": "Lesezeichen löschen",
"Delete this bookmark?": "Dieses Lesezeichen wirklich 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"
}

View File

@@ -0,0 +1,43 @@
{
"Bookmarks": "Bookmarks",
"Bookmark this page": "Bookmark this page",
"Save bookmark": "Save bookmark",
"Bookmark name": "Bookmark name",
"Delete bookmark": "Delete bookmark",
"Delete this bookmark?": "Delete this 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"
}

View File

@@ -130,4 +130,5 @@ return [
'permissions' => [],
'migrations_path' => null,
'i18n_path' => 'i18n',
];

View File

@@ -0,0 +1,10 @@
{
"Notifications": "Benachrichtigungen",
"No notifications": "Keine Benachrichtigungen",
"Mark all as read": "Alle als gelesen markieren",
"Mark as read": "Als gelesen markieren",
"Dismiss": "Verwerfen",
"Action failed": "Aktion fehlgeschlagen",
"New user: %s": "Neuer Benutzer: %s",
"User deleted: %s": "Benutzer gelöscht: %s"
}

View File

@@ -0,0 +1,10 @@
{
"Notifications": "Notifications",
"No notifications": "No notifications",
"Mark all as read": "Mark all as read",
"Mark as read": "Mark as read",
"Dismiss": "Dismiss",
"Action failed": "Action failed",
"New user: %s": "New user: %s",
"User deleted: %s": "User deleted: %s"
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* Notifications module manifest.
*
* In-app notifications with bell icon, dropdown panel,
* event-driven creation, and scheduled cleanup.
*/
return [
'id' => 'notifications',
'version' => '1.0.0',
'enabled_by_default' => true,
'load_order' => 15,
'requires' => [],
'routes' => [
['path' => 'notifications/list-data', 'target' => 'notifications/list-data'],
['path' => 'notifications/mark-read-data', 'target' => 'notifications/mark-read-data'],
['path' => 'notifications/delete-data', 'target' => 'notifications/delete-data'],
['path' => 'notifications/unread-count-data', 'target' => 'notifications/unread-count-data'],
],
'public_paths' => [],
'container_registrars' => [
\MintyPHP\Module\Notifications\NotificationsContainerRegistrar::class,
],
'ui_slots' => [
'topbar.right_item' => [
[
'key' => 'notifications-bell',
'template' => 'templates/topbar-notification-bell.phtml',
'permission' => 'notifications.view',
'order' => 100,
],
],
'layout.head_style' => [
[
'key' => 'notifications-topbar-style',
'path' => 'modules/notifications/css/layout/app-topbar-notifications.css',
'permission' => 'notifications.view',
'order' => 100,
],
[
'key' => 'notifications-dropdown-style',
'path' => 'modules/notifications/css/components/app-notification-dropdown.css',
'permission' => 'notifications.view',
'order' => 101,
],
],
'runtime.component' => [
[
'key' => 'notification-bell',
'script' => 'modules/notifications/js/components/app-notification-bell.js',
'export' => 'initNotificationBell',
'scope' => 'global',
'config_path' => 'components.notificationBell',
'default_config' => [],
'phase' => 'late',
'permission' => 'notifications.view',
'order' => 100,
],
],
],
'authorization_policies' => [
\MintyPHP\Module\Notifications\NotificationsAuthorizationPolicy::class,
],
'permissions' => [
[
'key' => 'notifications.view',
'label' => 'Notifications: View',
'description' => 'View and manage own notifications',
],
],
'event_listeners' => [
'user.created' => [
['class' => \MintyPHP\Module\Notifications\Listeners\UserCreatedNotificationListener::class, 'method' => 'handle'],
],
'user.deleted' => [
['class' => \MintyPHP\Module\Notifications\Listeners\UserDeletedNotificationListener::class, 'method' => 'handle'],
],
],
'scheduler_jobs' => [
[
'job_key' => 'notifications_cleanup',
'handler' => \MintyPHP\Module\Notifications\Handler\NotificationCleanupJobHandler::class,
'label' => 'Notification cleanup',
'description' => 'Purges read notifications older than 90 days',
'default_enabled' => 1,
'default_timezone' => 'UTC',
'default_schedule_type' => 'daily',
'default_schedule_interval' => 1,
'default_schedule_time' => '04:00',
'default_schedule_weekdays_csv' => null,
'default_catchup_once' => 1,
'allowed_schedule_types' => ['daily', 'weekly'],
],
],
'layout_context_providers' => [
\MintyPHP\Module\Notifications\Providers\NotificationsLayoutProvider::class,
],
'session_providers' => [
\MintyPHP\Module\Notifications\Providers\NotificationsSessionProvider::class,
],
'search_resources' => [],
'asset_groups' => [],
'migrations_path' => 'db/updates',
'i18n_path' => 'i18n',
];