Merge pull request #8723 from kobergj/SpaceTemplatesII

Server-Side Space Templates
This commit is contained in:
kobergj
2024-03-28 11:56:15 +01:00
committed by GitHub
23 changed files with 387 additions and 298 deletions
+5
View File
@@ -0,0 +1,5 @@
Enhancement: Make server side space templates production ready
Fixes several smaller bugs and adds some improvements to space templates, introduced with https://github.com/owncloud/ocis/pull/8558
https://github.com/owncloud/ocis/pull/8723
+90
View File
@@ -0,0 +1,90 @@
// package l10n holds translation mechanics that are used by user facing services (notifications, userlog, graph)
package l10n
import (
"context"
"errors"
"io/fs"
"os"
"github.com/leonelquinteros/gotext"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
micrometadata "go-micro.dev/v4/metadata"
)
// Template marks a string as translatable
func Template(s string) string { return s }
// Translator is able to translate strings
type Translator struct {
fs fs.FS
defaultLocale string
domain string
}
// NewTranslator creates a Translator with library path and language code and load default domain
func NewTranslator(defaultLocale string, domain string, fsys fs.FS) Translator {
return Translator{
fs: fsys,
defaultLocale: defaultLocale,
domain: domain,
}
}
// NewTranslatorFromCommonConfig creates a new Translator from legacy config
func NewTranslatorFromCommonConfig(defaultLocale string, domain string, path string, fsys fs.FS, fsSubPath string) Translator {
var filesystem fs.FS
if path == "" {
filesystem, _ = fs.Sub(fsys, fsSubPath)
} else { // use custom path instead
filesystem = os.DirFS(path)
}
return NewTranslator(defaultLocale, domain, filesystem)
}
// Translate translates a string to the locale
func (t Translator) Translate(str, locale string) string {
return t.Locale(locale).Get(str)
}
// Locale returns the gotext.Locale, use `.Get` method to translate strings
func (t Translator) Locale(locale string) *gotext.Locale {
l := gotext.NewLocaleFS(locale, t.fs)
l.AddDomain(t.domain) // make domain configurable only if needed
if locale != "en" && len(l.GetTranslations()) == 0 {
l = gotext.NewLocaleFS(t.defaultLocale, t.fs)
l.AddDomain(t.domain) // make domain configurable only if needed
}
return l
}
// MustGetUserLocale returns the locale the user wants to use, omitting errors
func MustGetUserLocale(ctx context.Context, userID string, preferedLang string, vc settingssvc.ValueService) string {
if preferedLang != "" {
return preferedLang
}
locale, _ := GetUserLocale(ctx, userID, vc)
return locale
}
// GetUserLocale returns the locale of the user
func GetUserLocale(ctx context.Context, userID string, vc settingssvc.ValueService) (string, error) {
resp, err := vc.GetValueByUniqueIdentifiers(
micrometadata.Set(ctx, middleware.AccountID, userID),
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: userID,
SettingId: defaults.SettingUUIDProfileLanguage,
},
)
if err != nil {
return "", err
}
val := resp.GetValue().GetValue().GetListValue().GetValues()
if len(val) == 0 {
return "", errors.New("no language setting found")
}
return val[0].GetStringValue(), nil
}
+25
View File
@@ -1,6 +1,10 @@
SHELL := bash SHELL := bash
NAME := graph NAME := graph
# Where to write the files generated by this makefile.
OUTPUT_DIR = ./pkg/service/v0/l10n
TEMPLATE_FILE = ./pkg/service/v0/l10n/graph.pot
include ../../.make/recursion.mk include ../../.make/recursion.mk
############ tooling ############ ############ tooling ############
@@ -30,6 +34,27 @@ ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this
.PHONY: ci-node-generate .PHONY: ci-node-generate
ci-node-generate: ci-node-generate:
############ translations ########
.PHONY: l10n-pull
l10n-pull:
cd $(OUTPUT_DIR) && tx pull --all --force --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)/graph.pot --keyword=l10n.Template --add-comments -s pkg/service/v0/spacetemplates.go
.PHONY: l10n-write
l10n-write:
.PHONY: l10n-clean
l10n-clean:
rm -f $(TEMPLATE_FILE);
############ licenses ############ ############ licenses ############
.PHONY: ci-node-check-licenses .PHONY: ci-node-check-licenses
ci-node-check-licenses: ci-node-check-licenses:
+1
View File
@@ -46,6 +46,7 @@ type Spaces struct {
UsersCacheTTL int `yaml:"users_cache_ttl" env:"GRAPH_SPACES_USERS_CACHE_TTL" desc:"Max TTL in seconds for the spaces users cache." introductionVersion:"pre5.0"` UsersCacheTTL int `yaml:"users_cache_ttl" env:"GRAPH_SPACES_USERS_CACHE_TTL" desc:"Max TTL in seconds for the spaces users cache." introductionVersion:"pre5.0"`
GroupsCacheTTL int `yaml:"groups_cache_ttl" env:"GRAPH_SPACES_GROUPS_CACHE_TTL" desc:"Max TTL in seconds for the spaces groups cache." introductionVersion:"pre5.0"` GroupsCacheTTL int `yaml:"groups_cache_ttl" env:"GRAPH_SPACES_GROUPS_CACHE_TTL" desc:"Max TTL in seconds for the spaces groups cache." introductionVersion:"pre5.0"`
StorageUsersAddress string `yaml:"storage_users_address" env:"GRAPH_SPACES_STORAGE_USERS_ADDRESS" desc:"The address of the storage-users service." introductionVersion:"5.0"` StorageUsersAddress string `yaml:"storage_users_address" env:"GRAPH_SPACES_STORAGE_USERS_ADDRESS" desc:"The address of the storage-users service." introductionVersion:"5.0"`
DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details." introductionVersion:"5.0"`
} }
type LDAP struct { type LDAP struct {
+26 -11
View File
@@ -29,6 +29,7 @@ import (
"github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils" "github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/ocis-pkg/conversions" "github.com/owncloud/ocis/v2/ocis-pkg/conversions"
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0" v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
@@ -326,7 +327,10 @@ func (g Graph) canCreateSpace(ctx context.Context, ownPersonalHome bool) bool {
func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) { func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context()) logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling create drive") logger.Info().Msg("calling create drive")
us, ok := revactx.ContextGetUser(r.Context())
ctx := r.Context()
us, ok := revactx.ContextGetUser(ctx)
if !ok { if !ok {
logger.Debug().Msg("could not create drive: invalid user") logger.Debug().Msg("could not create drive: invalid user")
errorcode.NotAllowed.Render(w, r, http.StatusUnauthorized, "invalid user") errorcode.NotAllowed.Render(w, r, http.StatusUnauthorized, "invalid user")
@@ -334,7 +338,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
} }
// TODO determine if the user tries to create his own personal space and pass that as a boolean // TODO determine if the user tries to create his own personal space and pass that as a boolean
canCreateSpace := g.canCreateSpace(r.Context(), false) canCreateSpace := g.canCreateSpace(ctx, false)
if !canCreateSpace { if !canCreateSpace {
logger.Debug().Bool("cancreatespace", canCreateSpace).Msg("could not create drive: insufficient permissions") logger.Debug().Bool("cancreatespace", canCreateSpace).Msg("could not create drive: insufficient permissions")
// if the permission is not existing for the user in context we can assume we don't have it. Return 401. // if the permission is not existing for the user in context we can assume we don't have it. Return 401.
@@ -393,7 +397,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
csr.Owner = us csr.Owner = us
} }
resp, err := gatewayClient.CreateStorageSpace(r.Context(), &csr) resp, err := gatewayClient.CreateStorageSpace(ctx, &csr)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("could not create drive: transport error") logger.Error().Err(err).Msg("could not create drive: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
@@ -423,27 +427,38 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
return return
} }
if driveType == _spaceTypeProject { space := resp.GetStorageSpace()
opaque, err := g.applySpaceTemplate(gatewayClient, resp.GetStorageSpace().GetRoot(), r.URL.Query().Get("template")) if t := r.URL.Query().Get(TemplateParameter); t != "" && driveType == _spaceTypeProject {
if err != nil { loc := l10n.MustGetUserLocale(ctx, us.GetId().GetOpaqueId(), r.Header.Get(HeaderAcceptLanguage), g.valueService)
if err := g.applySpaceTemplate(ctx, gatewayClient, space.GetRoot(), t, loc); err != nil {
logger.Error().Err(err).Msg("could not apply template to space") logger.Error().Err(err).Msg("could not apply template to space")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return return
} }
resp.StorageSpace.Opaque = utils.MergeOpaques(resp.GetStorageSpace().GetOpaque(), opaque) // refetch the drive to get quota information - should we calculate this ourselves to avoid the extra call?
space, err = utils.GetSpace(ctx, space.GetId().GetOpaqueId(), gatewayClient)
if err != nil {
logger.Error().Err(err).Msg("could not refetch space")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
} }
newDrive, err := g.cs3StorageSpaceToDrive(r.Context(), webDavBaseURL, resp.GetStorageSpace(), APIVersion_1) spaces, err := g.formatDrives(ctx, webDavBaseURL, []*storageprovider.StorageSpace{space}, APIVersion_1)
if err != nil { if err != nil {
logger.Debug().Err(err).Msg("could not create drive: error parsing drive") logger.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return return
} }
newDrive.Special = g.getSpecialDriveItems(r.Context(), webDavBaseURL, resp.GetStorageSpace()) if len(spaces) == 0 {
logger.Error().Msg("could not convert space")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not convert space")
return
}
render.Status(r, http.StatusCreated) render.Status(r, http.StatusCreated)
render.JSON(w, r, newDrive) render.JSON(w, r, spaces[0])
} }
// UpdateDrive updates the properties of a storage drive (space). // UpdateDrive updates the properties of a storage drive (space).
@@ -764,6 +764,12 @@ var _ = Describe("Graph", func() {
Constraint: v0.Permission_CONSTRAINT_ALL, Constraint: v0.Permission_CONSTRAINT_ALL,
}, },
}, nil) }, nil)
gatewayClient.On("GetQuota", mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
Status: status.NewOK(ctx),
TotalBytes: 500,
}, nil)
jsonBody := []byte(`{"Name": "Test Space", "DriveType": "project", "Description": "This space is for testing", "DriveAlias": "project/testspace"}`) jsonBody := []byte(`{"Name": "Test Space", "DriveType": "project", "Description": "This space is for testing", "DriveAlias": "project/testspace"}`)
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx) r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:owncloud-org:p:owncloud:r:ocis-graph]
file_filter = locale/<lang>/LC_MESSAGES/graph.po
minimum_perc = 75
source_file = graph.pot
source_lang = en
type = PO
@@ -0,0 +1,22 @@
# 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: 2024-03-26 15:11+0100\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/v0/spacetemplates.go:29
msgid "Here you can add a description for this Space."
msgstr ""
-6
View File
@@ -3,7 +3,6 @@ package svc
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"embed"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -36,11 +35,6 @@ const (
displayNameAttr = "displayName" displayNameAttr = "displayName"
) )
var (
//go:embed spacetemplate/*
_spaceTemplateFS embed.FS
)
// Service defines the service handlers. // Service defines the service handlers.
type Service interface { type Service interface {
ServeHTTP(http.ResponseWriter, *http.Request) ServeHTTP(http.ResponseWriter, *http.Request)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

@@ -1 +0,0 @@
Here you can add a description for this Space.
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,133 @@
package svc
import (
"context"
"embed"
"fmt"
"io/fs"
"path/filepath"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
v1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/storage/utils/metadata"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
)
var (
//go:embed spacetemplate/*
_spaceTemplateFS embed.FS
//go:embed l10n/locale
_localeFS embed.FS
// subfolder where the translation files are stored
_localeSubPath = "l10n/locale"
// name of the secret space folder
_spaceFolderName = ".space"
// path to the image file
_imagepath = "spacetemplate/image.png"
// text for the readme.md file
_readmeText = l10n.Template("Here you can add a description for this Space.")
// name of the readme.md file
_readmeName = "readme.md"
// domain of the graph service (transifex)
_domain = "graph"
// HeaderAcceptLanguage is the header key for the accept-language header
HeaderAcceptLanguage = "Accept-Language"
// TemplateParameter is the key for the template parameter in the request
TemplateParameter = "template"
)
func (g Graph) applySpaceTemplate(ctx context.Context, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, template string, locale string) error {
switch template {
default:
fallthrough
case "none":
return nil
case "default":
return g.applyDefaultTemplate(ctx, gwc, root, locale)
}
}
func (g Graph) applyDefaultTemplate(ctx context.Context, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, locale string) error {
mdc := metadata.NewCS3(g.config.Reva.Address, g.config.Spaces.StorageUsersAddress)
mdc.SpaceRoot = root
var opaque *v1beta1.Opaque
// create .space folder
if err := mdc.MakeDirIfNotExist(ctx, _spaceFolderName); err != nil {
return err
}
// upload space image
iid, err := imageUpload(ctx, mdc)
if err != nil {
return err
}
opaque = utils.AppendPlainToOpaque(opaque, SpaceImageSpecialFolderName, iid)
// upload readme.md
rid, err := readmeUpload(ctx, mdc, locale, g.config.Spaces.DefaultLanguage)
if err != nil {
return err
}
opaque = utils.AppendPlainToOpaque(opaque, ReadmeSpecialFolderName, rid)
// update space
resp, err := gwc.UpdateStorageSpace(ctx, &storageprovider.UpdateStorageSpaceRequest{
StorageSpace: &storageprovider.StorageSpace{
Id: &storageprovider.StorageSpaceId{
OpaqueId: storagespace.FormatResourceID(*root),
},
Root: root,
Opaque: opaque,
},
})
switch {
case err != nil:
return err
case resp.GetStatus().GetCode() != rpc.Code_CODE_OK:
return fmt.Errorf("could not update storage space: %s", resp.GetStatus().GetMessage())
default:
return nil
}
}
func imageUpload(ctx context.Context, mdc *metadata.CS3) (string, error) {
b, err := fs.ReadFile(_spaceTemplateFS, _imagepath)
if err != nil {
return "", err
}
res, err := mdc.Upload(ctx, metadata.UploadRequest{
Path: filepath.Join(_spaceFolderName, filepath.Base(_imagepath)),
Content: b,
})
if err != nil {
return "", err
}
return res.FileID, nil
}
func readmeUpload(ctx context.Context, mdc *metadata.CS3, locale string, defaultLocale string) (string, error) {
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, "", _localeFS, _localeSubPath)
res, err := mdc.Upload(ctx, metadata.UploadRequest{
Path: filepath.Join(_spaceFolderName, _readmeName),
Content: []byte(t.Translate(_readmeText, locale)),
})
if err != nil {
return "", err
}
return res.FileID, nil
}
-109
View File
@@ -3,22 +3,14 @@ package svc
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"io/fs"
"net/http" "net/http"
"os"
"path/filepath"
"reflect" "reflect"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
v1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/storage/utils/metadata"
"github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils" "github.com/cs3org/reva/v2/pkg/utils"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@@ -445,104 +437,3 @@ func cs3ReceivedShareToLibreGraphPermissions(ctx context.Context, logger *log.Lo
return permission, nil return permission, nil
} }
func (g Graph) applySpaceTemplate(gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, template string) (*v1beta1.Opaque, error) {
var fsys fs.ReadDirFS
switch template {
default:
fallthrough
case "none":
return &v1beta1.Opaque{}, nil
case "default":
f, err := fs.Sub(_spaceTemplateFS, "spacetemplate")
if err != nil {
return nil, err
}
fsys = f.(fs.ReadDirFS)
}
mdc := metadata.NewCS3(g.config.Reva.Address, g.config.Spaces.StorageUsersAddress)
mdc.SpaceRoot = root
ctx, err := utils.GetServiceUserContext(g.config.ServiceAccount.ServiceAccountID, gwc, g.config.ServiceAccount.ServiceAccountSecret)
if err != nil {
return nil, err
}
opaque, err := uploadFolder(ctx, mdc, ".", "", nil, fsys)
if err != nil {
return nil, err
}
resp, err := gwc.UpdateStorageSpace(ctx, &storageprovider.UpdateStorageSpaceRequest{
StorageSpace: &storageprovider.StorageSpace{
Id: &storageprovider.StorageSpaceId{
OpaqueId: storagespace.FormatResourceID(*root),
},
Root: root,
Opaque: opaque,
},
})
switch {
case err != nil:
return nil, err
case resp.GetStatus().GetCode() != rpc.Code_CODE_OK:
return nil, fmt.Errorf("could not update storage space: %s", resp.GetStatus().GetMessage())
default:
return opaque, nil
}
}
func uploadFolder(ctx context.Context, mdc *metadata.CS3, pathOnDisc, pathOnSpace string, opaque *v1beta1.Opaque, fsys fs.ReadDirFS) (*v1beta1.Opaque, error) {
entries, err := fsys.ReadDir(pathOnDisc)
if err != nil {
return nil, err
}
for _, entry := range entries {
opaque, err = uploadEntry(ctx, mdc, pathOnDisc, pathOnSpace, opaque, fsys, entry)
if err != nil {
return nil, err
}
}
return opaque, nil
}
func uploadEntry(ctx context.Context, mdc *metadata.CS3, pathOnDisc, pathOnSpace string, opaque *v1beta1.Opaque, fsys fs.ReadDirFS, entry os.DirEntry) (*v1beta1.Opaque, error) {
spacePath := filepath.Join(pathOnSpace, entry.Name())
discPath := filepath.Join(pathOnDisc, entry.Name())
switch {
case entry.IsDir():
err := mdc.MakeDirIfNotExist(ctx, spacePath)
if err != nil {
return nil, err
}
return uploadFolder(ctx, mdc, discPath, spacePath, opaque, fsys)
default:
b, err := fs.ReadFile(fsys, discPath)
if err != nil {
return nil, err
}
res, err := mdc.Upload(ctx, metadata.UploadRequest{
Path: spacePath,
Content: b,
})
if err != nil {
return nil, err
}
identifier := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
for _, special := range []string{ReadmeSpecialFolderName, SpaceImageSpecialFolderName} {
if special == identifier {
opaque = utils.AppendPlainToOpaque(opaque, identifier, res.FileID)
break
}
}
return opaque, nil
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ l10n-push:
.PHONY: l10n-read .PHONY: l10n-read
l10n-read: $(GO_XGETTEXT) l10n-read: $(GO_XGETTEXT)
go-xgettext -o $(OUTPUT_DIR)/notifications.pot --keyword=Template --add-comments -s pkg/email/templates.go go-xgettext -o $(OUTPUT_DIR)/notifications.pot --keyword=l10n.Template --add-comments -s pkg/email/templates.go
.PHONY: l10n-write .PHONY: l10n-write
l10n-write: l10n-write:
+18 -11
View File
@@ -2,29 +2,36 @@ package email
import ( import (
"bytes" "bytes"
"embed"
"strings" "strings"
"text/template" "text/template"
"github.com/owncloud/ocis/v2/services/notifications/pkg/email/l10n" "github.com/owncloud/ocis/v2/ocis-pkg/l10n"
)
var (
//go:embed l10n/locale
_translationFS embed.FS
_domain = "notifications"
) )
// NewTextTemplate replace the body message template placeholders with the translated template // NewTextTemplate replace the body message template placeholders with the translated template
func NewTextTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) { func NewTextTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) {
var err error var err error
t := l10n.NewTranslator(locale, defaultLocale, translationPath) t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _translationFS, "l10n/locale").Locale(locale)
mt.Subject, err = composeMessage(t.Translate(mt.Subject), vars) mt.Subject, err = composeMessage(t.Get(mt.Subject), vars)
if err != nil { if err != nil {
return mt, err return mt, err
} }
mt.Greeting, err = composeMessage(t.Translate(mt.Greeting), vars) mt.Greeting, err = composeMessage(t.Get(mt.Greeting), vars)
if err != nil { if err != nil {
return mt, err return mt, err
} }
mt.MessageBody, err = composeMessage(t.Translate(mt.MessageBody), vars) mt.MessageBody, err = composeMessage(t.Get(mt.MessageBody), vars)
if err != nil { if err != nil {
return mt, err return mt, err
} }
mt.CallToAction, err = composeMessage(t.Translate(mt.CallToAction), vars) mt.CallToAction, err = composeMessage(t.Get(mt.CallToAction), vars)
if err != nil { if err != nil {
return mt, err return mt, err
} }
@@ -34,20 +41,20 @@ func NewTextTemplate(mt MessageTemplate, locale, defaultLocale string, translati
// NewHTMLTemplate replace the body message template placeholders with the translated template // NewHTMLTemplate replace the body message template placeholders with the translated template
func NewHTMLTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) { func NewHTMLTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) {
var err error var err error
t := l10n.NewTranslator(locale, defaultLocale, translationPath) t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _translationFS, "l10n/locale").Locale(locale)
mt.Subject, err = composeMessage(t.Translate(mt.Subject), vars) mt.Subject, err = composeMessage(t.Get(mt.Subject), vars)
if err != nil { if err != nil {
return mt, err return mt, err
} }
mt.Greeting, err = composeMessage(newlineToBr(t.Translate(mt.Greeting)), vars) mt.Greeting, err = composeMessage(newlineToBr(t.Get(mt.Greeting)), vars)
if err != nil { if err != nil {
return mt, err return mt, err
} }
mt.MessageBody, err = composeMessage(newlineToBr(t.Translate(mt.MessageBody)), vars) mt.MessageBody, err = composeMessage(newlineToBr(t.Get(mt.MessageBody)), vars)
if err != nil { if err != nil {
return mt, err return mt, err
} }
mt.CallToAction, err = composeMessage(callToActionToHTML(t.Translate(mt.CallToAction)), vars) mt.CallToAction, err = composeMessage(callToActionToHTML(t.Get(mt.CallToAction)), vars)
if err != nil { if err != nil {
return mt, err return mt, err
} }
@@ -1,51 +0,0 @@
// Package l10n implements utility for translation the text templates.
//
// The l10n package use transifex translation for text templates.
package l10n
import (
"embed"
"io/fs"
"github.com/leonelquinteros/gotext"
)
var (
//go:embed locale
_translationFS embed.FS
_domain = "notifications"
)
// Translator is the interface to the translation
type Translator interface {
Translate(str string) string
}
type translator struct {
locale *gotext.Locale
}
// NewTranslator Create Translator with library path and language code and load default domain
func NewTranslator(locale, defaultLocale string, path string) Translator {
l := newLocate(locale, path)
if locale != "en" && len(l.GetTranslations()) == 0 {
l = newLocate(defaultLocale, path)
}
return &translator{locale: l}
}
func newLocate(local string, path string) *gotext.Locale {
var l *gotext.Locale
if path == "" {
filesystem, _ := fs.Sub(_translationFS, "locale")
l = gotext.NewLocaleFS(local, filesystem)
} else { // use custom path instead
l = gotext.NewLocale(path, local)
}
l.AddDomain(_domain) // make domain configurable only if needed
return l
}
func (t *translator) Translate(str string) string {
return t.locale.Get(str)
}
+19 -20
View File
@@ -1,7 +1,6 @@
package email package email
// Template marks the string as a translatable template import "github.com/owncloud/ocis/v2/ocis-pkg/l10n"
func Template(s string) string { return s }
// the available templates // the available templates
var ( var (
@@ -10,24 +9,24 @@ var (
textTemplate: "templates/text/email.text.tmpl", textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl", htmlTemplate: "templates/html/email.html.tmpl",
// ShareCreated email template, Subject field (resolves directly) // ShareCreated email template, Subject field (resolves directly)
Subject: Template(`{ShareSharer} shared '{ShareFolder}' with you`), Subject: l10n.Template(`{ShareSharer} shared '{ShareFolder}' with you`),
// ShareCreated email template, resolves via {{ .Greeting }} // ShareCreated email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {ShareGrantee}`), Greeting: l10n.Template(`Hello {ShareGrantee}`),
// ShareCreated email template, resolves via {{ .MessageBody }} // ShareCreated email template, resolves via {{ .MessageBody }}
MessageBody: Template(`{ShareSharer} has shared "{ShareFolder}" with you.`), MessageBody: l10n.Template(`{ShareSharer} has shared "{ShareFolder}" with you.`),
// ShareCreated email template, resolves via {{ .CallToAction }} // ShareCreated email template, resolves via {{ .CallToAction }}
CallToAction: Template(`Click here to view it: {ShareLink}`), CallToAction: l10n.Template(`Click here to view it: {ShareLink}`),
} }
ShareExpired = MessageTemplate{ ShareExpired = MessageTemplate{
textTemplate: "templates/text/email.text.tmpl", textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl", htmlTemplate: "templates/html/email.html.tmpl",
// ShareExpired email template, Subject field (resolves directly) // ShareExpired email template, Subject field (resolves directly)
Subject: Template(`Share to '{ShareFolder}' expired at {ExpiredAt}`), Subject: l10n.Template(`Share to '{ShareFolder}' expired at {ExpiredAt}`),
// ShareExpired email template, resolves via {{ .Greeting }} // ShareExpired email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {ShareGrantee},`), Greeting: l10n.Template(`Hello {ShareGrantee},`),
// ShareExpired email template, resolves via {{ .MessageBody }} // ShareExpired email template, resolves via {{ .MessageBody }}
MessageBody: Template(`Your share to {ShareFolder} has expired at {ExpiredAt} MessageBody: l10n.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.`), Even though this share has been revoked you still might have access through other shares and/or space memberships.`),
} }
@@ -37,39 +36,39 @@ Even though this share has been revoked you still might have access through othe
textTemplate: "templates/text/email.text.tmpl", textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl", htmlTemplate: "templates/html/email.html.tmpl",
// SharedSpace email template, Subject field (resolves directly) // SharedSpace email template, Subject field (resolves directly)
Subject: Template("{SpaceSharer} invited you to join {SpaceName}"), Subject: l10n.Template("{SpaceSharer} invited you to join {SpaceName}"),
// SharedSpace email template, resolves via {{ .Greeting }} // SharedSpace email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {SpaceGrantee},`), Greeting: l10n.Template(`Hello {SpaceGrantee},`),
// SharedSpace email template, resolves via {{ .MessageBody }} // SharedSpace email template, resolves via {{ .MessageBody }}
MessageBody: Template(`{SpaceSharer} has invited you to join "{SpaceName}".`), MessageBody: l10n.Template(`{SpaceSharer} has invited you to join "{SpaceName}".`),
// SharedSpace email template, resolves via {{ .CallToAction }} // SharedSpace email template, resolves via {{ .CallToAction }}
CallToAction: Template(`Click here to view it: {ShareLink}`), CallToAction: l10n.Template(`Click here to view it: {ShareLink}`),
} }
UnsharedSpace = MessageTemplate{ UnsharedSpace = MessageTemplate{
textTemplate: "templates/text/email.text.tmpl", textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl", htmlTemplate: "templates/html/email.html.tmpl",
// UnsharedSpace email template, Subject field (resolves directly) // UnsharedSpace email template, Subject field (resolves directly)
Subject: Template(`{SpaceSharer} removed you from {SpaceName}`), Subject: l10n.Template(`{SpaceSharer} removed you from {SpaceName}`),
// UnsharedSpace email template, resolves via {{ .Greeting }} // UnsharedSpace email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {SpaceGrantee},`), Greeting: l10n.Template(`Hello {SpaceGrantee},`),
// UnsharedSpace email template, resolves via {{ .MessageBody }} // UnsharedSpace email template, resolves via {{ .MessageBody }}
MessageBody: Template(`{SpaceSharer} has removed you from "{SpaceName}". MessageBody: l10n.Template(`{SpaceSharer} has removed you from "{SpaceName}".
You might still have access through your other groups or direct membership.`), You might still have access through your other groups or direct membership.`),
// UnsharedSpace email template, resolves via {{ .CallToAction }} // UnsharedSpace email template, resolves via {{ .CallToAction }}
CallToAction: Template(`Click here to check it: {ShareLink}`), CallToAction: l10n.Template(`Click here to check it: {ShareLink}`),
} }
MembershipExpired = MessageTemplate{ MembershipExpired = MessageTemplate{
textTemplate: "templates/text/email.text.tmpl", textTemplate: "templates/text/email.text.tmpl",
htmlTemplate: "templates/html/email.html.tmpl", htmlTemplate: "templates/html/email.html.tmpl",
// MembershipExpired email template, Subject field (resolves directly) // MembershipExpired email template, Subject field (resolves directly)
Subject: Template(`Membership of '{SpaceName}' expired at {ExpiredAt}`), Subject: l10n.Template(`Membership of '{SpaceName}' expired at {ExpiredAt}`),
// MembershipExpired email template, resolves via {{ .Greeting }} // MembershipExpired email template, resolves via {{ .Greeting }}
Greeting: Template(`Hello {SpaceGrantee},`), Greeting: l10n.Template(`Hello {SpaceGrantee},`),
// MembershipExpired email template, resolves via {{ .MessageBody }} // MembershipExpired email template, resolves via {{ .MessageBody }}
MessageBody: Template(`Your membership of space {SpaceName} has expired at {ExpiredAt} MessageBody: l10n.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`), Even though this membership has expired you still might have access through other shares and/or space memberships`),
} }
+2 -19
View File
@@ -18,6 +18,7 @@ import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
"github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware" "github.com/owncloud/ocis/v2/ocis-pkg/middleware"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
@@ -28,8 +29,6 @@ import (
"google.golang.org/protobuf/types/known/fieldmaskpb" "google.golang.org/protobuf/types/known/fieldmaskpb"
) )
var _defaultLocale = "en"
// Service should be named `Runner` // Service should be named `Runner`
type Service interface { type Service interface {
Run() error Run() error
@@ -108,7 +107,7 @@ func (s eventsNotifier) render(ctx context.Context, template email.MessageTempla
// Render the Email Template for each user // Render the Email Template for each user
messageList := make([]*channels.Message, len(granteeList)) messageList := make([]*channels.Message, len(granteeList))
for i, usr := range granteeList { for i, usr := range granteeList {
locale := s.getUserLang(ctx, usr.GetId()) locale := l10n.MustGetUserLocale(ctx, usr.GetId().GetOpaqueId(), "", s.valueService)
fields[granteeFieldName] = usr.GetDisplayName() fields[granteeFieldName] = usr.GetDisplayName()
rendered, err := email.RenderEmailTemplate(template, locale, s.defaultLanguage, s.emailTemplatePath, s.translationPath, fields) rendered, err := email.RenderEmailTemplate(template, locale, s.defaultLanguage, s.emailTemplatePath, s.translationPath, fields)
@@ -215,22 +214,6 @@ func (s eventsNotifier) getUser(ctx context.Context, u *user.UserId) (*user.User
return r.GetUser(), nil return r.GetUser(), nil
} }
func (s eventsNotifier) getUserLang(ctx context.Context, u *user.UserId) string {
granteeCtx := metadata.Set(ctx, middleware.AccountID, u.GetOpaqueId())
if resp, err := s.valueService.GetValueByUniqueIdentifiers(granteeCtx,
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: u.GetOpaqueId(),
SettingId: defaults.SettingUUIDProfileLanguage,
},
); err == nil {
val := resp.GetValue().GetValue().GetListValue().GetValues()
if len(val) > 0 && val[0] != nil {
return val[0].GetStringValue()
}
}
return _defaultLocale
}
func (s eventsNotifier) disableEmails(ctx context.Context, u *user.UserId) bool { func (s eventsNotifier) disableEmails(ctx context.Context, u *user.UserId) bool {
granteeCtx := metadata.Set(ctx, middleware.AccountID, u.OpaqueId) granteeCtx := metadata.Set(ctx, middleware.AccountID, u.OpaqueId)
if resp, err := s.valueService.GetValueByUniqueIdentifiers(granteeCtx, if resp, err := s.valueService.GetValueByUniqueIdentifiers(granteeCtx,
+1 -1
View File
@@ -44,7 +44,7 @@ l10n-push:
.PHONY: l10n-read .PHONY: l10n-read
l10n-read: $(GO_XGETTEXT) l10n-read: $(GO_XGETTEXT)
go-xgettext -o $(OUTPUT_DIR)/userlog.pot --keyword=Template -s pkg/service/templates.go go-xgettext -o $(OUTPUT_DIR)/userlog.pot --keyword=l10n.Template -s pkg/service/templates.go
.PHONY: l10n-write .PHONY: l10n-write
l10n-write: l10n-write:
+4 -21
View File
@@ -6,7 +6,6 @@ import (
"embed" "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"strings" "strings"
"text/template" "text/template"
"time" "time"
@@ -18,7 +17,7 @@ import (
"github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils" "github.com/cs3org/reva/v2/pkg/utils"
"github.com/leonelquinteros/gotext" "github.com/owncloud/ocis/v2/ocis-pkg/l10n"
) )
//go:embed l10n/locale //go:embed l10n/locale
@@ -338,7 +337,7 @@ func (c *Converter) getResource(ctx context.Context, resourceID *storageprovider
return resource, err return resource, err
} }
func (c *Converter) getUser(ctx context.Context, userID *user.UserId) (*user.User, error) { func (c *Converter) getUser(_ context.Context, userID *user.UserId) (*user.User, error) {
if u, ok := c.users[userID.GetOpaqueId()]; ok { if u, ok := c.users[userID.GetOpaqueId()]; ok {
return u, nil return u, nil
} }
@@ -361,25 +360,9 @@ func composeMessage(nt NotificationTemplate, locale, defaultLocale, path string,
return subject, subjectraw, message, messageraw, err return subject, subjectraw, message, messageraw, err
} }
func newLocate(locale string, path string) *gotext.Locale {
// 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
}
func loadTemplates(nt NotificationTemplate, locale, defaultLocale, path string) (string, string) { func loadTemplates(nt NotificationTemplate, locale, defaultLocale, path string) (string, string) {
l := newLocate(locale, path) t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, path, _translationFS, "l10n/locale").Locale(locale)
if locale != "en" && len(l.GetTranslations()) == 0 { return t.Get(nt.Subject), t.Get(nt.Message)
l = newLocate(defaultLocale, path)
}
return l.Get(nt.Subject), l.Get(nt.Message)
} }
func executeTemplate(raw string, vars map[string]interface{}) (string, error) { func executeTemplate(raw string, vars map[string]interface{}) (string, error) {
+2 -23
View File
@@ -11,20 +11,18 @@ import (
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
micrometadata "go-micro.dev/v4/metadata"
"go-micro.dev/v4/store" "go-micro.dev/v4/store"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils" "github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
"github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/roles" "github.com/owncloud/ocis/v2/ocis-pkg/roles"
ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0"
ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
"github.com/owncloud/ocis/v2/services/userlog/pkg/config" "github.com/owncloud/ocis/v2/services/userlog/pkg/config"
) )
@@ -343,7 +341,7 @@ func (ul *UserlogService) sendSSE(ctx context.Context, userIDs []string, event e
m := make(map[string]events.SendSSE) m := make(map[string]events.SendSSE)
for _, userid := range userIDs { for _, userid := range userIDs {
loc := ul.getUserLocale(userid) loc := l10n.MustGetUserLocale(ctx, userid, "", ul.valueClient)
if ev, ok := m[loc]; ok { if ev, ok := m[loc]; ok {
ev.UserIDs = append(m[loc].UserIDs, userid) ev.UserIDs = append(m[loc].UserIDs, userid)
m[loc] = ev m[loc] = ev
@@ -455,25 +453,6 @@ func (ul *UserlogService) alterGlobalEvents(ctx context.Context, alter func(map[
}) })
} }
func (ul *UserlogService) getUserLocale(userid string) string {
resp, err := ul.valueClient.GetValueByUniqueIdentifiers(
micrometadata.Set(context.Background(), middleware.AccountID, userid),
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: userid,
SettingId: defaults.SettingUUIDProfileLanguage,
},
)
if err != nil {
ul.log.Error().Err(err).Str("userid", userid).Msg("cannot get users locale")
return ""
}
val := resp.GetValue().GetValue().GetListValue().GetValues()
if len(val) == 0 {
return ""
}
return val[0].GetStringValue()
}
func removeExecutant(users []string, executant *user.UserId) []string { func removeExecutant(users []string, executant *user.UserId) []string {
var usrs []string var usrs []string
for _, u := range users { for _, u := range users {
+23 -24
View File
@@ -1,63 +1,62 @@
package service package service
// Template marks the string as a translatable template import "github.com/owncloud/ocis/v2/ocis-pkg/l10n"
func Template(s string) string { return s }
// the available templates // the available templates
var ( var (
VirusFound = NotificationTemplate{ VirusFound = NotificationTemplate{
Subject: Template("Virus found"), Subject: l10n.Template("Virus found"),
Message: Template("Virus found in {resource}. Upload not possible. Virus: {virus}"), Message: l10n.Template("Virus found in {resource}. Upload not possible. Virus: {virus}"),
} }
PoliciesEnforced = NotificationTemplate{ PoliciesEnforced = NotificationTemplate{
Subject: Template("Policies enforced"), Subject: l10n.Template("Policies enforced"),
Message: Template("File {resource} was deleted because it violates the policies"), Message: l10n.Template("File {resource} was deleted because it violates the policies"),
} }
SpaceShared = NotificationTemplate{ SpaceShared = NotificationTemplate{
Subject: Template("Space shared"), Subject: l10n.Template("Space shared"),
Message: Template("{user} added you to Space {space}"), Message: l10n.Template("{user} added you to Space {space}"),
} }
SpaceUnshared = NotificationTemplate{ SpaceUnshared = NotificationTemplate{
Subject: Template("Removed from Space"), Subject: l10n.Template("Removed from Space"),
Message: Template("{user} removed you from Space {space}"), Message: l10n.Template("{user} removed you from Space {space}"),
} }
SpaceDisabled = NotificationTemplate{ SpaceDisabled = NotificationTemplate{
Subject: Template("Space disabled"), Subject: l10n.Template("Space disabled"),
Message: Template("{user} disabled Space {space}"), Message: l10n.Template("{user} disabled Space {space}"),
} }
SpaceDeleted = NotificationTemplate{ SpaceDeleted = NotificationTemplate{
Subject: Template("Space deleted"), Subject: l10n.Template("Space deleted"),
Message: Template("{user} deleted Space {space}"), Message: l10n.Template("{user} deleted Space {space}"),
} }
SpaceMembershipExpired = NotificationTemplate{ SpaceMembershipExpired = NotificationTemplate{
Subject: Template("Membership expired"), Subject: l10n.Template("Membership expired"),
Message: Template("Access to Space {space} lost"), Message: l10n.Template("Access to Space {space} lost"),
} }
ShareCreated = NotificationTemplate{ ShareCreated = NotificationTemplate{
Subject: Template("Resource shared"), Subject: l10n.Template("Resource shared"),
Message: Template("{user} shared {resource} with you"), Message: l10n.Template("{user} shared {resource} with you"),
} }
ShareRemoved = NotificationTemplate{ ShareRemoved = NotificationTemplate{
Subject: Template("Resource unshared"), Subject: l10n.Template("Resource unshared"),
Message: Template("{user} unshared {resource} with you"), Message: l10n.Template("{user} unshared {resource} with you"),
} }
ShareExpired = NotificationTemplate{ ShareExpired = NotificationTemplate{
Subject: Template("Share expired"), Subject: l10n.Template("Share expired"),
Message: Template("Access to {resource} expired"), Message: l10n.Template("Access to {resource} expired"),
} }
PlatformDeprovision = NotificationTemplate{ PlatformDeprovision = NotificationTemplate{
Subject: Template("Instance will be shut down and deprovisioned"), Subject: l10n.Template("Instance will be shut down and deprovisioned"),
Message: Template("Attention! The instance will be shut down and deprovisioned on {date}. Download all your data before that date as no access past that date is possible."), Message: l10n.Template("Attention! The instance will be shut down and deprovisioned on {date}. Download all your data before that date as no access past that date is possible."),
} }
) )