diff --git a/services/notifications/pkg/email/composer.go b/services/notifications/pkg/email/composer.go index 1f81d87f0b..452219b2bf 100644 --- a/services/notifications/pkg/email/composer.go +++ b/services/notifications/pkg/email/composer.go @@ -6,6 +6,7 @@ import ( "strings" "text/template" + "github.com/leonelquinteros/gotext" "github.com/pkg/errors" "github.com/opencloud-eu/opencloud/pkg/l10n" @@ -21,19 +22,19 @@ var ( func NewTextTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) { var err error t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _translationFS, "l10n/locale").Locale(locale) - mt.Subject, err = composeMessage(t.Get(mt.Subject, []any{}...), vars) + mt.Subject, err = composeMessage(translate(t, mt.Subject), vars) if err != nil { return mt, err } - mt.Greeting, err = composeMessage(t.Get(mt.Greeting, []any{}...), vars) + mt.Greeting, err = composeMessage(translate(t, mt.Greeting), vars) if err != nil { return mt, err } - mt.MessageBody, err = composeMessage(t.Get(mt.MessageBody, []any{}...), vars) + mt.MessageBody, err = composeMessage(translate(t, mt.MessageBody), vars) if err != nil { return mt, err } - mt.CallToAction, err = composeMessage(t.Get(mt.CallToAction, []any{}...), vars) + mt.CallToAction, err = composeMessage(translate(t, mt.CallToAction), vars) if err != nil { return mt, err } @@ -44,19 +45,19 @@ func NewTextTemplate(mt MessageTemplate, locale, defaultLocale string, translati func NewHTMLTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) { var err error t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _translationFS, "l10n/locale").Locale(locale) - mt.Subject, err = composeMessage(t.Get(mt.Subject, []any{}...), vars) + mt.Subject, err = composeMessage(translate(t, mt.Subject), vars) if err != nil { return mt, err } - mt.Greeting, err = composeMessage(newlineToBr(t.Get(mt.Greeting, []any{}...)), vars) + mt.Greeting, err = composeMessage(newlineToBr(translate(t, mt.Greeting)), vars) if err != nil { return mt, err } - mt.MessageBody, err = composeMessage(newlineToBr(t.Get(mt.MessageBody, []any{}...)), vars) + mt.MessageBody, err = composeMessage(newlineToBr(translate(t, mt.MessageBody)), vars) if err != nil { return mt, err } - mt.CallToAction, err = composeMessage(callToActionToHTML(t.Get(mt.CallToAction, []any{}...)), vars) + mt.CallToAction, err = composeMessage(callToActionToHTML(translate(t, mt.CallToAction)), vars) if err != nil { return mt, err } @@ -71,18 +72,18 @@ func NewGroupedTextTemplate(gmt GroupedMessageTemplate, vars map[string]string, var err error t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _translationFS, "l10n/locale").Locale(locale) - gmt.Subject, err = composeMessage(t.Get(gmt.Subject, []any{}...), vars) + gmt.Subject, err = composeMessage(translate(t, gmt.Subject), vars) if err != nil { return gmt, err } - gmt.Greeting, err = composeMessage(t.Get(gmt.Greeting, []any{}...), vars) + gmt.Greeting, err = composeMessage(translate(t, gmt.Greeting), vars) if err != nil { return gmt, err } bodyParts := make([]string, 0, len(mtsVars)) for i, mt := range mts { - bodyPart, err := composeMessage(t.Get(mt.MessageBody, []any{}...), mtsVars[i]) + bodyPart, err := composeMessage(translate(t, mt.MessageBody), mtsVars[i]) if err != nil { return gmt, err } @@ -100,18 +101,18 @@ func NewGroupedHTMLTemplate(gmt GroupedMessageTemplate, vars map[string]string, var err error t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _translationFS, "l10n/locale").Locale(locale) - gmt.Subject, err = composeMessage(t.Get(gmt.Subject, []any{}...), vars) + gmt.Subject, err = composeMessage(translate(t, gmt.Subject), vars) if err != nil { return gmt, err } - gmt.Greeting, err = composeMessage(newlineToBr(t.Get(gmt.Greeting, []any{}...)), vars) + gmt.Greeting, err = composeMessage(newlineToBr(translate(t, gmt.Greeting)), vars) if err != nil { return gmt, err } bodyParts := make([]string, 0, len(mtsVars)) for i, mt := range mts { - bodyPart, err := composeMessage(t.Get(mt.MessageBody, []any{}...), mtsVars[i]) + bodyPart, err := composeMessage(translate(t, mt.MessageBody), mtsVars[i]) if err != nil { return gmt, err } @@ -122,6 +123,15 @@ func NewGroupedHTMLTemplate(gmt GroupedMessageTemplate, vars map[string]string, return gmt, nil } +// translate returns the translation of s. An empty string stays empty: gotext returns the +// header of the .po file for it. +func translate(t *gotext.Locale, s string) string { + if s == "" { + return "" + } + return t.Get(s) +} + // composeMessage renders the message based on template func composeMessage(tmpl string, vars map[string]string) (string, error) { tpl, err := template.New("").Parse(replacePlaceholders(tmpl)) diff --git a/services/notifications/pkg/email/email_test.go b/services/notifications/pkg/email/email_test.go index 01df4094f8..45580f7221 100644 --- a/services/notifications/pkg/email/email_test.go +++ b/services/notifications/pkg/email/email_test.go @@ -1,6 +1,9 @@ package email -import "testing" +import ( + "strings" + "testing" +) // Regression for #2804: escapeStringMap used to mutate its input map, and the // recipient render loop reuses that map across iterations. @@ -23,3 +26,18 @@ func TestEscapeStringMapDoesNotMutateInput(t *testing.T) { input["SpaceName"], raw) } } + +// Regression for #3830: translating an empty string returns the header of the .po file, so emails +// without a call to action got "Project-Id-Version: …" in every translated language. +func TestEmptyStringsAreNotTranslated(t *testing.T) { + vars := map[string]string{"ShareGrantee": "Ben", "ShareFolder": "Q3 Report", "ExpiredAt": "2026-10-27 10:00:00"} + msg, err := RenderEmailTemplate(ShareExpired, "de", "en", "", "", vars) + if err != nil { + t.Fatal(err) + } + for name, body := range map[string]string{"text": msg.TextBody, "html": msg.HTMLBody} { + if strings.Contains(body, "Project-Id-Version") { + t.Errorf("%s body contains the header of the translation file:\n%s", name, body) + } + } +}