From ad80447d4391d97ac5676d454938958d4dea4fcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:39:38 +0000 Subject: [PATCH 1/5] Initial plan From fb17388faa38f1811a0721b871d818c43657c2b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:48:45 +0000 Subject: [PATCH 2/5] Fix notification emails to use recipient's language preference Co-authored-by: Alanaktion <236490+Alanaktion@users.noreply.github.com> --- app/helper/notification.php | 116 +++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/app/helper/notification.php b/app/helper/notification.php index 33741ac7..2554ffa8 100644 --- a/app/helper/notification.php +++ b/app/helper/notification.php @@ -136,21 +136,23 @@ public function issue_comment(int $issue_id, int $comment_id): void // Get recipient list and remove current user $recipients = $this->_issue_watchers($issue_id); - $recipients = array_diff($recipients, [$comment->user_email]); + $recipients = array_filter($recipients, fn($r) => $r['email'] !== $comment->user_email); - // Render message body + // Set template variables $f3->set("issue", $issue); $f3->set("comment", $comment); $f3->set("previewText", $comment->text); - $text = $this->_render("notification/comment.txt"); - $body = $this->_render("notification/comment.html"); $subject = "[#{$issue->id}] - New comment on {$issue->name}"; - // Send to recipients + // Send to recipients in their preferred language foreach ($recipients as $recipient) { - $this->utf8mail($recipient, $subject, $body, $text); - $log->write("Sent comment notification to: " . $recipient); + $lang = $this->_set_language($recipient['language']); + $text = $this->_render("notification/comment.txt"); + $body = $this->_render("notification/comment.html"); + $f3->set("LANGUAGE", $lang); + $this->utf8mail($recipient['email'], $subject, $body, $text); + $log->write("Sent comment notification to: " . $recipient['email']); } } } @@ -188,13 +190,11 @@ public function issue_update(int $issue_id, int $update_id): ?bool // Get recipient list and remove update user $recipients = $this->_issue_watchers($issue_id); - $recipients = array_diff($recipients, [$update->user_email]); + $recipients = array_filter($recipients, fn($r) => $r['email'] !== $update->user_email); - // Render message body + // Set template variables $f3->set("issue", $issue); $f3->set("update", $update); - $text = $this->_render("notification/update.txt"); - $body = $this->_render("notification/update.html"); $changes->load(["issue_update_id = ? AND `field` = 'closed_date' AND old_value = '' and new_value != ''", $update->id]); if ($changes && $changes->id) { @@ -203,10 +203,14 @@ public function issue_update(int $issue_id, int $update_id): ?bool $subject = "[#{$issue->id}] - {$issue->name} updated"; } - // Send to recipients + // Send to recipients in their preferred language foreach ($recipients as $recipient) { - $this->utf8mail($recipient, $subject, $body, $text); - $log->write("Sent update notification to: " . $recipient); + $lang = $this->_set_language($recipient['language']); + $text = $this->_render("notification/update.txt"); + $body = $this->_render("notification/update.html"); + $f3->set("LANGUAGE", $lang); + $this->utf8mail($recipient['email'], $subject, $body, $text); + $log->write("Sent update notification to: " . $recipient['email']); } } @@ -240,21 +244,22 @@ public function issue_create(int $issue_id): void $user = new \Model\User(); $user->load($issue->author_id); if ($user->option('disable_self_notifications')) { - $recipients = array_diff($recipients, [$user->email]); + $recipients = array_filter($recipients, fn($r) => $r['email'] !== $user->email); } - // Render message body + // Set template variables $f3->set("issue", $issue); - $text = $this->_render("notification/new.txt"); - $body = $this->_render("notification/new.html"); - $subject = "[#{$issue->id}] - {$issue->name} created by {$issue->author_name}"; - // Send to recipients + // Send to recipients in their preferred language foreach ($recipients as $recipient) { - $this->utf8mail($recipient, $subject, $body, $text); - $log->write("Sent create notification to: " . $recipient); + $lang = $this->_set_language($recipient['language']); + $text = $this->_render("notification/new.txt"); + $body = $this->_render("notification/new.html"); + $f3->set("LANGUAGE", $lang); + $this->utf8mail($recipient['email'], $subject, $body, $text); + $log->write("Sent create notification to: " . $recipient['email']); } } } @@ -288,21 +293,23 @@ public function issue_file(int $issue_id, int $file_id): void // Get recipient list and remove current user $recipients = $this->_issue_watchers($issue_id); - $recipients = array_diff($recipients, [$file->user_email]); + $recipients = array_filter($recipients, fn($r) => $r['email'] !== $file->user_email); - // Render message body + // Set template variables $f3->set("issue", $issue); $f3->set("file", $file); $f3->set("previewText", $file->filename); - $text = $this->_render("notification/file.txt"); - $body = $this->_render("notification/file.html"); $subject = "[#{$issue->id}] - {$file->user_name} attached a file to {$issue->name}"; - // Send to recipients + // Send to recipients in their preferred language foreach ($recipients as $recipient) { - $this->utf8mail($recipient, $subject, $body, $text); - $log->write("Sent file notification to: " . $recipient); + $lang = $this->_set_language($recipient['language']); + $text = $this->_render("notification/file.txt"); + $body = $this->_render("notification/file.html"); + $f3->set("LANGUAGE", $lang); + $this->utf8mail($recipient['email'], $subject, $body, $text); + $log->write("Sent file notification to: " . $recipient['email']); } } } @@ -321,10 +328,12 @@ public function user_reset(int $user_id, string $token): void throw new \Exception("User does not exist."); } - // Render message body + // Render message body in the user's preferred language + $lang = $this->_set_language($user->language); $f3->set("token", $token); $text = $this->_render("notification/user_reset.txt"); $body = $this->_render("notification/user_reset.html"); + $f3->set("LANGUAGE", $lang); // Send email to user $subject = "Reset your password - " . $f3->get("site.name"); @@ -348,8 +357,10 @@ public function user_due_issues(\Model\User $user, array $due, array $overdue): $f3->set("previewText", $preview); $subject = "Due Today - " . $f3->get("site.name"); + $lang = $this->_set_language($user->language); $text = $this->_render("notification/user_due_issues.txt"); $body = $this->_render("notification/user_due_issues.html"); + $f3->set("LANGUAGE", $lang); return $this->utf8mail($user->email, $subject, $body, $text); } @@ -357,7 +368,7 @@ public function user_due_issues(\Model\User $user, array $due, array $overdue): } /** - * Get array of email addresses of all watchers on an issue + * Get array of watchers (email and language) for an issue */ protected function _issue_watchers(int $issue_id): array { @@ -365,36 +376,57 @@ protected function _issue_watchers(int $issue_id): array $recipients = []; // Add issue author and owner - $result = $db->exec("SELECT u.email FROM issue i INNER JOIN `user` u on i.author_id = u.id WHERE u.deleted_date IS NULL AND i.id = ?", $issue_id); + $result = $db->exec("SELECT u.email, u.language FROM issue i INNER JOIN `user` u on i.author_id = u.id WHERE u.deleted_date IS NULL AND i.id = ?", $issue_id); if (!empty($result[0]["email"])) { - $recipients[] = $result[0]["email"]; + $recipients[] = ['email' => $result[0]["email"], 'language' => $result[0]["language"]]; } - $result = $db->exec("SELECT u.email FROM issue i INNER JOIN `user` u on i.owner_id = u.id WHERE u.deleted_date IS NULL AND i.id = ?", $issue_id); + $result = $db->exec("SELECT u.email, u.language FROM issue i INNER JOIN `user` u on i.owner_id = u.id WHERE u.deleted_date IS NULL AND i.id = ?", $issue_id); if (!empty($result[0]["email"])) { - $recipients[] = $result[0]["email"]; + $recipients[] = ['email' => $result[0]["email"], 'language' => $result[0]["language"]]; } // Add whole group $result = $db->exec("SELECT u.role, u.id FROM issue i INNER JOIN `user` u on i.owner_id = u.id WHERE u.deleted_date IS NULL AND i.id = ?", $issue_id); if ($result && $result[0]["role"] == 'group') { - $group_users = $db->exec("SELECT g.user_email FROM user_group_user g WHERE g.deleted_date IS NULL AND g.group_id = ?", $result[0]["id"]); + $group_users = $db->exec("SELECT u.email, u.language FROM user_group g INNER JOIN `user` u ON g.user_id = u.id WHERE u.deleted_date IS NULL AND g.group_id = ?", $result[0]["id"]); foreach ($group_users as $group_user) { - if (!empty($group_user["user_email"])) { - $recipients[] = $group_user["user_email"]; + if (!empty($group_user["email"])) { + $recipients[] = ['email' => $group_user["email"], 'language' => $group_user["language"]]; } } } // Add watchers - $watchers = $db->exec("SELECT u.email FROM issue_watcher w INNER JOIN `user` u ON w.user_id = u.id WHERE u.deleted_date IS NULL AND issue_id = ?", $issue_id); + $watchers = $db->exec("SELECT u.email, u.language FROM issue_watcher w INNER JOIN `user` u ON w.user_id = u.id WHERE u.deleted_date IS NULL AND issue_id = ?", $issue_id); foreach ($watchers as $watcher) { - $recipients[] = $watcher["email"]; + $recipients[] = ['email' => $watcher["email"], 'language' => $watcher["language"]]; } - // Remove duplicate users - return array_unique($recipients); + // Remove duplicate users, keeping first occurrence + $seen = []; + $unique = []; + foreach ($recipients as $recipient) { + if (!in_array($recipient['email'], $seen)) { + $seen[] = $recipient['email']; + $unique[] = $recipient; + } + } + return $unique; + } + + /** + * Temporarily set the app language, returning the previous value for restoration + */ + protected function _set_language(?string $language): string + { + $f3 = \Base::instance(); + $original = (string) $f3->get("LANGUAGE"); + if ($language) { + $f3->set("LANGUAGE", $language); + } + return $original; } /** From 571f4e9ef430b82bc9309c2cbf9d898282689e87 Mon Sep 17 00:00:00 2001 From: Alan Date: Wed, 4 Mar 2026 09:24:20 -0700 Subject: [PATCH 3/5] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/helper/notification.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/helper/notification.php b/app/helper/notification.php index 2554ffa8..a2dd2197 100644 --- a/app/helper/notification.php +++ b/app/helper/notification.php @@ -408,8 +408,9 @@ protected function _issue_watchers(int $issue_id): array $seen = []; $unique = []; foreach ($recipients as $recipient) { - if (!in_array($recipient['email'], $seen)) { - $seen[] = $recipient['email']; + $email = $recipient['email']; + if (!isset($seen[$email])) { + $seen[$email] = true; $unique[] = $recipient; } } From e4065dd9c5736458f9182319229db4efd4abbd14 Mon Sep 17 00:00:00 2001 From: Alan Date: Wed, 4 Mar 2026 09:25:11 -0700 Subject: [PATCH 4/5] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/helper/notification.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/helper/notification.php b/app/helper/notification.php index a2dd2197..e3230e47 100644 --- a/app/helper/notification.php +++ b/app/helper/notification.php @@ -424,6 +424,16 @@ protected function _set_language(?string $language): string { $f3 = \Base::instance(); $original = (string) $f3->get("LANGUAGE"); + + // If no explicit language is provided, fall back to a configured site default. + if ($language === null || $language === '') { + $defaultLanguage = (string) $f3->get("site.default_language"); + if ($defaultLanguage === '') { + $defaultLanguage = (string) $f3->get("site.language"); + } + $language = $defaultLanguage !== '' ? $defaultLanguage : $language; + } + if ($language) { $f3->set("LANGUAGE", $language); } From 348ff367c1bb2283ef0c9c78deea95242348dd0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:00:11 +0000 Subject: [PATCH 5/5] Fix _set_language() to preserve null LANGUAGE on restore Co-authored-by: Alanaktion <236490+Alanaktion@users.noreply.github.com> --- app/helper/notification.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helper/notification.php b/app/helper/notification.php index e3230e47..f141f99c 100644 --- a/app/helper/notification.php +++ b/app/helper/notification.php @@ -420,10 +420,10 @@ protected function _issue_watchers(int $issue_id): array /** * Temporarily set the app language, returning the previous value for restoration */ - protected function _set_language(?string $language): string + protected function _set_language(?string $language): ?string { $f3 = \Base::instance(); - $original = (string) $f3->get("LANGUAGE"); + $original = $f3->get("LANGUAGE"); // If no explicit language is provided, fall back to a configured site default. if ($language === null || $language === '') {