feat: Notification translation (#192)

* feat: translate emails

* feat: translate emails

* fix: add notification statik to build

* fix: add codes to templates
This commit is contained in:
Fabi 2020-06-09 15:11:42 +02:00 committed by GitHub
parent e87fca28e7
commit 17f0eea4a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 186 additions and 57 deletions

View File

@ -103,6 +103,8 @@ jobs:
- run: cat pkg/console/statik/statik.go
- run: ./build/login/generate-static.sh
- run: cat internal/login/statik/statik.go
- run: ./build/notification/generate-static.sh
- run: cat internal/notification/statik/statik.go
- run: CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o zitadel-${{ matrix.goos }}-${{ matrix.goarch }} cmd/zitadel/main.go
- uses: actions/upload-artifact@v1
with:

View File

@ -0,0 +1,5 @@
#! /bin/sh
set -eux
go generate internal/notification/statik/generate.go

View File

@ -196,6 +196,7 @@ Console:
Notification:
Repository:
DefaultLanguage: 'de'
Eventstore:
ServiceName: 'Notification'
Repository:

View File

@ -133,30 +133,30 @@ SystemDefaults:
From: $TWILIO_SENDER_NAME
TemplateData:
InitCode:
Title: 'Zitadel - User initialisieren'
PreHeader: 'User initialisieren'
Subject: 'User initialisieren'
Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
Text: 'Dieser Benutzer wurde soeben im Zitadel erstellt. Du kannst den Button unten verwenden, um die Initialisierung abzuschliesen. Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren.'
ButtonText: 'Initialisierung abschliessen'
Title: 'InitCode.Title'
PreHeader: 'InitCode.PreHeader'
Subject: 'InitCode.Subject'
Greeting: 'InitCode.Greeting'
Text: 'InitCode.Text'
ButtonText: 'InitCode.ButtonText'
PasswordReset:
Title: 'Zitadel - Passwort zurücksetzen'
PreHeader: 'Passwort zurücksetzen'
Subject: 'Passwort zurücksetzen'
Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
Text: 'Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen. Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren.'
ButtonText: 'Passwort zurücksetzen'
Title: 'PasswordReset.Title'
PreHeader: 'PasswordReset.PreHeader'
Subject: 'PasswordReset.Subject'
Greeting: 'PasswordReset.Greeting'
Text: 'PasswordReset.Text'
ButtonText: 'PasswordReset.ButtonText'
VerifyEmail:
Title: 'Zitadel - Email verifizieren'
PreHeader: 'Email verifizieren'
Subject: 'Email verifizieren'
Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
Text: 'Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren. Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren.'
ButtonText: 'Email verifizieren'
Title: 'VerifyEmail.Title'
PreHeader: 'VerifyEmail.PreHeader'
Subject: 'VerifyEmail.Subject'
Greeting: 'VerifyEmail.Greeting'
Text: 'VerifyEmail.Text'
ButtonText: 'VerifyEmail.ButtonText'
VerifyPhone:
Title: 'Zitadel - Telefonnummer verifizieren'
PreHeader: 'Telefonnummer verifizieren'
Subject: 'Telefonnummer verifizieren'
Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
Text: 'Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst: {{.Code}}'
ButtonText: 'Telefon verifizieren'
Title: 'VerifyPhone.Title'
PreHeader: 'VerifyPhone.PreHeader'
Subject: 'VerifyPhone.Subject'
Greeting: 'VerifyPhone.Greeting'
Text: 'VerifyPhone.Text'
ButtonText: 'VerifyPhone.ButtonText'

View File

@ -89,8 +89,8 @@ func (t *Translator) LocalizeFromRequest(r *http.Request, id string, args map[st
return s
}
func (t *Translator) Localize(id string, args map[string]interface{}) string {
s, _ := t.localizer().Localize(&i18n.LocalizeConfig{
func (t *Translator) Localize(id string, args map[string]interface{}, langs ...string) string {
s, _ := t.localizer(langs...).Localize(&i18n.LocalizeConfig{
MessageID: id,
TemplateData: args,
})

View File

@ -5,6 +5,9 @@ import (
"github.com/caos/logging"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/notification/repository/eventsourcing"
"github.com/rakyll/statik/fs"
_ "github.com/caos/zitadel/internal/notification/statik"
)
type Config struct {
@ -12,6 +15,9 @@ type Config struct {
}
func Start(ctx context.Context, config Config, systemDefaults sd.SystemDefaults) {
_, err := eventsourcing.Start(config.Repository, systemDefaults)
statikFS, err := fs.NewWithNamespace("notification")
logging.Log("CONFI-7usEW").OnError(err).Panic("unable to start listener")
_, err = eventsourcing.Start(config.Repository, statikFS, systemDefaults)
logging.Log("MAIN-9uBxp").OnError(err).Panic("unable to start app")
}

View File

@ -7,6 +7,7 @@ import (
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/spooler"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
"time"
@ -29,14 +30,21 @@ type EventstoreRepos struct {
UserEvents *usr_event.UserEventstore
}
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, eventstore eventstore.Eventstore, repos EventstoreRepos, systemDefaults sd.SystemDefaults) []spooler.Handler {
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, eventstore eventstore.Eventstore, repos EventstoreRepos, systemDefaults sd.SystemDefaults, i18n *i18n.Translator) []spooler.Handler {
aesCrypto, err := crypto.NewAESCrypto(systemDefaults.UserVerificationKey)
if err != nil {
logging.Log("HANDL-s90ew").WithError(err).Debug("error create new aes crypto")
}
return []spooler.Handler{
&NotifyUser{handler: handler{view, bulkLimit, configs.cycleDuration("User"), errorCount}},
&Notification{handler: handler{view, bulkLimit, configs.cycleDuration("Notification"), errorCount}, eventstore: eventstore, userEvents: repos.UserEvents, systemDefaults: systemDefaults, AesCrypto: aesCrypto},
&Notification{
handler: handler{view, bulkLimit, configs.cycleDuration("Notification"), errorCount},
eventstore: eventstore,
userEvents: repos.UserEvents,
systemDefaults: systemDefaults,
AesCrypto: aesCrypto,
i18n: i18n,
},
}
}

View File

@ -6,6 +6,7 @@ import (
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/types"
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
@ -24,6 +25,7 @@ type Notification struct {
userEvents *usr_event.UserEventstore
systemDefaults sd.SystemDefaults
AesCrypto crypto.EncryptionAlgorithm
i18n *i18n.Translator
}
const (
@ -75,7 +77,7 @@ func (n *Notification) handleInitUserCode(event *models.Event) (err error) {
if err != nil {
return err
}
err = types.SendUserInitCode(user, initCode, n.systemDefaults, n.AesCrypto)
err = types.SendUserInitCode(n.i18n, user, initCode, n.systemDefaults, n.AesCrypto)
if err != nil {
return err
}
@ -93,7 +95,7 @@ func (n *Notification) handlePasswordCode(event *models.Event) (err error) {
if err != nil {
return err
}
err = types.SendPasswordCode(user, pwCode, n.systemDefaults, n.AesCrypto)
err = types.SendPasswordCode(n.i18n, user, pwCode, n.systemDefaults, n.AesCrypto)
if err != nil {
return err
}
@ -111,7 +113,7 @@ func (n *Notification) handleEmailVerificationCode(event *models.Event) (err err
if err != nil {
return err
}
err = types.SendEmailVerificationCode(user, emailCode, n.systemDefaults, n.AesCrypto)
err = types.SendEmailVerificationCode(n.i18n, user, emailCode, n.systemDefaults, n.AesCrypto)
if err != nil {
return err
}
@ -129,7 +131,7 @@ func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err err
if err != nil {
return err
}
err = types.SendPhoneVerificationCode(user, phoneCode, n.systemDefaults, n.AesCrypto)
err = types.SendPhoneVerificationCode(n.i18n, user, phoneCode, n.systemDefaults, n.AesCrypto)
if err != nil {
return err
}

View File

@ -5,13 +5,17 @@ import (
"github.com/caos/zitadel/internal/config/types"
es_int "github.com/caos/zitadel/internal/eventstore"
es_spol "github.com/caos/zitadel/internal/eventstore/spooler"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/handler"
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/spooler"
noti_view "github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
es_usr "github.com/caos/zitadel/internal/user/repository/eventsourcing"
"golang.org/x/text/language"
"net/http"
)
type Config struct {
DefaultLanguage language.Tag
Eventstore es_int.Config
View types.SQL
Spooler spooler.SpoolerConfig
@ -21,7 +25,7 @@ type EsRepository struct {
spooler *es_spol.Spooler
}
func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) {
func Start(conf Config, dir http.FileSystem, systemDefaults sd.SystemDefaults) (*EsRepository, error) {
es, err := es_int.Start(conf.Eventstore)
if err != nil {
return nil, err
@ -43,8 +47,12 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
if err != nil {
return nil, err
}
i18n, err := i18n.NewTranslator(dir, i18n.TranslatorConfig{DefaultLanguage: conf.DefaultLanguage})
if err != nil {
return nil, err
}
eventstoreRepos := handler.EventstoreRepos{UserEvents: user}
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, eventstoreRepos, systemDefaults)
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, eventstoreRepos, systemDefaults, i18n)
return &EsRepository{
spool,

View File

@ -5,6 +5,7 @@ import (
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/spooler"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/handler"
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
@ -21,12 +22,12 @@ type EventstoreRepos struct {
UserEvents *usr_event.UserEventstore
}
func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, sql *sql.DB, eventstoreRepos handler.EventstoreRepos, systemDefaults sd.SystemDefaults) *spooler.Spooler {
func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, sql *sql.DB, eventstoreRepos handler.EventstoreRepos, systemDefaults sd.SystemDefaults, i18n *i18n.Translator) *spooler.Spooler {
spoolerConfig := spooler.Config{
Eventstore: es,
Locker: &locker{dbClient: sql},
ConcurrentTasks: c.ConcurrentTasks,
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, eventstoreRepos, systemDefaults),
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, eventstoreRepos, systemDefaults, i18n),
}
spool := spoolerConfig.New()
spool.Start()

View File

@ -0,0 +1,29 @@
InitCode:
Title: Zitadel - User initialisieren
PreHeader: User initialisieren
Subject: User initialisieren
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Dieser Benutzer wurde soeben im Zitadel erstellt. Du kannst den Button unten verwenden, um die Initialisierung abzuschliesen. (Code {{.Code}}) Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren.
ButtonText: Initialisierung abschliessen
PasswordReset:
Title: Zitadel - Passwort zurücksetzen
PreHeader: Passwort zurücksetzen
Subject: Passwort zurücksetzen
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen. (Code {{.Code}}) Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren.
ButtonText: Passwort zurücksetzen
VerifyEmail:
Title: Zitadel - Email verifizieren
PreHeader: Email verifizieren
Subject: Email verifizieren
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren. (Code {{.Code}}) Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren.
ButtonText: Email verifizieren
VerifyPhone:
Title: Zitadel - Telefonnummer verifizieren
PreHeader: Telefonnummer verifizieren
Subject: Telefonnummer verifizieren
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst {{.Code}}
ButtonText: Telefon verifizieren

View File

@ -0,0 +1,29 @@
InitCode:
Title: Zitadel - Initialize User
PreHeader: Initialize User
Subject: Initialize User
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: This user was created in Zitadel. Please click the button below to finish the initialization process. (Code {{.Code}}) If you didn't ask for this mail, please ignore it.
ButtonText: Finish initialization
PasswordReset:
Title: Zitadel - Reset password
PreHeader: Reset password
Subject: Reset password
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: We received a password reset request. Please use the button below to reset your password. (Code {{.Code}}) If you didn't ask for this mail, please ignore it.
ButtonText: Reset password
VerifyEmail:
Title: Zitadel - Verify email
PreHeader: Verify email
Subject: Verify email
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you din't add a new email, please ignore this email.
ButtonText: Verify email
VerifyPhone:
Title: Zitadel - Verify phone
PreHeader: Verify phone
Subject: Verify phone
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: A new phonenumber has been added. Please use the following code to verify it {{.Code}}
ButtonText: Verify phone

View File

@ -0,0 +1,3 @@
package statik
//go:generate statik -src=../static -dest=.. -ns=notification

View File

@ -1,5 +1,9 @@
package templates
import (
"github.com/caos/zitadel/internal/i18n"
)
type TemplateData struct {
Title string
PreHeader string
@ -9,3 +13,13 @@ type TemplateData struct {
Href string
ButtonText string
}
func (data *TemplateData) Translate(i18n *i18n.Translator, args map[string]interface{}, langs ...string) {
data.Title = i18n.Localize(data.Title, nil, langs...)
data.PreHeader = i18n.Localize(data.PreHeader, nil, langs...)
data.Subject = i18n.Localize(data.Subject, nil, langs...)
data.Greeting = i18n.Localize(data.Greeting, args, langs...)
data.Text = i18n.Localize(data.Text, args, langs...)
data.Href = i18n.Localize(data.Href, nil, langs...)
data.ButtonText = i18n.Localize(data.ButtonText, nil, langs...)
}

View File

@ -3,6 +3,7 @@ package types
import (
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
@ -10,12 +11,10 @@ import (
type EmailVerificationCodeData struct {
templates.TemplateData
FirstName string
LastName string
URL string
}
func SendEmailVerificationCode(user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
func SendEmailVerificationCode(i18n *i18n.Translator, user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@ -24,7 +23,13 @@ func SendEmailVerificationCode(user *view_model.NotifyUser, code *es_model.Email
if err != nil {
return err
}
emailCodeData := &EmailVerificationCodeData{TemplateData: systemDefaults.Notifications.TemplateData.VerifyEmail, FirstName: user.FirstName, LastName: user.LastName, URL: url}
var args = map[string]interface{}{
"FirstName": user.FirstName,
"LastName": user.LastName,
"Code": codeString,
}
systemDefaults.Notifications.TemplateData.VerifyEmail.Translate(i18n, args, user.PreferredLanguage)
emailCodeData := &EmailVerificationCodeData{TemplateData: systemDefaults.Notifications.TemplateData.VerifyEmail, URL: url}
template, err := templates.GetParsedTemplate(emailCodeData)
if err != nil {

View File

@ -3,6 +3,7 @@ package types
import (
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
@ -10,8 +11,6 @@ import (
type InitCodeEmailData struct {
templates.TemplateData
FirstName string
LastName string
URL string
}
@ -20,7 +19,7 @@ type UrlData struct {
Code string
}
func SendUserInitCode(user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
func SendUserInitCode(i18n *i18n.Translator, user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@ -29,7 +28,13 @@ func SendUserInitCode(user *view_model.NotifyUser, code *es_model.InitUserCode,
if err != nil {
return err
}
initCodeData := &InitCodeEmailData{TemplateData: systemDefaults.Notifications.TemplateData.InitCode, FirstName: user.FirstName, LastName: user.LastName, URL: url}
var args = map[string]interface{}{
"FirstName": user.FirstName,
"LastName": user.LastName,
"Code": codeString,
}
systemDefaults.Notifications.TemplateData.InitCode.Translate(i18n, args, user.PreferredLanguage)
initCodeData := &InitCodeEmailData{TemplateData: systemDefaults.Notifications.TemplateData.InitCode, URL: url}
template, err := templates.GetParsedTemplate(initCodeData)
if err != nil {

View File

@ -3,6 +3,7 @@ package types
import (
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
@ -15,7 +16,7 @@ type PasswordCodeData struct {
URL string
}
func SendPasswordCode(user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
func SendPasswordCode(i18n *i18n.Translator, user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@ -24,6 +25,12 @@ func SendPasswordCode(user *view_model.NotifyUser, code *es_model.PasswordCode,
if err != nil {
return err
}
var args = map[string]interface{}{
"FirstName": user.FirstName,
"LastName": user.LastName,
"Code": codeString,
}
systemDefaults.Notifications.TemplateData.PasswordReset.Translate(i18n, args, user.PreferredLanguage)
passwordCodeData := &PasswordCodeData{TemplateData: systemDefaults.Notifications.TemplateData.PasswordReset, FirstName: user.FirstName, LastName: user.LastName, URL: url}
template, err := templates.GetParsedTemplate(passwordCodeData)

View File

@ -3,24 +3,28 @@ package types
import (
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
)
type PhoneVerificationCodeData struct {
FirstName string
LastName string
Code string
UserID string
}
func SendPhoneVerificationCode(user *view_model.NotifyUser, code *es_model.PhoneCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
func SendPhoneVerificationCode(i18n *i18n.Translator, user *view_model.NotifyUser, code *es_model.PhoneCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
}
codeData := &PhoneVerificationCodeData{FirstName: user.FirstName, LastName: user.LastName, UserID: user.ID, Code: codeString}
var args = map[string]interface{}{
"FirstName": user.FirstName,
"LastName": user.LastName,
"Code": codeString,
}
systemDefaults.Notifications.TemplateData.VerifyPhone.Translate(i18n, args, user.PreferredLanguage)
codeData := &PhoneVerificationCodeData{UserID: user.ID}
template, err := templates.ParseTemplateText(systemDefaults.Notifications.TemplateData.VerifyPhone.Text, codeData)
if err != nil {
return err