Files
vikunja/pkg/models/notifications.go
kolaente 138a545523 fix(notifications): pass lang to overdue reminder translation
The call to i18n.T for notifications.task.overdue.overdue was missing
its first positional argument, so the translation key was being passed
as the language code. This surfaced as a "dead key" once the
translation check learned to look for unused entries. Fix the call so
the reminder line is properly localised.
2026-04-23 13:30:51 +02:00

471 lines
18 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"fmt"
"sort"
"strconv"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/i18n"
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/modules/avatar"
"code.vikunja.io/api/pkg/notifications"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
)
// getDoerAvatarDataURI returns the avatar data URI for a user, for use in email headers.
func getDoerAvatarDataURI(doer *user.User) string {
provider := avatar.GetProvider(doer)
dataURI, err := provider.AsDataURI(doer, 20)
if err != nil {
return ""
}
return dataURI
}
// getThreadID generates a Message-ID format thread ID for a task
func getThreadID(taskID int64) string {
return fmt.Sprintf("<task-%d@%s>", taskID, mail.GetMailDomain())
}
// ReminderDueNotification represents a ReminderDueNotification notification
type ReminderDueNotification struct {
User *user.User `json:"user,omitempty"`
Task *Task `json:"task"`
Project *Project `json:"project"`
TaskReminder *TaskReminder `json:"reminder"`
}
// ToMail returns the mail notification for ReminderDueNotification
func (n *ReminderDueNotification) ToMail(lang string) *notifications.Mail {
return notifications.NewMail().
IncludeLinkToSettings(lang).
To(n.User.Email).
Subject(i18n.T(lang, "notifications.task.reminder.subject", n.Task.Title, n.Project.Title)).
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
Line(i18n.T(lang, "notifications.task.reminder.message", notifications.EscapeMarkdown(n.Task.Title), notifications.EscapeMarkdown(n.Project.Title))).
Action(i18n.T(lang, "notifications.common.actions.open_task"), config.ServicePublicURL.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)).
Line(i18n.T(lang, "notifications.common.have_nice_day"))
}
// ToDB returns the ReminderDueNotification notification in a format which can be saved in the db
func (n *ReminderDueNotification) ToDB() interface{} {
return &ReminderDueNotification{
Task: n.Task,
Project: n.Project,
}
}
// Name returns the name of the notification
func (n *ReminderDueNotification) Name() string {
return "task.reminder"
}
// ThreadID returns the thread ID for email threading
func (n *ReminderDueNotification) ThreadID() string {
return getThreadID(n.Task.ID)
}
// TaskCommentNotification represents a TaskCommentNotification notification
type TaskCommentNotification struct {
Doer *user.User `json:"doer"`
Task *Task `json:"task"`
Comment *TaskComment `json:"comment"`
Mentioned bool `json:"mentioned"`
Project *Project `json:"project"`
}
func (n *TaskCommentNotification) SubjectID() int64 {
return n.Comment.ID
}
// ToMail returns the mail notification for TaskCommentNotification
func (n *TaskCommentNotification) ToMail(lang string) *notifications.Mail {
s := db.NewSession()
defer s.Close()
formattedComment := formatMentionsForEmail(s, n.Comment.Comment)
mail := notifications.NewMail().
Conversational().
From(n.Doer.GetNameAndFromEmail()).
Subject(i18n.T(lang, "notifications.task.comment.subject", n.Task.Title, n.Task.GetFullIdentifier()))
// Add header line
action := i18n.T(lang, "notifications.common.actions.left_comment", n.Doer.GetName())
if n.Mentioned {
action = i18n.T(lang, "notifications.common.actions.mentioned_you_comment", n.Doer.GetName())
mail.Subject(i18n.T(lang, "notifications.task.comment.mentioned_subject", n.Doer.GetName(), n.Task.Title, n.Task.GetFullIdentifier()))
}
headerLine := notifications.CreateConversationalHeader(
getDoerAvatarDataURI(n.Doer),
action,
n.Task.GetFrontendURL(),
n.Project.Title,
n.Task.GetFullIdentifier(),
n.Task.Title,
)
mail.HeaderLine(headerLine)
// Add the actual comment content wrapped in a div for consistent spacing
mail.HTML(formattedComment)
return mail.
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
IncludeLinkToSettings(lang)
}
// ToDB returns the TaskCommentNotification notification in a format which can be saved in the db
func (n *TaskCommentNotification) ToDB() interface{} {
return n
}
// Name returns the name of the notification
func (n *TaskCommentNotification) Name() string {
return "task.comment"
}
// ThreadID returns the thread ID for email threading
func (n *TaskCommentNotification) ThreadID() string {
return getThreadID(n.Task.ID)
}
// TaskAssignedNotification represents a TaskAssignedNotification notification
type TaskAssignedNotification struct {
Doer *user.User `json:"doer"`
Task *Task `json:"task"`
Assignee *user.User `json:"assignee"`
Target *user.User `json:"-"`
Project *Project `json:"project"`
}
// ToMail returns the mail notification for TaskAssignedNotification
func (n *TaskAssignedNotification) ToMail(lang string) *notifications.Mail {
if n.Target.ID == n.Assignee.ID {
// Notification to the assignee
return notifications.NewMail().
From(n.Doer.GetNameAndFromEmail()).
Subject(i18n.T(lang, "notifications.task.assigned.subject_to_assignee", n.Task.Title, n.Task.GetFullIdentifier())).
Greeting(i18n.T(lang, "notifications.greeting", n.Target.GetName())).
Line(i18n.T(lang, "notifications.task.assigned.message_to_assignee", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Task.Title))).
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
IncludeLinkToSettings(lang)
}
// Check if the doer assigned the task to themselves
if n.Doer.ID == n.Assignee.ID {
return notifications.NewMail().
From(n.Doer.GetNameAndFromEmail()).
Subject(i18n.T(lang, "notifications.task.assigned.subject_to_others_self", n.Task.Title, n.Task.GetFullIdentifier(), n.Doer.GetName())).
Greeting(i18n.T(lang, "notifications.greeting", n.Target.GetName())).
Line(i18n.T(lang, "notifications.task.assigned.message_to_others_self", notifications.EscapeMarkdown(n.Doer.GetName()))).
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
IncludeLinkToSettings(lang)
}
// Notification to others about assignment
return notifications.NewMail().
From(n.Doer.GetNameAndFromEmail()).
Subject(i18n.T(lang, "notifications.task.assigned.subject_to_others", n.Task.Title, n.Task.GetFullIdentifier(), n.Assignee.GetName())).
Greeting(i18n.T(lang, "notifications.greeting", n.Target.GetName())).
Line(i18n.T(lang, "notifications.task.assigned.message_to_others", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Assignee.GetName()))).
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
IncludeLinkToSettings(lang)
}
// ToDB returns the TaskAssignedNotification notification in a format which can be saved in the db
func (n *TaskAssignedNotification) ToDB() interface{} {
return n
}
// Name returns the name of the notification
func (n *TaskAssignedNotification) Name() string {
return "task.assigned"
}
// ThreadID returns the thread ID for email threading
func (n *TaskAssignedNotification) ThreadID() string {
return getThreadID(n.Task.ID)
}
// TaskDeletedNotification represents a TaskDeletedNotification notification
type TaskDeletedNotification struct {
Doer *user.User `json:"doer"`
Task *Task `json:"task"`
}
// ToMail returns the mail notification for TaskDeletedNotification
func (n *TaskDeletedNotification) ToMail(lang string) *notifications.Mail {
return notifications.NewMail().
Subject(i18n.T(lang, "notifications.task.deleted.subject", n.Task.Title, n.Task.GetFullIdentifier())).
Line(i18n.T(lang, "notifications.task.deleted.message", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Task.Title), notifications.EscapeMarkdown(n.Task.GetFullIdentifier())))
}
// ToDB returns the TaskDeletedNotification notification in a format which can be saved in the db
func (n *TaskDeletedNotification) ToDB() interface{} {
return n
}
// Name returns the name of the notification
func (n *TaskDeletedNotification) Name() string {
return "task.deleted"
}
// ThreadID returns the thread ID for email threading
func (n *TaskDeletedNotification) ThreadID() string {
return getThreadID(n.Task.ID)
}
// ProjectCreatedNotification represents a ProjectCreatedNotification notification
type ProjectCreatedNotification struct {
Doer *user.User `json:"doer"`
Project *Project `json:"project"`
}
// ToMail returns the mail notification for ProjectCreatedNotification
func (n *ProjectCreatedNotification) ToMail(lang string) *notifications.Mail {
return notifications.NewMail().
Subject(i18n.T(lang, "notifications.project.created", n.Doer.GetName(), n.Project.Title)).
Line(i18n.T(lang, "notifications.project.created", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Project.Title))).
Action(i18n.T(lang, "notifications.common.actions.open_project"), config.ServicePublicURL.GetString()+"projects/")
}
// ToDB returns the ProjectCreatedNotification notification in a format which can be saved in the db
func (n *ProjectCreatedNotification) ToDB() interface{} {
return n
}
// Name returns the name of the notification
func (n *ProjectCreatedNotification) Name() string {
return "project.created"
}
// TeamMemberAddedNotification represents a TeamMemberAddedNotification notification
type TeamMemberAddedNotification struct {
Member *user.User `json:"member"`
Doer *user.User `json:"doer"`
Team *Team `json:"team"`
}
// ToMail returns the mail notification for TeamMemberAddedNotification
func (n *TeamMemberAddedNotification) ToMail(lang string) *notifications.Mail {
return notifications.NewMail().
Subject(i18n.T(lang, "notifications.team.member_added.subject", n.Doer.GetName(), n.Team.Name)).
From(n.Doer.GetNameAndFromEmail()).
Greeting(i18n.T(lang, "notifications.greeting", n.Member.GetName())).
Line(i18n.T(lang, "notifications.team.member_added.message", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Team.Name))).
Action(i18n.T(lang, "notifications.common.actions.open_team"), config.ServicePublicURL.GetString()+"teams/"+strconv.FormatInt(n.Team.ID, 10)+"/edit")
}
// ToDB returns the TeamMemberAddedNotification notification in a format which can be saved in the db
func (n *TeamMemberAddedNotification) ToDB() interface{} {
return n
}
// Name returns the name of the notification
func (n *TeamMemberAddedNotification) Name() string {
return "team.member.added"
}
func getOverdueSinceString(until time.Duration, language string) (overdueSince string) {
if until == 0 {
return i18n.T(language, "notifications.task.overdue.overdue_now")
}
return i18n.T(language, "notifications.task.overdue.overdue_since", utils.HumanizeDuration(until, language))
}
// UndoneTaskOverdueNotification represents a UndoneTaskOverdueNotification notification
type UndoneTaskOverdueNotification struct {
User *user.User
Task *Task
Project *Project
}
// ToMail returns the mail notification for UndoneTaskOverdueNotification
func (n *UndoneTaskOverdueNotification) ToMail(lang string) *notifications.Mail {
until := time.Until(n.Task.DueDate).Round(1*time.Hour) * -1
return notifications.NewMail().
IncludeLinkToSettings(lang).
Subject(i18n.T(lang, "notifications.task.overdue.subject", n.Task.Title, n.Project.Title)).
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
Line(i18n.T(lang, "notifications.task.overdue.message", notifications.EscapeMarkdown(n.Task.Title), notifications.EscapeMarkdown(n.Project.Title), getOverdueSinceString(until, n.User.Language))).
Action(i18n.T(lang, "notifications.common.actions.open_task"), config.ServicePublicURL.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)).
Line(i18n.T(lang, "notifications.common.have_nice_day"))
}
// ToDB returns the UndoneTaskOverdueNotification notification in a format which can be saved in the db
func (n *UndoneTaskOverdueNotification) ToDB() interface{} {
return nil
}
// Name returns the name of the notification
func (n *UndoneTaskOverdueNotification) Name() string {
return "task.undone.overdue"
}
// ThreadID returns the thread ID for email threading
func (n *UndoneTaskOverdueNotification) ThreadID() string {
return getThreadID(n.Task.ID)
}
// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification
type UndoneTasksOverdueNotification struct {
User *user.User
Tasks map[int64]*Task
Projects map[int64]*Project
}
// ToMail returns the mail notification for UndoneTasksOverdueNotification
func (n *UndoneTasksOverdueNotification) ToMail(lang string) *notifications.Mail {
sortedTasks := make([]*Task, 0, len(n.Tasks))
for _, task := range n.Tasks {
sortedTasks = append(sortedTasks, task)
}
sort.Slice(sortedTasks, func(i, j int) bool {
return sortedTasks[i].DueDate.Before(sortedTasks[j].DueDate)
})
overdueLine := ""
for _, task := range sortedTasks {
until := time.Until(task.DueDate).Round(1*time.Hour) * -1
overdueLine += `* [` + notifications.EscapeMarkdown(task.Title) + `](` + config.ServicePublicURL.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `) (` + notifications.EscapeMarkdown(n.Projects[task.ProjectID].Title) + `), ` + i18n.T(lang, "notifications.task.overdue.overdue", getOverdueSinceString(until, n.User.Language)) + "\n"
}
return notifications.NewMail().
IncludeLinkToSettings(lang).
Subject(i18n.T(lang, "notifications.task.overdue.multiple_subject")).
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
Line(i18n.T(lang, "notifications.task.overdue.multiple_message")).
Line(overdueLine).
Action(i18n.T(lang, "notifications.common.actions.open_vikunja"), config.ServicePublicURL.GetString()).
Line(i18n.T(lang, "notifications.common.have_nice_day"))
}
// ToDB returns the UndoneTasksOverdueNotification notification in a format which can be saved in the db
func (n *UndoneTasksOverdueNotification) ToDB() interface{} {
return nil
}
// Name returns the name of the notification
func (n *UndoneTasksOverdueNotification) Name() string {
return "task.undone.overdue"
}
// UserMentionedInTaskNotification represents a UserMentionedInTaskNotification notification
type UserMentionedInTaskNotification struct {
Doer *user.User `json:"doer"`
Task *Task `json:"task"`
IsNew bool `json:"is_new"`
Project *Project `json:"project"`
}
func (n *UserMentionedInTaskNotification) SubjectID() int64 {
return n.Task.ID
}
// ToMail returns the mail notification for UserMentionedInTaskNotification
func (n *UserMentionedInTaskNotification) ToMail(lang string) *notifications.Mail {
s := db.NewSession()
defer s.Close()
formattedDescription := formatMentionsForEmail(s, n.Task.Description)
var subject string
if n.IsNew {
subject = i18n.T(lang, "notifications.task.mentioned.subject_new", n.Doer.GetName(), n.Task.Title, n.Task.GetFullIdentifier())
} else {
subject = i18n.T(lang, "notifications.task.mentioned.subject", n.Doer.GetName(), n.Task.Title, n.Task.GetFullIdentifier())
}
mail := notifications.NewMail().
Conversational().
From(n.Doer.GetNameAndFromEmail()).
Subject(subject)
// Add header line
action := i18n.T(lang, "notifications.common.actions.mentioned_you", n.Doer.GetName())
if n.IsNew {
action = i18n.T(lang, "notifications.common.actions.mentioned_you_new_task", n.Doer.GetName())
}
headerLine := notifications.CreateConversationalHeader(
getDoerAvatarDataURI(n.Doer),
action,
n.Task.GetFrontendURL(),
n.Project.Title,
n.Task.GetFullIdentifier(),
n.Task.Title,
)
mail.HeaderLine(headerLine)
if formattedDescription != "" {
mail.HTML(formattedDescription)
}
return mail.
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
IncludeLinkToSettings(lang)
}
// ToDB returns the UserMentionedInTaskNotification notification in a format which can be saved in the db
func (n *UserMentionedInTaskNotification) ToDB() interface{} {
return n
}
// Name returns the name of the notification
func (n *UserMentionedInTaskNotification) Name() string {
return "task.mentioned"
}
// ThreadID returns the thread ID for email threading
func (n *UserMentionedInTaskNotification) ThreadID() string {
return getThreadID(n.Task.ID)
}
// DataExportReadyNotification represents a DataExportReadyNotification notification
type DataExportReadyNotification struct {
User *user.User `json:"user"`
}
// ToMail returns the mail notification for DataExportReadyNotification
func (n *DataExportReadyNotification) ToMail(lang string) *notifications.Mail {
return notifications.NewMail().
Subject(i18n.T(lang, "notifications.data_export.ready.subject")).
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
Line(i18n.T(lang, "notifications.data_export.ready.message")).
Action(i18n.T(lang, "notifications.common.actions.download"), config.ServicePublicURL.GetString()+"user/export/download").
Line(i18n.T(lang, "notifications.data_export.ready.availability")).
Line(i18n.T(lang, "notifications.common.have_nice_day"))
}
// ToDB returns the DataExportReadyNotification notification in a format which can be saved in the db
func (n *DataExportReadyNotification) ToDB() interface{} {
return nil
}
// Name returns the name of the notification
func (n *DataExportReadyNotification) Name() string {
return "data.export.ready"
}