Repository: owncloud/ocis
Pull request #6038 (2023-04-12 12:34:42)

Make Emails translatable via transifex #6025
<!--
Thanks for submitting a change to ownCloud!

This is the bug tracker for the oCIS component. Find other components at https://github.com/owncloud/core/blob/master/.github/CONTRIBUTING.md#guidelines

For fixing potential security issues please see https://owncloud.org/security/

To make it possible for us to get your change reviewed and merged please carefully fill out the requested information below.

Please note that any kind of change needs first be submitted to the master branch which holds the next version of oCIS.

Please set the following labels:

- Set label "Status:Needs-Review" for review or "Status:In-Progress" if the PR still has open tasks.
- Assignment: assign to self
- Reviewers: pick at least one
-->

## Description
Make Emails translatable via transifex

## Related Issue
<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->
- Fixes [https://github.com/owncloud/ocis/issues/6025](https://github.com/owncloud/ocis/issues/6025)


## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
- test environment: local
- test case 1: tested by existing test suite 

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [x] Code changes
- [ ] Unit tests added
- [ ] Acceptance tests added
- [ ] Documentation ticket raised: <link> 


mmattel: * I am curious why we have in `templates.go` source strings in german language.
The default language for TX is english. From there stuff is translated.
* The other thing is, that I cant identify the "string" to be translated. It looks to me like a big junk of text not separated into blocks. If we stay at this, this is gonny be very hard for translators and error prone.
dragonchaser: > I am curious why we have in templates.go source strings in german language.
The default language for TX is english. From there stuff is translated.

This is ported from the old approach where we did not have i18n (yet), the templates then contained both strings, and were sent out this way, this is supposed to change in the future.
mmattel: Just for the record, here is the current output of `make l10n-read`:
`cat ./pkg/service/l10n/notifications.pot`
```
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid   ""
msgstr  "Project-Id-Version: \n"
        "Report-Msgid-Bugs-To: EMAIL\n"
        "POT-Creation-Date: 2023-04-13 09:31+0200\n"
        "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
        "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
        "Language-Team: LANGUAGE <LL@li.org>\n"
        "Language: \n"
        "MIME-Version: 1.0\n"
        "Content-Type: text/plain; charset=CHARSET\n"
        "Content-Transfer-Encoding: 8bit\n"

#: pkg/service/l10n/templates.go:32
msgid   "Hello {ShareGrantee},\n"
        "\n"
        "Your share to {ShareFolder} has expired at {ExpiredAt}\n"
        "\n"
        "Even though this share has been revoked you still might have access through other shares and/or space memberships\n"
        "\n"
        "----------------------------------------------------------\n"
        "\n"
        "Hallo {ShareGrantee},\n"
        "\n"
        "Deine Freigabe zu {ShareFolder} ist am {ExpiredAt} abgelaufen.\n"
        "\n"
        "Obwohl diese Freigabe nicht mehr zur Verfügung steht, könntest du immer noch Zugriff über andere Freigaben und/oder Space Mitgliedschaften haben.\n"
        "\n"
        "\n"
        "---\n"
        "ownCloud - Store. Share. Work.\n"
        "https://owncloud.com\n"
msgstr  ""

#: pkg/service/l10n/templates.go:10
msgid   "Hello {ShareGrantee},\n"
        "\n"
        "{ShareSharer} has shared \"{ShareFolder}\" with you.\n"
        "\n"
        "Click here to view it: {ShareLink}\n"
        "\n"
        "----------------------------------------------------------\n"
        "\n"
        "Hallo {ShareGrantee},\n"
        "\n"
        "{ShareSharer} hat dich zu \"{ShareFolder}\" eingeladen.\n"
        "\n"
        "Klicke hier zum Anzeigen: {ShareLink}\n"
        "\n"
        "\n"
        "---\n"
        "ownCloud - Store. Share. Work.\n"
        "https://owncloud.com"
msgstr  ""

#: pkg/service/l10n/templates.go:99
msgid   "Hello {SpaceGrantee},\n"
        "\n"
        "Your membership of space {SpaceName} has expired at {ExpiredAt}\n"
        "\n"
        "Even though this membership has expired you still might have access through other shares and/or space memberships\n"
        "\n"
        "----------------------------------------------------------\n"
        "\n"
        "Hallo {SpaceGrantee},\n"
        "\n"
        "Deine Mitgliedschaft zu dem Space {SpaceName} ist am {ExpiredAt} abgelaufen.\n"
        "\n"
        "Obwohl diese Mitgliedschaft nicht mehr zur Verfügung steht, könntest du immer noch Zugriff über andere Freigaben und/oder Space Mitgliedschaften haben.\n"
        "\n"
        "\n"
        "---\n"
        "ownCloud - Store. Share. Work.\n"
        "https://owncloud.com"
msgstr  ""

#: pkg/service/l10n/templates.go:55
msgid   "Hello {SpaceGrantee},\n"
        "\n"
        "{SpaceSharer} has invited you to join \"{SpaceName}\".\n"
        "\n"
        "Click here to view it: {ShareLink}\n"
        "\n"
        "----------------------------------------------------------\n"
        "\n"
        "Hallo {SpaceGrantee},\n"
        "\n"
        "{SpaceSharer} hat dich in den Space \"{SpaceName}\" eingeladen.\n"
        "\n"
        "Klicke hier zum Anzeigen: {ShareLink}\n"
        "\n"
        "\n"
        "---\n"
        "ownCloud - Store. Share. Work.\n"
        "https://owncloud.com"
msgstr  ""

#: pkg/service/l10n/templates.go:77
msgid   "Hello {SpaceGrantee},\n"
        "\n"
        "{SpaceSharer} has removed you from \"{SpaceName}\".\n"
        "\n"
        "You might still have access through your other groups or direct membership. Click here to check it: {ShareLink}\n"
        "\n"
        "----------------------------------------------------------\n"
        "\n"
        "Hallo {SpaceGrantee},\n"
        "\n"
        "{SpaceSharer} hat dich aus dem Space \"{SpaceName}\" entfernt.\n"
        "\n"
        "Du könntest über deine anderen Gruppen oder deiner direkten Mitgliedschaft noch Zugriff haben. Klicke hier zum Überprüfen: {ShareLink}\n"
        "\n"
        "\n"
        "---\n"
        "ownCloud - Store. Share. Work.\n"
        "https://owncloud.com"
msgstr  ""

#: pkg/service/l10n/templates.go:98
msgid   "Membership of '{SpaceName}' expired at {ExpiredAt}"
msgstr  ""

#: pkg/service/l10n/templates.go:31
msgid   "Share to '{ShareFolder}' expired at {ExpiredAt}"
msgstr  ""

#: pkg/service/l10n/templates.go:9
msgid   "{ShareSharer} shared '{ShareFolder}' with you"
msgstr  ""

#: pkg/service/l10n/templates.go:54
msgid   "{SpaceSharer} invited you to join {SpaceName}"
msgstr  ""

#: pkg/service/l10n/templates.go:76
msgid   "{SpaceSharer} removed you from {SpaceName}"
msgstr  ""
```
mmattel: Note that `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` (see referenced issue: #6025) is still present and I doubt that it is needed (but I can be wrong). @dragonchaser

Imho we should [deprecate](https://owncloud.dev/ocis/development/deprecating-variables/) it with no replacement.
micbar: Hmm, i am late to the party, but i am not ok by removing the template files. IMO this was a miscommunication.
micbar: @mmattel So no deprecation is planned, we still need the email templates. But they should be translatable.
2403905: > 


2403905: > Hmm, i am late to the party, but i am not ok by removing the template files. IMO this was a miscommunication.

Do you mean, I have to keep using the templates? https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates
micbar: > > Hmm, i am late to the party, but i am not ok by removing the template files. IMO this was a miscommunication.
> 
> Do you mean, I have to keep using the templates? https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates

yes.
mmattel: We must add some documentation in the readme.md of the notification service.
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6038)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=BUG) [1 Bug](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6038&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6038&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6038&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=CODE_SMELL)

[![16.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '16.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6038&metric=new_coverage&view=list) [16.3% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6038&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6038&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6038&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6038)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=BUG) [1 Bug](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6038&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6038&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6038&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6038&resolved=false&types=CODE_SMELL)

[![16.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '16.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6038&metric=new_coverage&view=list) [16.3% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6038&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6038&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6038&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75c4294c597..b816e9ccdea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,7 +48,7 @@ The following sections list the changes for unreleased.
 * Enhancement - Web config additions: [#6032](https://github.com/owncloud/ocis/pull/6032)
 * Enhancement - Eventhistory service: [#5600](https://github.com/owncloud/ocis/pull/5600)
 * Enhancement - Expiration Notifications: [#5330](https://github.com/owncloud/ocis/pull/5330)
-* Enhancement - GDPR Export: [#6064](https://github.com/owncloud/ocis/pull/6064)
+* Enhancement - GDPR Export: [#5950](https://github.com/owncloud/ocis/pull/5950)
 * Enhancement - Make graph/education API errors more consistent: [#5682](https://github.com/owncloud/ocis/pull/5682)
 * Enhancement - Add endpoint to list permissions: [#5594](https://github.com/owncloud/ocis/pull/5594)
 * Enhancement - No Notifications for own actions: [#5871](https://github.com/owncloud/ocis/pull/5871)
diff --git a/changelog/unreleased/gdpr-export.md b/changelog/unreleased/gdpr-export.md
index 467f93c43af..9d6740f554e 100644
--- a/changelog/unreleased/gdpr-export.md
+++ b/changelog/unreleased/gdpr-export.md
@@ -2,5 +2,4 @@ Enhancement: GDPR Export
 
 Adds an endpoint to collect all data that is related to a user
 
-https://github.com/owncloud/ocis/pull/6064
 https://github.com/owncloud/ocis/pull/5950
diff --git a/changelog/unreleased/make-emails-translatable-via-transifex.md b/changelog/unreleased/make-emails-translatable-via-transifex.md
new file mode 100644
index 00000000000..d7b40f862b7
--- /dev/null
+++ b/changelog/unreleased/make-emails-translatable-via-transifex.md
@@ -0,0 +1,9 @@
+Enhancement: Notifications
+
+Make Emails translatable via transifex
+The transifex translation add in to the email templates.
+The optional environment variable NOTIFICATIONS_TRANSLATION_PATH added to config.
+The optional global environment variable OCIS_TRANSLATION_PATH added to notifications and userlog config.
+
+https://github.com/owncloud/ocis/pull/6038
+https://github.com/owncloud/ocis/issues/6025
diff --git a/go.mod b/go.mod
index 62476eb9873..6d5c559ef9f 100644
--- a/go.mod
+++ b/go.mod
@@ -80,7 +80,7 @@ require (
 	github.com/test-go/testify v1.1.4
 	github.com/thejerf/suture/v4 v4.0.2
 	github.com/tus/tusd v1.10.1
-	github.com/urfave/cli/v2 v2.25.1
+	github.com/urfave/cli/v2 v2.24.4
 	github.com/xhit/go-simple-mail/v2 v2.13.0
 	go-micro.dev/v4 v4.9.0
 	go.etcd.io/bbolt v1.3.7
diff --git a/go.sum b/go.sum
index 7d911324b6f..fc5f4d0b7d0 100644
--- a/go.sum
+++ b/go.sum
@@ -1268,6 +1268,8 @@ github.com/maxymania/go-system v0.0.0-20170110133659-647cc364bf0b h1:Q53idHrTuQD
 github.com/maxymania/go-system v0.0.0-20170110133659-647cc364bf0b/go.mod h1:KirJrATYGbTyUwVR26xIkaipRqRcMRXBf8N5dacvGus=
 github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 h1:Z/i1e+gTZrmcGeZyWckaLfucYG6KYOXLWo4co8pZYNY=
 github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103/go.mod h1:o9YPB5aGP8ob35Vy6+vyq3P3bWe7NQWzf+JLiXCiMaE=
+github.com/micbar/reva/v2 v2.0.0-20230403215034-66e24325ae82 h1:ivuBEObF9BlpArq363tS2qB9w4YO+b+uO59GueepZhk=
+github.com/micbar/reva/v2 v2.0.0-20230403215034-66e24325ae82/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
@@ -1589,8 +1591,8 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri
 github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
 github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
-github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw=
-github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
+github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU=
+github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
diff --git a/ocis-pkg/keycloak/client.go b/ocis-pkg/keycloak/client.go
index f88f2b1421e..49cd3e9819e 100644
--- a/ocis-pkg/keycloak/client.go
+++ b/ocis-pkg/keycloak/client.go
@@ -132,14 +132,14 @@ func (c *ConcreteClient) GetPIIReport(ctx context.Context, realm string, email s
 		return nil, err
 	}
 
-	sessions, err := c.keycloak.GetUserSessions(ctx, token.AccessToken, realm, keycloakID)
+	creds, err := c.keycloak.GetCredentials(ctx, token.AccessToken, realm, keycloakID)
 	if err != nil {
 		return nil, err
 	}
 
 	return &PIIReport{
-		UserData: u,
-		Sessions: sessions,
+		UserData:    u,
+		Credentials: creds,
 	}, nil
 }
 
diff --git a/ocis-pkg/keycloak/gocloak.go b/ocis-pkg/keycloak/gocloak.go
index 52af2e053ba..c4a4de0cdac 100644
--- a/ocis-pkg/keycloak/gocloak.go
+++ b/ocis-pkg/keycloak/gocloak.go
@@ -16,5 +16,4 @@ type GoCloak interface {
 	LoginClient(ctx context.Context, clientID, clientSecret, realm string) (*gocloak.JWT, error)
 	RetrospectToken(ctx context.Context, accessToken, clientID, clientSecret, realm string) (*gocloak.IntroSpectTokenResult, error)
 	GetCredentials(ctx context.Context, accessToken, realm, userID string) ([]*gocloak.CredentialRepresentation, error)
-	GetUserSessions(ctx context.Context, token, realm, userID string) ([]*gocloak.UserSessionRepresentation, error)
 }
diff --git a/ocis-pkg/keycloak/types.go b/ocis-pkg/keycloak/types.go
index 9018938232d..366a550e319 100644
--- a/ocis-pkg/keycloak/types.go
+++ b/ocis-pkg/keycloak/types.go
@@ -26,8 +26,8 @@ var userActionsToString = map[UserAction]string{
 
 // PIIReport is a structure of all the PersonalIdentifiableInformation contained in keycloak.
 type PIIReport struct {
-	UserData *libregraph.User
-	Sessions []*gocloak.UserSessionRepresentation
+	UserData    *libregraph.User                    `json:"user_data,omitempty"`
+	Credentials []*gocloak.CredentialRepresentation `json:"credentials,omitempty"`
 }
 
 // Client represents a keycloak client.
diff --git a/services/notifications/Makefile b/services/notifications/Makefile
index 2982566e5b1..b64d1a6f536 100644
--- a/services/notifications/Makefile
+++ b/services/notifications/Makefile
@@ -1,6 +1,10 @@
 SHELL := bash
 NAME := notifications
 
+# Where to write the files generated by this makefile.
+OUTPUT_DIR = ./pkg/email/l10n
+TEMPLATE_FILE = ./pkg/email/l10n/notifications.pot
+
 include ../../.make/recursion.mk
 
 ############ tooling ############
@@ -29,6 +33,26 @@ ci-go-generate: # CI runs ci-node-generate automatically before this target
 .PHONY: ci-node-generate
 ci-node-generate:
 
+############ translations ########
+.PHONY: l10n-pull
+l10n-pull:
+	cd $(OUTPUT_DIR) && tx pull -a --skip --minimum-perc=75
+
+.PHONY: l10n-push
+l10n-push:
+	cd $(OUTPUT_DIR) && tx push -s --skip
+
+.PHONY: l10n-read
+l10n-read: $(GO_XGETTEXT)
+	go-xgettext -o $(OUTPUT_DIR)/notifications.pot --keyword=Template --add-comments -s pkg/email/l10n/templates.go
+
+.PHONY: l10n-write
+l10n-write:
+
+.PHONY: l10n-clean
+l10n-clean:
+	rm -f $(TEMPLATE_FILE);
+
 ############ licenses ############
 .PHONY: ci-node-check-licenses
 ci-node-check-licenses:
diff --git a/services/notifications/README.md b/services/notifications/README.md
index 51d58bc9b88..81b25c4b999 100644
--- a/services/notifications/README.md
+++ b/services/notifications/README.md
@@ -2,3 +2,66 @@
 
 The notification service is responsible for sending emails to users informing them about events that happened. To do this it hooks into the event system and listens for certain events that the users need to be informed about.
 
+## Email Notification Templates
+
+The `notifications` service has embedded email body templates.
+The email templates contain placeholders `{{ .Greeting }}`, `{{ .MessageBody }}`, `{{ .CallToAction }}` that are
+replaced with translations, see the [Translations](#translations) section.
+These embedded templates are available for all deployment scenarios. In addition, the service supports custom
+templates.
+The custom email template takes precedence over the embedded one. If a custom email template exists, the embedded ones
+are not used. To configure custom email templates,
+the `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` environment variable needs to point to a base folder that will contain the email
+templates. The source template files provided by ocis are located
+in [https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates](https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates) in the `shares`
+and `spaces` subfolders:
+[shares/shareCreated.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareCreated.email.body.tmpl)
+[shares/shareExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareExpired.email.body.tmpl)
+[spaces/membershipExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/membershipExpired.email.body.tmpl)
+[spaces/sharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/sharedSpace.email.body.tmpl)
+[spaces/unsharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.body.tmpl)
+
+Custom Email templates referenced via `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` must be located in subfolders `shares`
+and `spaces` and have the same names as the embedded templates. This naming must match the embedded ones.
+```text
+templates
+│
+└───shares
+│   │   shareCreated.email.body.tmpl
+│   │   shareExpired.email.body.tmpl
+│
+└───spaces
+    │   membershipExpired.email.body.tmpl
+    │   sharedSpace.email.body.tmpl
+    │   unsharedSpace.email.body.tmpl
+```
+
+## Translations
+
+The `notifications` service has embedded translations sourced via transifex to provide a basic set of translated languages.
+These embedded translations are available for all deployment scenarios. In addition, the service supports custom
+translations, though it is currently not possible to just add custom translations to embedded ones. If custom
+translations are configured, the embedded ones are not used. To configure custom translations,
+the `NOTIFICATIONS_TRANSLATION_PATH` environment variable needs to point to a base folder that will contain the translation
+files. This path must be available from all instances of the notifications service, a shared storage is recommended.
+Translation files must be of type  [.po](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files)
+or [.mo](https://www.gnu.org/software/gettext/manual/html_node/Binaries.html). For each language, the filename needs to
+be `translations.po` (or `translations.mo`) and stored in a folder structure defining the language code. In general the path/name
+pattern for a translation file needs to be:
+
+```text
+{NOTIFICATIONS_TRANSLATION_PATH}/{language-code}/LC_MESSAGES/translations.po
+```
+
+The language code pattern is composed of `language[_territory]` where  `language` is the base language and `_territory`
+is optional and defines a country.
+
+For example, for the language `de_DE`, one needs to place the corresponding translation files
+to `{NOTIFICATIONS_TRANSLATION_PATH}/de_DE/LC_MESSAGES/translations.po`.
+
+### Translation Rules
+
+*   If a requested language code is not available, the service tries to fall back to the base language if available.
+For example, if `de_DE` is not available, the service tries to fall back to translations in the `de` folder.
+*   If the base language `de` is also not available, the service falls back to the system's default English (`en`),
+which is the source of the texts provided by the code.
diff --git a/services/notifications/pkg/config/config.go b/services/notifications/pkg/config/config.go
index 90afa48b469..65ebf1c76b4 100644
--- a/services/notifications/pkg/config/config.go
+++ b/services/notifications/pkg/config/config.go
@@ -28,6 +28,7 @@ type Notifications struct {
 	Events            Events                `yaml:"events"`
 	MachineAuthAPIKey string                `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;NOTIFICATIONS_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."`
 	EmailTemplatePath string                `yaml:"email_template_path" env:"OCIS_EMAIL_TEMPLATE_PATH;NOTIFICATIONS_EMAIL_TEMPLATE_PATH" desc:"Path to Email notification templates overriding embedded ones."`
+	TranslationPath   string                `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH,NOTIFICATIONS_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details."`
 	RevaGateway       string                `yaml:"reva_gateway" env:"OCIS_REVA_GATEWAY;REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" deprecationVersion:"3.0" removalVersion:"3.1" deprecationInfo:"REVA_GATEWAY changing name for consistency" deprecationReplacement:"OCIS_REVA_GATEWAY"`
 	GRPCClientTLS     *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
 }
diff --git a/services/notifications/pkg/email/composer.go b/services/notifications/pkg/email/composer.go
new file mode 100644
index 00000000000..ece92e0a21c
--- /dev/null
+++ b/services/notifications/pkg/email/composer.go
@@ -0,0 +1,41 @@
+package email
+
+import (
+	"embed"
+	"io/fs"
+	"strings"
+
+	"github.com/leonelquinteros/gotext"
+)
+
+var (
+	//go:embed l10n/locale
+	_translationFS embed.FS
+	_domain        = "notifications"
+)
+
+// ComposeMessage renders the message based on template
+func ComposeMessage(template, locale string, path string) string {
+	raw := loadTemplate(template, locale, path)
+	return replacePlaceholders(raw)
+}
+
+func loadTemplate(template, locale string, path string) string {
+	// Create Locale with library path and language code and load default domain
+	var l *gotext.Locale
+	if path == "" {
+		filesystem, _ := fs.Sub(_translationFS, "l10n/locale")
+		l = gotext.NewLocaleFS(locale, filesystem)
+	} else { // use custom path instead
+		l = gotext.NewLocale(path, locale)
+	}
+	l.AddDomain(_domain) // make domain configurable only if needed
+	return l.Get(template)
+}
+
+func replacePlaceholders(raw string) string {
+	for o, n := range _placeholders {
+		raw = strings.ReplaceAll(raw, o, n)
+	}
+	return raw
+}
diff --git a/services/notifications/pkg/email/email.go b/services/notifications/pkg/email/email.go
index d3302ccdf74..283559b9c5f 100644
--- a/services/notifications/pkg/email/email.go
+++ b/services/notifications/pkg/email/email.go
@@ -1,8 +1,12 @@
+// Package email implements utility for rendering the Email.
+//
+// The email package supports transifex translation for email templates.
 package email
 
 import (
 	"bytes"
 	"embed"
+	"html"
 	"html/template"
 	"path/filepath"
 )
@@ -13,22 +17,63 @@ var (
 )
 
 // RenderEmailTemplate renders the email template for a new share
-func RenderEmailTemplate(templateName string, templateVariables map[string]string, emailTemplatePath string) (string, error) {
+func RenderEmailTemplate(mt MessageTemplate, locale string, emailTemplatePath string, translationPath string, vars map[string]interface{}) (string, string, error) {
+	// translate a message
+	mt.Subject = ComposeMessage(mt.Subject, locale, translationPath)
+	mt.Greeting = ComposeMessage(mt.Greeting, locale, translationPath)
+	mt.MessageBody = ComposeMessage(mt.MessageBody, locale, translationPath)
+	mt.CallToAction = ComposeMessage(mt.CallToAction, locale, translationPath)
+
+	// replace the body email placeholders with the values
+	subject, err := executeRaw(mt.Subject, vars)
+	if err != nil {
+		return "", "", err
+	}
+
+	// replace the body email template placeholders with the translated template
+	rawBody, err := executeEmailTemplate(emailTemplatePath, mt)
+	if err != nil {
+		return "", "", err
+	}
+	// replace the body email placeholders with the values
+	body, err := executeRaw(rawBody, vars)
+	if err != nil {
+		return "", "", err
+	}
+	return subject, body, nil
+}
+
+func executeEmailTemplate(emailTemplatePath string, mt MessageTemplate) (string, error) {
 	var err error
 	var tpl *template.Template
 	// try to lookup the files in the filesystem
-	tpl, err = template.ParseFiles(filepath.Join(emailTemplatePath, templateName))
+	tpl, err = template.ParseFiles(filepath.Join(emailTemplatePath, mt.bodyTemplate))
 	if err != nil {
 		// template has not been found in the fs, or path has not been specified => use embed templates
-		tpl, err = template.ParseFS(templatesFS, filepath.Join("templates/", templateName))
+		tpl, err = template.ParseFS(templatesFS, filepath.Join("templates/", mt.bodyTemplate))
 		if err != nil {
 			return "", err
 		}
 	}
-	var writer bytes.Buffer
-	err = tpl.Execute(&writer, templateVariables)
+	str, err := executeTemplate(tpl, mt)
 	if err != nil {
 		return "", err
 	}
+	return html.UnescapeString(str), err
+}
+
+func executeRaw(raw string, vars map[string]interface{}) (string, error) {
+	tpl, err := template.New("").Parse(raw)
+	if err != nil {
+		return "", err
+	}
+	return executeTemplate(tpl, vars)
+}
+
+func executeTemplate(tpl *template.Template, vars any) (string, error) {
+	var writer bytes.Buffer
+	if err := tpl.Execute(&writer, vars); err != nil {
+		return "", err
+	}
 	return writer.String(), nil
 }
diff --git a/services/notifications/pkg/email/l10n/.tx/config b/services/notifications/pkg/email/l10n/.tx/config
new file mode 100755
index 00000000000..b56891b8bb3
--- /dev/null
+++ b/services/notifications/pkg/email/l10n/.tx/config
@@ -0,0 +1,9 @@
+[main]
+host = https://www.transifex.com
+
+[o:owncloud-org:p:owncloud:r:ocis-notifications]
+file_filter = locale/<lang>/LC_MESSAGES/notifications.po
+minimum_perc = 75
+source_file = notifications.pot
+source_lang = en
+type = PO
diff --git a/services/notifications/pkg/email/l10n/locale/de_DE/LC_MESSAGES/notifications.po b/services/notifications/pkg/email/l10n/locale/de_DE/LC_MESSAGES/notifications.po
new file mode 100644
index 00000000000..a434807bea9
--- /dev/null
+++ b/services/notifications/pkg/email/l10n/locale/de_DE/LC_MESSAGES/notifications.po
@@ -0,0 +1,102 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid   ""
+msgstr  "Project-Id-Version: \n"
+        "Report-Msgid-Bugs-To: EMAIL\n"
+        "POT-Creation-Date: 2023-04-14 16:58+0200\n"
+        "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+        "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+        "Language-Team: LANGUAGE <LL@li.org>\n"
+        "Language: \n"
+        "MIME-Version: 1.0\n"
+        "Content-Type: text/plain; charset=CHARSET\n"
+        "Content-Transfer-Encoding: 8bit\n"
+
+#. UnsharedSpace email template, CallToAction field
+#: pkg/email/templates.go:57
+msgid   "Click here to check it: {ShareLink}"
+msgstr  ""
+
+#. ShareCreated email template, CallToAction field
+#. SharedSpace email template, CallToAction field
+#: pkg/email/templates.go:18 pkg/email/templates.go:43
+msgid   "Click here to view it: {ShareLink}"
+msgstr  ""
+
+#. ShareCreated email template, Greeting field
+#: pkg/email/templates.go:14
+msgid   "Hello {ShareGrantee}"
+msgstr  ""
+
+#. ShareExpired email template, Greeting field
+#: pkg/email/templates.go:26
+msgid   "Hello {ShareGrantee},"
+msgstr  ""
+
+#. SharedSpace email template, Greeting field
+#. UnsharedSpace email template, Greeting field
+#. MembershipExpired email template, Greeting field
+#: pkg/email/templates.go:39 pkg/email/templates.go:51 pkg/email/templates.go:65
+msgid   "Hello {SpaceGrantee},"
+msgstr  ""
+
+#. MembershipExpired email template, Subject field
+#: pkg/email/templates.go:63
+msgid   "Membership of '{SpaceName}' expired at {ExpiredAt}"
+msgstr  ""
+
+#. ShareExpired email template, Subject field
+#: pkg/email/templates.go:24
+msgid   "Share to '{ShareFolder}' expired at {ExpiredAt}"
+msgstr  ""
+
+#. MembershipExpired email template, Message field
+#: pkg/email/templates.go:67
+msgid   "Your membership of space {SpaceName} has expired at {ExpiredAt}\n"
+        "\n"
+        "Even though this membership has expired you still might have access through other shares and/or space memberships"
+msgstr  ""
+
+#. ShareExpired email template, Message field
+#: pkg/email/templates.go:28
+msgid   "Your share to {ShareFolder} has expired at {ExpiredAt}\n"
+        "\n"
+        "Even though this share has been revoked you still might have access through other shares and/or space memberships."
+msgstr  ""
+
+#. ShareCreated email template, Message field
+#: pkg/email/templates.go:16
+msgid   "{ShareSharer} has shared \"{ShareFolder}\" with you."
+msgstr  ""
+
+#. ShareCreated email template, Subject field
+#: pkg/email/templates.go:12
+msgid   "{ShareSharer} shared '{ShareFolder}' with you"
+msgstr  ""
+
+#. SharedSpace email template, Message field
+#: pkg/email/templates.go:41
+msgid   "{SpaceSharer} has invited you to join \"{SpaceName}\"."
+msgstr  ""
+
+#. UnsharedSpace email template, Message field
+#: pkg/email/templates.go:53
+msgid   "{SpaceSharer} has removed you from \"{SpaceName}\".\n"
+        "\n"
+        "You might still have access through your other groups or direct membership."
+msgstr  ""
+
+#. SharedSpace email template, Subject field
+#: pkg/email/templates.go:37
+msgid   "{SpaceSharer} invited you to join {SpaceName}"
+msgstr  ""
+
+#. UnsharedSpace email template, Subject field
+#: pkg/email/templates.go:49
+msgid   "{SpaceSharer} removed you from {SpaceName}"
+msgstr  ""
+
diff --git a/services/notifications/pkg/email/templates.go b/services/notifications/pkg/email/templates.go
new file mode 100644
index 00000000000..c42f205a55d
--- /dev/null
+++ b/services/notifications/pkg/email/templates.go
@@ -0,0 +1,93 @@
+package email
+
+// Template marks the string as a translatable template
+func Template(s string) string { return s }
+
+// the available templates
+var (
+	// Shares
+	ShareCreated = MessageTemplate{
+		bodyTemplate: "shares/shareCreated.email.body.tmpl",
+		// ShareCreated email template, Subject field
+		Subject: Template(`{ShareSharer} shared '{ShareFolder}' with you`),
+		// ShareCreated email template, Greeting field
+		Greeting: Template(`Hello {ShareGrantee}`),
+		// ShareCreated email template, Message field
+		MessageBody: Template(`{ShareSharer} has shared "{ShareFolder}" with you.`),
+		// ShareCreated email template, CallToAction field
+		CallToAction: Template(`Click here to view it: {ShareLink}`),
+	}
+
+	ShareExpired = MessageTemplate{
+		bodyTemplate: "shares/shareExpired.email.body.tmpl",
+		// ShareExpired email template, Subject field
+		Subject: Template(`Share to '{ShareFolder}' expired at {ExpiredAt}`),
+		// ShareExpired email template, Greeting field
+		Greeting: Template(`Hello {ShareGrantee},`),
+		// ShareExpired email template, Message field
+		MessageBody: Template(`Your share to {ShareFolder} has expired at {ExpiredAt}
+
+Even though this share has been revoked you still might have access through other shares and/or space memberships.`),
+	}
+
+	// Spaces templates
+	SharedSpace = MessageTemplate{
+		bodyTemplate: "spaces/sharedSpace.email.body.tmpl",
+		// SharedSpace email template, Subject field
+		Subject: Template("{SpaceSharer} invited you to join {SpaceName}"),
+		// SharedSpace email template, Greeting field
+		Greeting: Template(`Hello {SpaceGrantee},`),
+		// SharedSpace email template, Message field
+		MessageBody: Template(`{SpaceSharer} has invited you to join "{SpaceName}".`),
+		// SharedSpace email template, CallToAction field
+		CallToAction: Template(`Click here to view it: {ShareLink}`),
+	}
+
+	UnsharedSpace = MessageTemplate{
+		bodyTemplate: "spaces/unsharedSpace.email.body.tmpl",
+		// UnsharedSpace email template, Subject field
+		Subject: Template(`{SpaceSharer} removed you from {SpaceName}`),
+		// UnsharedSpace email template, Greeting field
+		Greeting: Template(`Hello {SpaceGrantee},`),
+		// UnsharedSpace email template, Message field
+		MessageBody: Template(`{SpaceSharer} has removed you from "{SpaceName}".
+
+You might still have access through your other groups or direct membership.`),
+		// UnsharedSpace email template, CallToAction field
+		CallToAction: Template(`Click here to check it: {ShareLink}`),
+	}
+
+	MembershipExpired = MessageTemplate{
+		bodyTemplate: "spaces/membershipExpired.email.body.tmpl",
+		// MembershipExpired email template, Subject field
+		Subject: Template(`Membership of '{SpaceName}' expired at {ExpiredAt}`),
+		// MembershipExpired email template, Greeting field
+		Greeting: Template(`Hello {SpaceGrantee},`),
+		// MembershipExpired email template, Message field
+		MessageBody: Template(`Your membership of space {SpaceName} has expired at {ExpiredAt}
+
+Even though this membership has expired you still might have access through other shares and/or space memberships`),
+	}
+)
+
+// holds the information to turn the raw template into a parseable go template
+var _placeholders = map[string]string{
+	"{ShareSharer}":  "{{ .ShareSharer }}",
+	"{ShareFolder}":  "{{ .ShareFolder }}",
+	"{ShareGrantee}": "{{ .ShareGrantee }}",
+	"{ShareLink}":    "{{ .ShareLink }}",
+	"{SpaceName}":    "{{ .SpaceName }}",
+	"{SpaceGrantee}": "{{ .SpaceGrantee }}",
+	"{SpaceSharer}":  "{{ .SpaceSharer }}",
+	"{ExpiredAt}":    "{{ .ExpiredAt }}",
+}
+
+// MessageTemplate is the data structure for the email
+type MessageTemplate struct {
+	// bodyTemplate represent the path to .tmpl file
+	bodyTemplate string
+	Subject      string
+	Greeting     string
+	MessageBody  string
+	CallToAction string
+}
diff --git a/services/notifications/pkg/email/templates/shares/shareCreated.email.body.tmpl b/services/notifications/pkg/email/templates/shares/shareCreated.email.body.tmpl
index a13c63e3dcc..a09897c8198 100644
--- a/services/notifications/pkg/email/templates/shares/shareCreated.email.body.tmpl
+++ b/services/notifications/pkg/email/templates/shares/shareCreated.email.body.tmpl
@@ -1,16 +1,8 @@
-Hello {{ .ShareGrantee }},
+{{ .Greeting }}
 
-{{ .ShareSharer }} has shared "{{ .ShareFolder }}" with you.
+{{ .MessageBody }}
 
-Click here to view it: {{ .ShareLink }}
-
-----------------------------------------------------------
-
-Hallo {{ .ShareGrantee }},
-
-{{ .ShareSharer }} hat dich zu "{{ .ShareFolder }}" eingeladen.
-
-Klicke hier zum Anzeigen: {{ .ShareLink }}
+{{ .CallToAction }}
 
 
 ---
diff --git a/services/notifications/pkg/email/templates/shares/shareCreated.email.subject.tmpl b/services/notifications/pkg/email/templates/shares/shareCreated.email.subject.tmpl
deleted file mode 100644
index 09dccb9d801..00000000000
--- a/services/notifications/pkg/email/templates/shares/shareCreated.email.subject.tmpl
+++ /dev/null
@@ -1,1 +0,0 @@
-{{ .ShareSharer }} shared '{{ .ShareFolder }}' with you
\ No newline at end of file
diff --git a/services/notifications/pkg/email/templates/shares/shareExpired.email.body.tmpl b/services/notifications/pkg/email/templates/shares/shareExpired.email.body.tmpl
index 322f28e520d..34a55c95df9 100644
--- a/services/notifications/pkg/email/templates/shares/shareExpired.email.body.tmpl
+++ b/services/notifications/pkg/email/templates/shares/shareExpired.email.body.tmpl
@@ -1,16 +1,6 @@
-Hello {{ .ShareGrantee }},
+{{ .Greeting }}
 
-Your share to {{ .ShareFolder }} has expired at {{ .ExpiredAt }}
-
-Even though this share has been revoked you still might have access through other shares and/or space memberships
-
-----------------------------------------------------------
-
-Hallo {{ .ShareGrantee }},
-
-Deine Freigabe zu {{ .ShareFolder }} ist am {{ .ExpiredAt }} abgelaufen.
-
-Obwohl diese Freigabe nicht mehr zur Verfügung steht, könntest du immer noch Zugriff über andere Freigaben und/oder Space Mitgliedschaften haben.
+{{ .MessageBody }}
 
 
 ---
diff --git a/services/notifications/pkg/email/templates/shares/shareExpired.email.subject.tmpl b/services/notifications/pkg/email/templates/shares/shareExpired.email.subject.tmpl
deleted file mode 100644
index e9593dcd5af..00000000000
--- a/services/notifications/pkg/email/templates/shares/shareExpired.email.subject.tmpl
+++ /dev/null
@@ -1,1 +0,0 @@
-Share to '{{ .ShareFolder }}' expired at {{ .ExpiredAt }}
\ No newline at end of file
diff --git a/services/notifications/pkg/email/templates/spaces/membershipExpired.email.body.tmpl b/services/notifications/pkg/email/templates/spaces/membershipExpired.email.body.tmpl
index 95ba3897bab..34a55c95df9 100644
--- a/services/notifications/pkg/email/templates/spaces/membershipExpired.email.body.tmpl
+++ b/services/notifications/pkg/email/templates/spaces/membershipExpired.email.body.tmpl
@@ -1,16 +1,6 @@
-Hello {{ .SpaceGrantee }},
+{{ .Greeting }}
 
-Your membership of space {{ .SpaceName }} has expired at {{ .ExpiredAt }}
-
-Even though this membership has expired you still might have access through other shares and/or space memberships
-
-----------------------------------------------------------
-
-Hallo {{ .SpaceGrantee }},
-
-Deine Mitgliedschaft zu dem Space {{ .SpaceName }} ist am {{ .ExpiredAt }} abgelaufen.
-
-Obwohl diese Mitgliedschaft nicht mehr zur Verfügung steht, könntest du immer noch Zugriff über andere Freigaben und/oder Space Mitgliedschaften haben.
+{{ .MessageBody }}
 
 
 ---
diff --git a/services/notifications/pkg/email/templates/spaces/membershipExpired.email.subject.tmpl b/services/notifications/pkg/email/templates/spaces/membershipExpired.email.subject.tmpl
deleted file mode 100644
index 93658183883..00000000000
--- a/services/notifications/pkg/email/templates/spaces/membershipExpired.email.subject.tmpl
+++ /dev/null
@@ -1,1 +0,0 @@
-Membership of '{{ .SpaceName }}' expired at {{ .ExpiredAt }}
\ No newline at end of file
diff --git a/services/notifications/pkg/email/templates/spaces/sharedSpace.email.body.tmpl b/services/notifications/pkg/email/templates/spaces/sharedSpace.email.body.tmpl
index 897e4eb66a3..a09897c8198 100644
--- a/services/notifications/pkg/email/templates/spaces/sharedSpace.email.body.tmpl
+++ b/services/notifications/pkg/email/templates/spaces/sharedSpace.email.body.tmpl
@@ -1,16 +1,8 @@
-Hello {{ .SpaceGrantee }},
+{{ .Greeting }}
 
-{{ .SpaceSharer }} has invited you to join "{{ .SpaceName }}".
+{{ .MessageBody }}
 
-Click here to view it: {{ .ShareLink }}
-
-----------------------------------------------------------
-
-Hallo {{ .SpaceGrantee }},
-
-{{ .SpaceSharer }} hat dich in den Space "{{ .SpaceName }}" eingeladen.
-
-Klicke hier zum Anzeigen: {{ .ShareLink }}
+{{ .CallToAction }}
 
 
 ---
diff --git a/services/notifications/pkg/email/templates/spaces/sharedSpace.email.subject.tmpl b/services/notifications/pkg/email/templates/spaces/sharedSpace.email.subject.tmpl
deleted file mode 100644
index e65e04292d1..00000000000
--- a/services/notifications/pkg/email/templates/spaces/sharedSpace.email.subject.tmpl
+++ /dev/null
@@ -1,1 +0,0 @@
-{{ .SpaceSharer }} invited you to join {{ .SpaceName }}
\ No newline at end of file
diff --git a/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.body.tmpl b/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.body.tmpl
index 4c6fa9ece4c..a09897c8198 100644
--- a/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.body.tmpl
+++ b/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.body.tmpl
@@ -1,16 +1,8 @@
-Hello {{ .SpaceGrantee }},
+{{ .Greeting }}
 
-{{ .SpaceSharer }} has removed you from "{{ .SpaceName }}".
+{{ .MessageBody }}
 
-You might still have access through your other groups or direct membership. Click here to check it: {{ .ShareLink }}
-
-----------------------------------------------------------
-
-Hallo {{ .SpaceGrantee }},
-
-{{ .SpaceSharer }} hat dich aus dem Space "{{ .SpaceName }}" entfernt.
-
-Du könntest über deine anderen Gruppen oder deiner direkten Mitgliedschaft noch Zugriff haben. Klicke hier zum Überprüfen: {{ .ShareLink }}
+{{ .CallToAction }}
 
 
 ---
diff --git a/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.subject.tmpl b/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.subject.tmpl
deleted file mode 100644
index c6628dfb3dd..00000000000
--- a/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.subject.tmpl
+++ /dev/null
@@ -1,1 +0,0 @@
-{{ .SpaceSharer }} removed you from {{ .SpaceName }}
\ No newline at end of file
diff --git a/services/notifications/pkg/service/service.go b/services/notifications/pkg/service/service.go
index 9c3df81f91d..714696e720f 100644
--- a/services/notifications/pkg/service/service.go
+++ b/services/notifications/pkg/service/service.go
@@ -54,6 +54,7 @@ type eventsNotifier struct {
 	gwClient          gateway.GatewayAPIClient
 	machineAuthAPIKey string
 	emailTemplatePath string
+	translationPath   string
 	ocisURL           string
 }
 
@@ -86,18 +87,9 @@ func (s eventsNotifier) Run() error {
 	}
 }
 
-func (s eventsNotifier) render(bodyTemplate string, subjTemplate string, values map[string]string) (string, string, error) {
-	msg, err := email.RenderEmailTemplate(bodyTemplate, values, s.emailTemplatePath)
-	if err != nil {
-		return "", "", err
-	}
-
-	sub, err := email.RenderEmailTemplate(subjTemplate, values, s.emailTemplatePath)
-	if err != nil {
-		return "", "", err
-	}
-
-	return msg, sub, nil
+func (s eventsNotifier) render(template email.MessageTemplate, values map[string]interface{}) (string, string, error) {
+	// The locate have to come from the user setting
+	return email.RenderEmailTemplate(template, "en", s.emailTemplatePath, s.translationPath, values)
 }
 
 func (s eventsNotifier) send(ctx context.Context, u *user.UserId, g *group.GroupId, msg, subj, sender string) error {
diff --git a/services/notifications/pkg/service/shares.go b/services/notifications/pkg/service/shares.go
index 67588494366..6451783bc2f 100644
--- a/services/notifications/pkg/service/shares.go
+++ b/services/notifications/pkg/service/shares.go
@@ -3,6 +3,7 @@ package service
 import (
 	"github.com/cs3org/reva/v2/pkg/events"
 	"github.com/cs3org/reva/v2/pkg/utils"
+	"github.com/owncloud/ocis/v2/services/notifications/pkg/email"
 	"google.golang.org/protobuf/types/known/fieldmaskpb"
 )
 
@@ -41,7 +42,7 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
 	}
 
 	sharerDisplayName := owner.GetDisplayName()
-	msg, subj, err := s.render("shares/shareCreated.email.body.tmpl", "shares/shareCreated.email.subject.tmpl", map[string]string{
+	subj, msg, err := s.render(email.ShareCreated, map[string]interface{}{
 		"ShareGrantee": shareGrantee,
 		"ShareSharer":  sharerDisplayName,
 		"ShareFolder":  resourceInfo.Name,
@@ -84,7 +85,7 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
 		return
 	}
 
-	msg, subj, err := s.render("shares/shareExpired.email.body.tmpl", "shares/shareExpired.email.subject.tmpl", map[string]string{
+	subj, msg, err := s.render(email.ShareExpired, map[string]interface{}{
 		"ShareGrantee": shareGrantee,
 		"ShareFolder":  resourceInfo.GetName(),
 		"ExpiredAt":    e.ExpiredAt.Format("2006-01-02 15:04:05"),
diff --git a/services/notifications/pkg/service/spaces.go b/services/notifications/pkg/service/spaces.go
index 44affdfcc46..bf7d7473428 100644
--- a/services/notifications/pkg/service/spaces.go
+++ b/services/notifications/pkg/service/spaces.go
@@ -4,6 +4,7 @@ import (
 	"github.com/cs3org/reva/v2/pkg/events"
 	"github.com/cs3org/reva/v2/pkg/storagespace"
 	"github.com/cs3org/reva/v2/pkg/utils"
+	"github.com/owncloud/ocis/v2/services/notifications/pkg/email"
 )
 
 func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
@@ -54,7 +55,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
 	}
 
 	sharerDisplayName := owner.GetDisplayName()
-	msg, subj, err := s.render("spaces/sharedSpace.email.body.tmpl", "spaces/sharedSpace.email.subject.tmpl", map[string]string{
+	subj, msg, err := s.render(email.SharedSpace, map[string]interface{}{
 		"SpaceGrantee": spaceGrantee,
 		"SpaceSharer":  sharerDisplayName,
 		"SpaceName":    resourceInfo.GetSpace().GetName(),
@@ -116,7 +117,7 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
 	}
 
 	sharerDisplayName := owner.GetDisplayName()
-	msg, subj, err := s.render("spaces/unsharedSpace.email.body.tmpl", "spaces/unsharedSpace.email.subject.tmpl", map[string]string{
+	subj, msg, err := s.render(email.UnsharedSpace, map[string]interface{}{
 		"SpaceGrantee": spaceGrantee,
 		"SpaceSharer":  sharerDisplayName,
 		"SpaceName":    resourceInfo.GetSpace().Name,
@@ -151,7 +152,7 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp
 		return
 	}
 
-	msg, subj, err := s.render("spaces/membershipExpired.email.body.tmpl", "spaces/membershipExpired.email.subject.tmpl", map[string]string{
+	subj, msg, err := s.render(email.MembershipExpired, map[string]interface{}{
 		"SpaceGrantee": shareGrantee,
 		"SpaceName":    e.SpaceName,
 		"ExpiredAt":    e.ExpiredAt.Format("2006-01-02 15:04:05"),
diff --git a/services/userlog/pkg/config/config.go b/services/userlog/pkg/config/config.go
index 41985cb7707..6998f826029 100644
--- a/services/userlog/pkg/config/config.go
+++ b/services/userlog/pkg/config/config.go
@@ -23,7 +23,7 @@ type Config struct {
 
 	MachineAuthAPIKey string      `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;USERLOG_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."`
 	RevaGateway       string      `yaml:"reva_gateway" env:"OCIS_REVA_GATEWAY;REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" deprecationVersion:"3.0" removalVersion:"3.1" deprecationInfo:"REVA_GATEWAY changing name for consistency" deprecationReplacement:"OCIS_REVA_GATEWAY"`
-	TranslationPath   string      `yaml:"translation_path" env:"USERLOG_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. See the documentation for more details."`
+	TranslationPath   string      `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH,USERLOG_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details."`
 	Events            Events      `yaml:"events"`
 	Persistence       Persistence `yaml:"persistence"`
 
diff --git a/services/userlog/pkg/service/conversion.go b/services/userlog/pkg/service/conversion.go
index c2cd6081a93..e1bce3525c6 100644
--- a/services/userlog/pkg/service/conversion.go
+++ b/services/userlog/pkg/service/conversion.go
@@ -356,7 +356,6 @@ func composeMessage(nt NotificationTemplate, locale string, path string, vars ma
 
 	message, err := executeTemplate(messageraw, vars)
 	return subject, subjectraw, message, messageraw, err
-
 }
 
 func loadTemplates(nt NotificationTemplate, locale string, path string) (string, string) {


Test patch:
diff --git a/services/notifications/pkg/service/service_test.go b/services/notifications/pkg/service/service_test.go
index a9c323473a9..aad56181995 100644
--- a/services/notifications/pkg/service/service_test.go
+++ b/services/notifications/pkg/service/service_test.go
@@ -66,8 +66,19 @@ var _ = Describe("Notifications", func() {
 		Entry("Share Created", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Dr. S. Harer shared 'secrets of the board' with you",
-			expectedSender:      sharer.GetDisplayName(),
-			done:                make(chan struct{}),
+			expectedMessage: `Hello Dr. S. Harer
+
+Dr. S. Harer has shared "secrets of the board" with you.
+
+Click here to view it: files/shares/with-me
+
+
+---
+ownCloud - Store. Share. Work.
+https://owncloud.com
+`,
+			expectedSender: sharer.GetDisplayName(),
+			done:           make(chan struct{}),
 		}, events.Event{
 			Event: events.ShareCreated{
 				Sharer:        sharer.GetId(),
@@ -80,8 +91,19 @@ var _ = Describe("Notifications", func() {
 		Entry("Share Expired", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Share to 'secrets of the board' expired at 2023-04-17 16:42:00",
-			expectedSender:      sharer.GetDisplayName(),
-			done:                make(chan struct{}),
+			expectedMessage: `Hello Dr. S. Harer,
+
+Your share to secrets of the board has expired at 2023-04-17 16:42:00
+
+Even though this share has been revoked you still might have access through other shares and/or space memberships.
+
+
+---
+ownCloud - Store. Share. Work.
+https://owncloud.com
+`,
+			expectedSender: sharer.GetDisplayName(),
+			done:           make(chan struct{}),
 		}, events.Event{
 			Event: events.ShareExpired{
 				ShareOwner:    sharer.GetId(),
@@ -94,8 +116,19 @@ var _ = Describe("Notifications", func() {
 		Entry("Added to Space", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Dr. S. Harer invited you to join secret space",
-			expectedSender:      sharer.GetDisplayName(),
-			done:                make(chan struct{}),
+			expectedMessage: `Hello Dr. S. Harer,
+
+Dr. S. Harer has invited you to join "secret space".
+
+Click here to view it: f/spaceid
+
+
+---
+ownCloud - Store. Share. Work.
+https://owncloud.com
+`,
+			expectedSender: sharer.GetDisplayName(),
+			done:           make(chan struct{}),
 		}, events.Event{
 			Event: events.SpaceShared{
 				Executant:     sharer.GetId(),
@@ -108,8 +141,21 @@ var _ = Describe("Notifications", func() {
 		Entry("Removed from Space", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Dr. S. Harer removed you from secret space",
-			expectedSender:      sharer.GetDisplayName(),
-			done:                make(chan struct{}),
+			expectedMessage: `Hello Dr. S. Harer,
+
+Dr. S. Harer has removed you from "secret space".
+
+You might still have access through your other groups or direct membership.
+
+Click here to check it: f/spaceid
+
+
+---
+ownCloud - Store. Share. Work.
+https://owncloud.com
+`,
+			expectedSender: sharer.GetDisplayName(),
+			done:           make(chan struct{}),
 		}, events.Event{
 			Event: events.SpaceUnshared{
 				Executant:     sharer.GetId(),
@@ -121,8 +167,19 @@ var _ = Describe("Notifications", func() {
 		Entry("Space Expired", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Membership of 'secret space' expired at 2023-04-17 16:42:00",
-			expectedSender:      sharer.GetDisplayName(),
-			done:                make(chan struct{}),
+			expectedMessage: `Hello Dr. S. Harer,
+
+Your membership of space secret space has expired at 2023-04-17 16:42:00
+
+Even though this membership has expired you still might have access through other shares and/or space memberships
+
+
+---
+ownCloud - Store. Share. Work.
+https://owncloud.com
+`,
+			expectedSender: sharer.GetDisplayName(),
+			done:           make(chan struct{}),
 		}, events.Event{
 			Event: events.SpaceMembershipExpired{
 				SpaceOwner:    sharer.GetId(),
@@ -139,6 +196,7 @@ var _ = Describe("Notifications", func() {
 type testChannel struct {
 	expectedReceipients map[string]bool
 	expectedSubject     string
+	expectedMessage     string
 	expectedSender      string
 	done                chan struct{}
 }
@@ -150,8 +208,7 @@ func (tc testChannel) SendMessage(ctx context.Context, userIDs []string, msg, su
 		Expect(tc.expectedReceipients[u]).To(Equal(true))
 	}
 
-	// TODO: test the message?
-	//Expect(msg).To(Equal(tc.expectedMessage))
+	Expect(msg).To(Equal(tc.expectedMessage))
 	Expect(subject).To(Equal(tc.expectedSubject))
 	Expect(senderDisplayName).To(Equal(tc.expectedSender))
 	tc.done <- struct{}{}