From 68843a626dc01fa366c4e999db884284a0567742 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sat, 4 Jul 2020 12:54:35 +0200 Subject: [PATCH] Backend: Add translations for API messages Signed-off-by: Michael Mayer --- internal/api/account.go | 53 +++++++------- internal/api/account_test.go | 19 ++--- internal/api/album.go | 100 ++++++++++++------------- internal/api/album_test.go | 7 +- internal/api/api.go | 34 +++++++++ internal/api/batch.go | 26 +++---- internal/api/config.go | 2 +- internal/api/errors.go | 2 +- internal/api/event.go | 8 +- internal/api/file.go | 4 +- internal/api/folder.go | 5 +- internal/api/geo.go | 4 +- internal/api/import.go | 6 +- internal/api/index.go | 6 +- internal/api/label.go | 12 +-- internal/api/link.go | 12 +-- internal/api/moments_time.go | 2 +- internal/api/photo.go | 70 +++++++++--------- internal/api/photo_label.go | 22 +++--- internal/api/photo_label_test.go | 7 +- internal/api/photo_search.go | 15 ++-- internal/api/photo_test.go | 5 +- internal/api/session.go | 3 +- internal/api/settings.go | 12 +-- internal/api/upload.go | 8 +- internal/api/user.go | 2 +- internal/api/zip.go | 4 +- internal/config/settings.go | 5 +- internal/config/test.go | 3 + internal/event/hub.go | 17 +++++ internal/i18n/i18n.go | 97 +++++++++++++++++++++++++ internal/i18n/i18n_test.go | 121 +++++++++++++++++++++++++++++++ internal/i18n/lang-de.go | 33 +++++++++ internal/i18n/lang-en.go | 67 +++++++++++++++++ internal/i18n/lang-fr.go | 32 ++++++++ internal/i18n/lang-nl.go | 32 ++++++++ internal/i18n/lang-ru.go | 32 ++++++++ internal/i18n/lang.go | 35 +++++++++ internal/i18n/lang_test.go | 18 +++++ internal/i18n/response.go | 33 +++++++++ internal/i18n/response_test.go | 38 ++++++++++ 41 files changed, 802 insertions(+), 211 deletions(-) create mode 100644 internal/i18n/i18n.go create mode 100644 internal/i18n/i18n_test.go create mode 100644 internal/i18n/lang-de.go create mode 100644 internal/i18n/lang-en.go create mode 100644 internal/i18n/lang-fr.go create mode 100644 internal/i18n/lang-nl.go create mode 100644 internal/i18n/lang-ru.go create mode 100644 internal/i18n/lang.go create mode 100644 internal/i18n/lang_test.go create mode 100644 internal/i18n/response.go create mode 100644 internal/i18n/response_test.go diff --git a/internal/api/account.go b/internal/api/account.go index 88e31d0a4..370eeb924 100644 --- a/internal/api/account.go +++ b/internal/api/account.go @@ -13,6 +13,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/internal/workers" @@ -26,7 +27,7 @@ func GetAccounts(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionSearch) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -35,14 +36,14 @@ func GetAccounts(router *gin.RouterGroup) { err := c.MustBindWith(&f, binding.Form) if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } result, err := query.AccountSearch(f) if err != nil { - c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -63,7 +64,7 @@ func GetAccount(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionRead) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -72,7 +73,7 @@ func GetAccount(router *gin.RouterGroup) { if m, err := query.AccountByID(id); err == nil { c.JSON(http.StatusOK, m) } else { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAccountNotFound) } }) } @@ -86,7 +87,7 @@ func GetAccountFolders(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionRead) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -110,7 +111,7 @@ func GetAccountFolders(router *gin.RouterGroup) { m, err := query.AccountByID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAccountNotFound) return } @@ -118,7 +119,7 @@ func GetAccountFolders(router *gin.RouterGroup) { if err != nil { log.Errorf("account-folders: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusNotFound, ErrConnectionFailed) + Abort(c, http.StatusBadRequest, i18n.ErrConnectionFailed) return } @@ -140,7 +141,7 @@ func ShareWithAccount(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionUpload) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -149,14 +150,14 @@ func ShareWithAccount(router *gin.RouterGroup) { m, err := query.AccountByID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAccountNotFound) return } var f form.AccountShare if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -164,7 +165,7 @@ func ShareWithAccount(router *gin.RouterGroup) { files, err := query.FilesByUID(f.Photos, 1000, 0) if err != nil { - c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) + AbortEntityNotFound(c) return } @@ -187,20 +188,20 @@ func CreateAccount(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionCreate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } var f form.Account if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } if err := f.ServiceDiscovery(); err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + Abort(c, http.StatusBadRequest, i18n.ErrConnectionFailed) return } @@ -210,11 +211,11 @@ func CreateAccount(router *gin.RouterGroup) { if err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } - event.Success("account created") + event.SuccessMsg(i18n.MsgAccountCreated) c.JSON(http.StatusOK, m) }) @@ -229,7 +230,7 @@ func UpdateAccount(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -238,7 +239,7 @@ func UpdateAccount(router *gin.RouterGroup) { m, err := query.AccountByID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAccountNotFound) return } @@ -247,30 +248,30 @@ func UpdateAccount(router *gin.RouterGroup) { if err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } // 2) Update form with values from request if err := c.BindJSON(&f); err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusBadRequest, ErrFormInvalid) + AbortBadRequest(c) return } // 3) Save model with values from form if err := m.SaveForm(f); err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } - event.Success("account saved") + event.SuccessMsg(i18n.MsgAccountSaved) m, err = query.AccountByID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound) + AbortEntityNotFound(c) return } @@ -287,7 +288,7 @@ func DeleteAccount(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionDelete) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -305,7 +306,7 @@ func DeleteAccount(router *gin.RouterGroup) { return } - event.Success("account deleted") + event.SuccessMsg(i18n.MsgAccountDeleted) c.JSON(http.StatusOK, m) }) diff --git a/internal/api/account_test.go b/internal/api/account_test.go index c0d4eff70..3ada6294a 100644 --- a/internal/api/account_test.go +++ b/internal/api/account_test.go @@ -1,6 +1,7 @@ package api import ( + "github.com/photoprism/photoprism/internal/i18n" "github.com/tidwall/gjson" "net/http" "testing" @@ -41,7 +42,7 @@ func TestGetAccount(t *testing.T) { GetAccount(router) r := PerformRequest(app, "GET", "/api/v1/accounts/999000") val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Account not found", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String()) assert.Equal(t, http.StatusNotFound, r.Code) }) } @@ -62,7 +63,7 @@ func TestGetAccountFolders(t *testing.T) { GetAccountFolders(router) r := PerformRequest(app, "GET", "/api/v1/accounts/999000/folders") val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Account not found", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String()) assert.Equal(t, http.StatusNotFound, r.Code) }) } @@ -73,7 +74,7 @@ func TestShareWithAccount(t *testing.T) { ShareWithAccount(router) r := PerformRequest(app, "POST", "/api/v1/accounts/1000000/share") val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Invalid request", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrBadRequest), val.String()) assert.Equal(t, http.StatusBadRequest, r.Code) }) t.Run("account not found", func(t *testing.T) { @@ -81,7 +82,7 @@ func TestShareWithAccount(t *testing.T) { ShareWithAccount(router) r := PerformRequest(app, "POST", "/api/v1/accounts/999000/share") val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Account not found", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String()) assert.Equal(t, http.StatusNotFound, r.Code) }) } @@ -92,7 +93,7 @@ func TestCreateAccount(t *testing.T) { CreateAccount(router) r := PerformRequest(app, "POST", "/api/v1/accounts") val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Invalid request", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrBadRequest), val.String()) assert.Equal(t, http.StatusBadRequest, r.Code) }) t.Run("could not connect", func(t *testing.T) { @@ -102,7 +103,7 @@ func TestCreateAccount(t *testing.T) { "AccKey": "123", "AccUser": "testuser", "AccPass": "testpasswd", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0, "SyncPath": "", "SyncInterval": 3, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`) val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Could not connect", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrConnectionFailed), val.String()) assert.Equal(t, http.StatusBadRequest, r.Code) }) t.Run("successful request", func(t *testing.T) { @@ -150,7 +151,7 @@ func TestUpdateAccount(t *testing.T) { UpdateAccount(router) r := PerformRequestWithBody(app, "PUT", "/api/v1/accounts/xxx", `{"AccName": "CreateTestUpdated", "AccOwner": "TestUpdated123", "SyncInterval": 9}`) val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Photo not found", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String()) assert.Equal(t, http.StatusNotFound, r.Code) }) @@ -159,7 +160,7 @@ func TestUpdateAccount(t *testing.T) { UpdateAccount(router) r := PerformRequestWithBody(app, "PUT", "/api/v1/accounts/"+id, `{"AccName": 6, "AccOwner": "TestUpdated123", "SyncInterval": 9, "AccUrl": "https:xxx.com"}`) val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Changes could not be saved", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrBadRequest), val.String()) assert.Equal(t, http.StatusBadRequest, r.Code) }) } @@ -183,7 +184,7 @@ func TestDeleteAccount(t *testing.T) { GetAccount(router) r2 := PerformRequest(app, "GET", "/api/v1/accounts/"+id) val2 := gjson.Get(r2.Body.String(), "error") - assert.Equal(t, "Account not found", val2.String()) + assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val2.String()) assert.Equal(t, http.StatusNotFound, r2.Code) }) diff --git a/internal/api/album.go b/internal/api/album.go index 581757fb3..83da31a09 100644 --- a/internal/api/album.go +++ b/internal/api/album.go @@ -16,6 +16,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/service" @@ -34,7 +35,7 @@ func GetAlbums(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionSearch) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -43,7 +44,7 @@ func GetAlbums(router *gin.RouterGroup) { err := c.MustBindWith(&f, binding.Form) if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -73,7 +74,7 @@ func GetAlbum(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionRead) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -81,7 +82,7 @@ func GetAlbum(router *gin.RouterGroup) { m, err := query.AlbumByUID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound) return } @@ -95,14 +96,14 @@ func CreateAlbum(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionCreate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } var f form.Album if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -112,12 +113,11 @@ func CreateAlbum(router *gin.RouterGroup) { log.Debugf("album: creating %+v %+v", f, m) if res := entity.Db().Create(m); res.Error != nil { - log.Error(res.Error.Error()) - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("%s already exists", txt.Quote(m.AlbumTitle))}) + AbortAlreadyExists(c, txt.Quote(m.AlbumTitle)) return } - event.Success("album created") + event.SuccessMsg(i18n.MsgAlbumCreated) UpdateClientConfig() @@ -133,7 +133,7 @@ func UpdateAlbum(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -141,7 +141,7 @@ func UpdateAlbum(router *gin.RouterGroup) { m, err := query.AlbumByUID(uid) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound) return } @@ -149,25 +149,25 @@ func UpdateAlbum(router *gin.RouterGroup) { if err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } if err := c.BindJSON(&f); err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusBadRequest, ErrFormInvalid) + AbortBadRequest(c) return } if err := m.SaveForm(f); err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } UpdateClientConfig() - event.Success("album saved") + event.SuccessMsg(i18n.MsgAlbumSaved) PublishAlbumEvent(EntityUpdated, uid, c) @@ -181,7 +181,7 @@ func DeleteAlbum(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionDelete) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -191,7 +191,7 @@ func DeleteAlbum(router *gin.RouterGroup) { m, err := query.AlbumByUID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound) return } @@ -200,7 +200,8 @@ func DeleteAlbum(router *gin.RouterGroup) { conf.Db().Delete(&m) UpdateClientConfig() - event.Success(fmt.Sprintf("album %s deleted", txt.Quote(m.AlbumTitle))) + + event.SuccessMsg(i18n.MsgAlbumDeleted, txt.Quote(m.AlbumTitle)) c.JSON(http.StatusOK, m) }) @@ -215,7 +216,7 @@ func LikeAlbum(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionLike) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -224,7 +225,7 @@ func LikeAlbum(router *gin.RouterGroup) { album, err := query.AlbumByUID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound) return } @@ -234,7 +235,7 @@ func LikeAlbum(router *gin.RouterGroup) { UpdateClientConfig() PublishAlbumEvent(EntityUpdated, id, c) - c.JSON(http.StatusOK, http.Response{}) + c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgChangesSaved)) }) } @@ -247,7 +248,7 @@ func DislikeAlbum(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionLike) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -256,7 +257,7 @@ func DislikeAlbum(router *gin.RouterGroup) { album, err := query.AlbumByUID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound) return } @@ -266,7 +267,7 @@ func DislikeAlbum(router *gin.RouterGroup) { UpdateClientConfig() PublishAlbumEvent(EntityUpdated, id, c) - c.JSON(http.StatusOK, http.Response{}) + c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgChangesSaved)) }) } @@ -276,21 +277,21 @@ func CloneAlbums(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } a, err := query.AlbumByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound) return } var f form.Selection if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -315,12 +316,12 @@ func CloneAlbums(router *gin.RouterGroup) { } if len(added) > 0 { - event.Success(fmt.Sprintf("selection added to %s", txt.Quote(a.Title()))) + event.SuccessMsg(i18n.MsgSelectionAddedTo, txt.Quote(a.Title())) PublishAlbumEvent(EntityUpdated, a.AlbumUID, c) } - c.JSON(http.StatusOK, gin.H{"message": "album contents cloned", "album": a, "added": added}) + c.JSON(http.StatusOK, gin.H{"message": i18n.Msg(i18n.MsgAlbumCloned), "album": a, "added": added}) }) } @@ -330,14 +331,14 @@ func AddPhotosToAlbum(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } var f form.Selection if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -345,7 +346,7 @@ func AddPhotosToAlbum(router *gin.RouterGroup) { a, err := query.AlbumByUID(uid) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound) return } @@ -353,7 +354,7 @@ func AddPhotosToAlbum(router *gin.RouterGroup) { if err != nil { log.Errorf("album: %s", err) - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -361,15 +362,15 @@ func AddPhotosToAlbum(router *gin.RouterGroup) { if len(added) > 0 { if len(added) == 1 { - event.Success(fmt.Sprintf("one entry added to %s", txt.Quote(a.Title()))) + event.SuccessMsg(i18n.MsgEntryAddedTo, txt.Quote(a.Title())) } else { - event.Success(fmt.Sprintf("%d entries added to %s", len(added), txt.Quote(a.Title()))) + event.SuccessMsg(i18n.MsgEntriesAddedTo, len(added), txt.Quote(a.Title())) } PublishAlbumEvent(EntityUpdated, a.AlbumUID, c) } - c.JSON(http.StatusOK, gin.H{"message": "photos added to album", "album": a, "photos": photos.UIDs(), "added": added}) + c.JSON(http.StatusOK, gin.H{"message": i18n.Msg(i18n.MsgChangesSaved), "album": a, "photos": photos.UIDs(), "added": added}) }) } @@ -379,27 +380,26 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } var f form.Selection if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } if len(f.Photos) == 0 { - log.Error("no items selected") - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst("no items selected")}) + Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected) return } a, err := query.AlbumByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound) return } @@ -407,15 +407,15 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) { if len(removed) > 0 { if len(removed) == 1 { - event.Success(fmt.Sprintf("one entry removed from %s", txt.Quote(a.Title()))) + event.SuccessMsg(i18n.MsgEntryRemovedFrom, txt.Quote(a.Title())) } else { - event.Success(fmt.Sprintf("%d entries removed from %s", len(removed), txt.Quote(txt.Quote(a.Title())))) + event.SuccessMsg(i18n.MsgEntriesRemovedFrom, len(removed), txt.Quote(txt.Quote(a.Title()))) } PublishAlbumEvent(EntityUpdated, a.AlbumUID, c) } - c.JSON(http.StatusOK, gin.H{"message": "entries removed from album", "album": a, "photos": f.Photos, "removed": removed}) + c.JSON(http.StatusOK, gin.H{"message": i18n.Msg(i18n.MsgChangesSaved), "album": a, "photos": f.Photos, "removed": removed}) }) } @@ -423,7 +423,7 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) { func DownloadAlbum(router *gin.RouterGroup) { router.GET("/albums/:uid/dl", func(c *gin.Context) { if InvalidDownloadToken(c) { - c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg) + AbortUnauthorized(c) return } @@ -432,14 +432,14 @@ func DownloadAlbum(router *gin.RouterGroup) { a, err := query.AlbumByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound) + Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound) return } p, err := query.AlbumPhotos(a, 10000) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())}) + AbortEntityNotFound(c) return } @@ -450,7 +450,7 @@ func DownloadAlbum(router *gin.RouterGroup) { if err := os.MkdirAll(zipPath, 0700); err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst("failed to create zip folder")}) + Abort(c, http.StatusInternalServerError, i18n.ErrCreateFolder) return } @@ -458,7 +458,7 @@ func DownloadAlbum(router *gin.RouterGroup) { if err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())}) + Abort(c, http.StatusInternalServerError, i18n.ErrCreateFile) return } @@ -475,7 +475,7 @@ func DownloadAlbum(router *gin.RouterGroup) { if fs.FileExists(fileName) { if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst("failed to create zip file")}) + Abort(c, http.StatusInternalServerError, i18n.ErrCreateFile) return } log.Infof("album: added %s as %s", txt.Quote(f.FileName), txt.Quote(fileAlias)) diff --git a/internal/api/album_test.go b/internal/api/album_test.go index 8d5192fa5..4ac4b467f 100644 --- a/internal/api/album_test.go +++ b/internal/api/album_test.go @@ -4,6 +4,7 @@ import ( "net/http" "testing" + "github.com/photoprism/photoprism/internal/i18n" "github.com/tidwall/gjson" "github.com/stretchr/testify/assert" @@ -182,7 +183,7 @@ func TestAddPhotosToAlbum(t *testing.T) { AddPhotosToAlbum(router) r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`) val := gjson.Get(r.Body.String(), "message") - assert.Equal(t, "photos added to album", val.String()) + assert.Equal(t, i18n.Msg(i18n.MsgChangesSaved), val.String()) assert.Equal(t, http.StatusOK, r.Code) }) t.Run("add one photo to album", func(t *testing.T) { @@ -190,7 +191,7 @@ func TestAddPhotosToAlbum(t *testing.T) { AddPhotosToAlbum(router) r := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12"]}`) val := gjson.Get(r.Body.String(), "message") - assert.Equal(t, "photos added to album", val.String()) + assert.Equal(t, i18n.Msg(i18n.MsgChangesSaved), val.String()) assert.Equal(t, http.StatusOK, r.Code) }) t.Run("invalid request", func(t *testing.T) { @@ -222,7 +223,7 @@ func TestRemovePhotosFromAlbum(t *testing.T) { RemovePhotosFromAlbum(router) r := PerformRequestWithBody(app, "DELETE", "/api/v1/albums/"+uid+"/photos", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`) val := gjson.Get(r.Body.String(), "message") - assert.Equal(t, "entries removed from album", val.String()) + assert.Equal(t, i18n.Msg(i18n.MsgChangesSaved), val.String()) assert.Equal(t, http.StatusOK, r.Code) }) t.Run("no items selected", func(t *testing.T) { diff --git a/internal/api/api.go b/internal/api/api.go index ede266131..15d03c969 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -32,7 +32,11 @@ https://docs.photoprism.org/developer-guide/ package api import ( + "net/http" + + "github.com/gin-gonic/gin" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/service" ) @@ -49,3 +53,33 @@ func UpdateClientConfig() { event.Publish("config.updated", event.Data{"config": conf.UserConfig()}) } + +func Abort(c *gin.Context, code int, id i18n.Message, params ...interface{}) { + resp := i18n.NewResponse(code, id, params...) + log.Errorf("api: %s", resp.LowerString()) + c.AbortWithStatusJSON(code, resp) +} + +func AbortUnauthorized(c *gin.Context) { + Abort(c, http.StatusUnauthorized, i18n.ErrUnauthorized) +} + +func AbortEntityNotFound(c *gin.Context) { + Abort(c, http.StatusNotFound, i18n.ErrEntityNotFound) +} + +func AbortSaveFailed(c *gin.Context) { + Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed) +} + +func AbortUnexpected(c *gin.Context) { + Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected) +} + +func AbortBadRequest(c *gin.Context) { + Abort(c, http.StatusBadRequest, i18n.ErrBadRequest) +} + +func AbortAlreadyExists(c *gin.Context, s string) { + Abort(c, http.StatusConflict, i18n.ErrAlreadyExists, s) +} diff --git a/internal/api/batch.go b/internal/api/batch.go index 475b1335e..9b7d76ada 100644 --- a/internal/api/batch.go +++ b/internal/api/batch.go @@ -22,7 +22,7 @@ func BatchPhotosArchive(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -31,7 +31,7 @@ func BatchPhotosArchive(router *gin.RouterGroup) { var f form.Selection if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -47,7 +47,7 @@ func BatchPhotosArchive(router *gin.RouterGroup) { err := entity.Db().Where("photo_uid IN (?)", f.Photos).Delete(&entity.Photo{}).Error if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } @@ -74,7 +74,7 @@ func BatchPhotosRestore(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -83,7 +83,7 @@ func BatchPhotosRestore(router *gin.RouterGroup) { var f form.Selection if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -99,7 +99,7 @@ func BatchPhotosRestore(router *gin.RouterGroup) { UpdateColumn("deleted_at", gorm.Expr("NULL")).Error if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } @@ -123,14 +123,14 @@ func BatchAlbumsDelete(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionDelete) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } var f form.Selection if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -159,7 +159,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionPrivate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -168,7 +168,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) { var f form.Selection if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -184,7 +184,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) { gorm.Expr("CASE WHEN photo_private > 0 THEN 0 ELSE 1 END")).Error if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } @@ -210,14 +210,14 @@ func BatchLabelsDelete(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionDelete) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } var f form.Selection if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } diff --git a/internal/api/config.go b/internal/api/config.go index dd789bfcb..ecd9cf43a 100644 --- a/internal/api/config.go +++ b/internal/api/config.go @@ -14,7 +14,7 @@ func GetConfig(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceConfig, acl.ActionRead) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } diff --git a/internal/api/errors.go b/internal/api/errors.go index 61c75e7f5..e0a8c5377 100644 --- a/internal/api/errors.go +++ b/internal/api/errors.go @@ -37,7 +37,7 @@ func GetErrors(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceLogs, acl.ActionSearch) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } diff --git a/internal/api/event.go b/internal/api/event.go index 769f0bbf9..29f5cc098 100644 --- a/internal/api/event.go +++ b/internal/api/event.go @@ -1,8 +1,6 @@ package api import ( - "net/http" - "github.com/gin-gonic/gin" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" @@ -23,7 +21,7 @@ func PublishPhotoEvent(e EntityEvent, uid string, c *gin.Context) { if err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrUnexpectedError) + AbortUnexpected(c) return } @@ -36,7 +34,7 @@ func PublishAlbumEvent(e EntityEvent, uid string, c *gin.Context) { if err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrUnexpectedError) + AbortUnexpected(c) return } @@ -49,7 +47,7 @@ func PublishLabelEvent(e EntityEvent, uid string, c *gin.Context) { if err != nil { log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrUnexpectedError) + AbortUnexpected(c) return } diff --git a/internal/api/file.go b/internal/api/file.go index e7670d2b1..5a974b1b4 100644 --- a/internal/api/file.go +++ b/internal/api/file.go @@ -17,14 +17,14 @@ func GetFile(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceFiles, acl.ActionRead) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } p, err := query.FileByHash(c.Param("hash")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } diff --git a/internal/api/folder.go b/internal/api/folder.go index c89f3455b..0c5d23ddb 100644 --- a/internal/api/folder.go +++ b/internal/api/folder.go @@ -15,7 +15,6 @@ import ( "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/service" - "github.com/photoprism/photoprism/pkg/txt" ) type FoldersResponse struct { @@ -32,7 +31,7 @@ func GetFolders(router *gin.RouterGroup, urlPath, rootName, rootPath string) { s := Auth(SessionID(c), acl.ResourceFolders, acl.ActionSearch) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -42,7 +41,7 @@ func GetFolders(router *gin.RouterGroup, urlPath, rootName, rootPath string) { err := c.MustBindWith(&f, binding.Form) if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } diff --git a/internal/api/geo.go b/internal/api/geo.go index 64cb34e89..c90384f8b 100644 --- a/internal/api/geo.go +++ b/internal/api/geo.go @@ -21,7 +21,7 @@ func GetGeo(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionSearch) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -30,7 +30,7 @@ func GetGeo(router *gin.RouterGroup) { err := c.MustBindWith(&f, binding.Form) if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } diff --git a/internal/api/import.go b/internal/api/import.go index 77a34e531..f43fcd9e4 100644 --- a/internal/api/import.go +++ b/internal/api/import.go @@ -24,7 +24,7 @@ func StartImport(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionImport) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -40,7 +40,7 @@ func StartImport(router *gin.RouterGroup) { var f form.ImportOptions if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -112,7 +112,7 @@ func CancelImport(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionImport) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } diff --git a/internal/api/index.go b/internal/api/index.go index 4e7768885..c71fb0e37 100644 --- a/internal/api/index.go +++ b/internal/api/index.go @@ -21,7 +21,7 @@ func StartIndexing(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -37,7 +37,7 @@ func StartIndexing(router *gin.RouterGroup) { var f form.IndexOptions if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -96,7 +96,7 @@ func CancelIndexing(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } diff --git a/internal/api/label.go b/internal/api/label.go index b3c6d0151..63d266637 100644 --- a/internal/api/label.go +++ b/internal/api/label.go @@ -28,7 +28,7 @@ func GetLabels(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionSearch) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -37,7 +37,7 @@ func GetLabels(router *gin.RouterGroup) { err := c.MustBindWith(&f, binding.Form) if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -62,14 +62,14 @@ func UpdateLabel(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } var f form.Label if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -101,7 +101,7 @@ func LikeLabel(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -139,7 +139,7 @@ func DislikeLabel(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } diff --git a/internal/api/link.go b/internal/api/link.go index dd6b322c3..8465b39de 100644 --- a/internal/api/link.go +++ b/internal/api/link.go @@ -18,14 +18,14 @@ func UpdateLink(c *gin.Context) { s := Auth(SessionID(c), acl.ResourceLinks, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } var f form.Link if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -61,7 +61,7 @@ func DeleteLink(c *gin.Context) { s := Auth(SessionID(c), acl.ResourceLinks, acl.ActionDelete) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -82,14 +82,14 @@ func CreateLink(c *gin.Context) { s := Auth(SessionID(c), acl.ResourceLinks, acl.ActionCreate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } var f form.Link if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -160,7 +160,7 @@ func GetAlbumLinks(router *gin.RouterGroup) { func CreatePhotoLink(router *gin.RouterGroup) { router.POST("/photos/:uid/links", func(c *gin.Context) { if _, err := query.PhotoByUID(c.Param("uid")); err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } diff --git a/internal/api/moments_time.go b/internal/api/moments_time.go index 6531a4936..86a9412cd 100644 --- a/internal/api/moments_time.go +++ b/internal/api/moments_time.go @@ -16,7 +16,7 @@ func GetMomentsTime(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionExport) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } diff --git a/internal/api/photo.go b/internal/api/photo.go index 514a4111e..4943e8b8d 100644 --- a/internal/api/photo.go +++ b/internal/api/photo.go @@ -9,6 +9,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/service" @@ -41,14 +42,14 @@ func GetPhoto(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionRead) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } p, err := query.PhotoPreloadByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } @@ -62,7 +63,7 @@ func UpdatePhoto(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -71,7 +72,7 @@ func UpdatePhoto(router *gin.RouterGroup) { m, err := query.PhotoByUID(uid) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } @@ -80,33 +81,30 @@ func UpdatePhoto(router *gin.RouterGroup) { f, err := form.NewPhoto(m) if err != nil { - log.Errorf("photo: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed) return } // 2) Update form with values from request if err := c.BindJSON(&f); err != nil { - log.Errorf("photo: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusBadRequest, ErrFormInvalid) + Abort(c, http.StatusBadRequest, i18n.ErrBadRequest) return } // 3) Save model with values from form if err := entity.SavePhotoForm(m, f, conf.GeoCodingApi()); err != nil { - log.Error(err) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed) return } PublishPhotoEvent(EntityUpdated, uid, c) - event.Success("photo saved") + event.SuccessMsg(i18n.MsgChangesSaved) p, err := query.PhotoPreloadByUID(uid) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } @@ -130,7 +128,7 @@ func GetPhotoDownload(router *gin.RouterGroup) { f, err := query.FileByPhotoUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg) return } @@ -163,7 +161,7 @@ func GetPhotoYaml(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionExport) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -198,7 +196,7 @@ func ApprovePhoto(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -206,13 +204,13 @@ func ApprovePhoto(router *gin.RouterGroup) { m, err := query.PhotoByUID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } if err := m.Approve(); err != nil { log.Errorf("photo: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } @@ -233,7 +231,7 @@ func LikePhoto(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionLike) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -241,13 +239,13 @@ func LikePhoto(router *gin.RouterGroup) { m, err := query.PhotoByUID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } if err := m.SetFavorite(true); err != nil { log.Errorf("photo: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } @@ -268,7 +266,7 @@ func DislikePhoto(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionLike) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -276,13 +274,13 @@ func DislikePhoto(router *gin.RouterGroup) { m, err := query.PhotoByUID(id) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } if err := m.SetFavorite(false); err != nil { log.Errorf("photo: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } @@ -304,7 +302,7 @@ func PhotoFilePrimary(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -313,18 +311,18 @@ func PhotoFilePrimary(router *gin.RouterGroup) { err := query.SetPhotoPrimary(uid, fileUID) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } PublishPhotoEvent(EntityUpdated, uid, c) - event.Success("photo saved") + event.SuccessMsg(i18n.MsgChangesSaved) p, err := query.PhotoPreloadByUID(uid) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } @@ -342,7 +340,7 @@ func PhotoFileUngroup(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -353,13 +351,13 @@ func PhotoFileUngroup(router *gin.RouterGroup) { if err != nil { log.Errorf("photo: %s (ungroup)", err) - c.AbortWithStatusJSON(http.StatusNotFound, ErrFileNotFound) + AbortEntityNotFound(c) return } if file.FilePrimary { log.Errorf("photo: can't ungroup primary files") - c.AbortWithStatusJSON(http.StatusBadRequest, ErrFormInvalid) + AbortBadRequest(c) return } @@ -368,7 +366,7 @@ func PhotoFileUngroup(router *gin.RouterGroup) { if err := entity.UnscopedDb().Create(&newPhoto).Error; err != nil { log.Errorf("photo: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } @@ -378,7 +376,7 @@ func PhotoFileUngroup(router *gin.RouterGroup) { if err := file.Save(); err != nil { log.Errorf("photo: %s", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } @@ -388,25 +386,25 @@ func PhotoFileUngroup(router *gin.RouterGroup) { if err != nil { log.Errorf("photo: %s (ungroup)", err) - c.AbortWithStatusJSON(http.StatusNotFound, ErrFileNotFound) + AbortEntityNotFound(c) return } if err := service.Index().MediaFile(f, photoprism.IndexOptions{Rescan: true}, existingPhoto.OriginalName).Error; err != nil { log.Errorf("photo: %s", err) - c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed) + AbortSaveFailed(c) return } PublishPhotoEvent(EntityCreated, file.PhotoUID, c) PublishPhotoEvent(EntityUpdated, photoUID, c) - event.Success("file ungrouped") + event.SuccessMsg(i18n.MsgFileUngrouped) p, err := query.PhotoPreloadByUID(photoUID) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } diff --git a/internal/api/photo_label.go b/internal/api/photo_label.go index 32c50649f..cd23a00d7 100644 --- a/internal/api/photo_label.go +++ b/internal/api/photo_label.go @@ -23,21 +23,21 @@ func AddPhotoLabel(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } m, err := query.PhotoByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } var f form.Label if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -71,7 +71,7 @@ func AddPhotoLabel(router *gin.RouterGroup) { p, err := query.PhotoPreloadByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } @@ -98,14 +98,14 @@ func RemovePhotoLabel(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } m, err := query.PhotoByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } @@ -133,7 +133,7 @@ func RemovePhotoLabel(router *gin.RouterGroup) { p, err := query.PhotoPreloadByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } @@ -162,7 +162,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -171,7 +171,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup) { m, err := query.PhotoByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } @@ -190,7 +190,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup) { } if err := c.BindJSON(&label); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -202,7 +202,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup) { p, err := query.PhotoPreloadByUID(c.Param("uid")) if err != nil { - c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + AbortEntityNotFound(c) return } diff --git a/internal/api/photo_label_test.go b/internal/api/photo_label_test.go index dfdb6652b..65d2f7f99 100644 --- a/internal/api/photo_label_test.go +++ b/internal/api/photo_label_test.go @@ -4,6 +4,7 @@ import ( "net/http" "testing" + "github.com/photoprism/photoprism/internal/i18n" "github.com/stretchr/testify/assert" "github.com/tidwall/gjson" ) @@ -29,7 +30,7 @@ func TestAddPhotoLabel(t *testing.T) { AddPhotoLabel(router) r := PerformRequestWithBody(app, "POST", "/api/v1/photos/xxx/label", `{"Name": "Flower", "Uncertainty": 10, "Priority": 2}`) val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Photo not found", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrEntityNotFound), val.String()) assert.Equal(t, http.StatusNotFound, r.Code) }) t.Run("invalid request", func(t *testing.T) { @@ -65,7 +66,7 @@ func TestRemovePhotoLabel(t *testing.T) { RemovePhotoLabel(router) r := PerformRequest(app, "DELETE", "/api/v1/photos/xxx/label/10000001") val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Photo not found", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrEntityNotFound), val.String()) assert.Equal(t, http.StatusNotFound, r.Code) }) t.Run("label not existing", func(t *testing.T) { @@ -105,7 +106,7 @@ func TestUpdatePhotoLabel(t *testing.T) { r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/xxx/label/1000006", `{"Label": {"Name": "NewLabelName"}}`) assert.Equal(t, http.StatusNotFound, r.Code) val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Photo not found", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrEntityNotFound), val.String()) }) t.Run("label not existing", func(t *testing.T) { app, router, _ := NewApiTest() diff --git a/internal/api/photo_search.go b/internal/api/photo_search.go index d472fdbbb..e78735b15 100644 --- a/internal/api/photo_search.go +++ b/internal/api/photo_search.go @@ -4,13 +4,11 @@ import ( "net/http" "strconv" - "github.com/photoprism/photoprism/internal/acl" - "github.com/photoprism/photoprism/internal/query" - "github.com/photoprism/photoprism/pkg/txt" - "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" + "github.com/photoprism/photoprism/internal/acl" "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/internal/query" ) // GET /api/v1/photos @@ -32,7 +30,7 @@ func GetPhotos(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionSearch) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -41,14 +39,14 @@ func GetPhotos(router *gin.RouterGroup) { err := c.MustBindWith(&f, binding.Form) if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } // Guests may only see public content in shared albums. if s.Guest() { if f.Album == "" || !s.HasShare(f.Album) { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -62,7 +60,8 @@ func GetPhotos(router *gin.RouterGroup) { result, count, err := query.PhotoSearch(f) if err != nil { - c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())}) + log.Error(err) + AbortBadRequest(c) return } diff --git a/internal/api/photo_test.go b/internal/api/photo_test.go index 972adb17b..b07e6f002 100644 --- a/internal/api/photo_test.go +++ b/internal/api/photo_test.go @@ -4,6 +4,7 @@ import ( "net/http" "testing" + "github.com/photoprism/photoprism/internal/i18n" "github.com/stretchr/testify/assert" "github.com/tidwall/gjson" ) @@ -50,7 +51,7 @@ func TestUpdatePhoto(t *testing.T) { UpdatePhoto(router) r := PerformRequestWithBody(app, "PUT", "/api/v1/photos/xxx", `{"Name": "Updated01", "Country": "de"}`) val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Photo not found", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrEntityNotFound), val.String()) assert.Equal(t, http.StatusNotFound, r.Code) }) } @@ -131,7 +132,7 @@ func TestSetPhotoPrimary(t *testing.T) { PhotoFilePrimary(router) r := PerformRequest(app, "POST", "/api/v1/photos/xxx/files/ft1es39w45bnlqdw/primary") val := gjson.Get(r.Body.String(), "error") - assert.Equal(t, "Photo not found", val.String()) + assert.Equal(t, i18n.Msg(i18n.ErrEntityNotFound), val.String()) assert.Equal(t, http.StatusNotFound, r.Code) }) } diff --git a/internal/api/session.go b/internal/api/session.go index a5723e521..79ea47fec 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -9,7 +9,6 @@ import ( "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/internal/session" - "github.com/photoprism/photoprism/pkg/txt" ) // POST /api/v1/session @@ -18,7 +17,7 @@ func CreateSession(router *gin.RouterGroup) { var f form.Login if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } diff --git a/internal/api/settings.go b/internal/api/settings.go index dec669638..9dd6a6bef 100644 --- a/internal/api/settings.go +++ b/internal/api/settings.go @@ -5,8 +5,8 @@ import ( "github.com/gin-gonic/gin" "github.com/photoprism/photoprism/internal/acl" + "github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/service" - "github.com/photoprism/photoprism/pkg/txt" ) // GET /api/v1/settings @@ -15,7 +15,7 @@ func GetSettings(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceSettings, acl.ActionRead) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -33,21 +33,21 @@ func SaveSettings(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourceSettings, acl.ActionUpdate) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } conf := service.Config() if conf.SettingsHidden() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } settings := conf.Settings() if err := c.BindJSON(settings); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -58,7 +58,7 @@ func SaveSettings(router *gin.RouterGroup) { UpdateClientConfig() - log.Infof("settings saved") + log.Infof(i18n.Msg(i18n.MsgSettingsSaved)) c.JSON(http.StatusOK, settings) }) diff --git a/internal/api/upload.go b/internal/api/upload.go index d5ac11185..427974b74 100644 --- a/internal/api/upload.go +++ b/internal/api/upload.go @@ -28,7 +28,7 @@ func Upload(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpload) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -38,7 +38,7 @@ func Upload(router *gin.RouterGroup) { f, err := c.MultipartForm() if err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -51,7 +51,7 @@ func Upload(router *gin.RouterGroup) { p := path.Join(conf.ImportPath(), "upload", subPath) if err := os.MkdirAll(p, os.ModePerm); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } @@ -61,7 +61,7 @@ func Upload(router *gin.RouterGroup) { log.Debugf("upload: saving file %s", txt.Quote(file.Filename)) if err := c.SaveUploadedFile(file, filename); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } diff --git a/internal/api/user.go b/internal/api/user.go index 622af9a47..4f819fbbe 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -23,7 +23,7 @@ func ChangePassword(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePeople, acl.ActionUpdateSelf) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } diff --git a/internal/api/zip.go b/internal/api/zip.go index 6a92d1eed..37c2177a8 100644 --- a/internal/api/zip.go +++ b/internal/api/zip.go @@ -28,7 +28,7 @@ func CreateZip(router *gin.RouterGroup) { s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDownload) if s.Invalid() { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + AbortUnauthorized(c) return } @@ -43,7 +43,7 @@ func CreateZip(router *gin.RouterGroup) { start := time.Now() if err := c.BindJSON(&f); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + AbortBadRequest(c) return } diff --git a/internal/config/settings.go b/internal/config/settings.go index 07e620936..525f81870 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" + "github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/txt" "gopkg.in/yaml.v2" @@ -106,8 +107,8 @@ func NewSettings() *Settings { } // Propagate updates settings in other packages as needed. -func (s *Settings) Propagate() { - +func (s Settings) Propagate() { + i18n.SetLang(s.Language) } // Load uses a yaml config file to initiate the configuration entity. diff --git a/internal/config/test.go b/internal/config/test.go index bd6206a3b..3935049db 100644 --- a/internal/config/test.go +++ b/internal/config/test.go @@ -150,6 +150,9 @@ func NewTestConfig() *Config { // NewTestErrorConfig inits invalid config used for testing func NewTestErrorConfig() *Config { c := &Config{params: NewTestParamsError()} + + c.initSettings() + err := c.Init(context.Background()) if err != nil { log.Fatalf("config: %s", err.Error()) diff --git a/internal/event/hub.go b/internal/event/hub.go index 1e9e90c66..2407d7d2a 100644 --- a/internal/event/hub.go +++ b/internal/event/hub.go @@ -2,6 +2,7 @@ package event import ( "github.com/leandro-lugaresi/hub" + "github.com/photoprism/photoprism/internal/i18n" ) type Hub = hub.Hub @@ -39,6 +40,22 @@ func Warning(msg string) { Publish("notify.warning", Data{"msg": msg}) } +func ErrorMsg(id i18n.Message, params ...interface{}) { + Error(i18n.Msg(id, params...)) +} + +func SuccessMsg(id i18n.Message, params ...interface{}) { + Success(i18n.Msg(id, params...)) +} + +func InfoMsg(id i18n.Message, params ...interface{}) { + Info(i18n.Msg(id, params...)) +} + +func WarningMsg(id i18n.Message, params ...interface{}) { + Warning(i18n.Msg(id, params...)) +} + func Publish(event string, data Data) { SharedHub().Publish(Message{ Name: event, diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go new file mode 100644 index 000000000..7fe9c17b5 --- /dev/null +++ b/internal/i18n/i18n.go @@ -0,0 +1,97 @@ +/* + +Package i18n contains PhotoPrism status and error message strings. + +Copyright (c) 2018 - 2020 Michael Mayer + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + + PhotoPrism™ is a registered trademark of Michael Mayer. You may use it as required + to describe our software, run your own server, for educational purposes, but not for + offering commercial goods, products, or services without prior written permission. + In other words, please ask. + +Feel free to send an e-mail to hello@photoprism.org if you have questions, +want to support our work, or just want to say hello. + +Additional information can be found in our Developer Guide: +https://docs.photoprism.org/developer-guide/ + +*/ +package i18n + +import ( + "errors" + "fmt" + "strings" +) + +type Message int +type MessageMap map[Message]string + +func Msg(id Message, params ...interface{}) string { + return LangMsg(id, Lang, params...) +} + +func LangMsg(id Message, lang Language, params ...interface{}) string { + msgs, ok := Languages[lang] + + if !ok && lang != Default { + msgs, ok = Languages[Default] + } + + msg, ok := msgs[id] + + if !ok { + msg, ok = Languages[Default][id] + } + + if !ok { + return fmt.Sprintf("i18n: unknown message id %d", id) + } + + if strings.Contains(msg, "%") { + msg = fmt.Sprintf(msg, params...) + } + + return msg +} + +func DefaultMsg(id Message, params ...interface{}) string { + return LangMsg(id, Default, params...) +} + +func Error(id Message, params ...interface{}) error { + return errors.New(Msg(id, params...)) +} + +func LangError(id Message, lang Language, params ...interface{}) error { + return errors.New(LangMsg(id, lang, params...)) +} + +func DefaultError(id Message, params ...interface{}) error { + return LangError(id, Default, params...) +} + +/* +func JsonBadRequest(id Message) gin.H { + return JsonError(http.StatusBadRequest, msg) +} + +func JsonForbidden(id Message) gin.H { + return JsonError(http.StatusForbidden, msg) +} + + +*/ diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go new file mode 100644 index 000000000..47abf9034 --- /dev/null +++ b/internal/i18n/i18n_test.go @@ -0,0 +1,121 @@ +package i18n + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMsg(t *testing.T) { + t.Run("already exists", func(t *testing.T) { + msg := Msg(ErrAlreadyExists, "A cat") + assert.Equal(t, "A cat already exists", msg) + }) + + t.Run("unexpected error", func(t *testing.T) { + msg := Msg(ErrUnexpected, "A cat") + assert.Equal(t, "Unexpected error, please try again", msg) + }) + + t.Run("already exists german", func(t *testing.T) { + SetLang("de") + msgGerman := Msg(ErrAlreadyExists, "Eine Katze") + assert.Equal(t, "Eine Katze existiert bereits", msgGerman) + SetLang("") + msgDefault := Msg(ErrAlreadyExists, "A cat") + assert.Equal(t, "A cat already exists", msgDefault) + }) +} + +func TestLangMsg(t *testing.T) { + t.Run("already exists", func(t *testing.T) { + msgDefault := LangMsg(ErrAlreadyExists, Default, "A cat") + assert.Equal(t, "A cat already exists", msgDefault) + msgEnglish := LangMsg(ErrAlreadyExists, English, "A cat") + assert.Equal(t, msgEnglish, msgDefault) + }) + + t.Run("unexpected error", func(t *testing.T) { + msgDefault := LangMsg(ErrUnexpected, Default, "A cat") + assert.Equal(t, "Unexpected error, please try again", msgDefault) + msgEnglish := LangMsg(ErrUnexpected, English, "A cat") + assert.Equal(t, msgEnglish, msgDefault) + }) + + t.Run("already exists german", func(t *testing.T) { + msg := LangMsg(ErrAlreadyExists, German, "Eine Katze") + assert.Equal(t, "Eine Katze existiert bereits", msg) + }) + + t.Run("unexpected error german", func(t *testing.T) { + msg := LangMsg(ErrUnexpected, German, "Eine Katze") + assert.Equal(t, "Unerwarteter Fehler, bitte erneut versuchen", msg) + }) +} + +func TestDefaultMsg(t *testing.T) { + t.Run("already exists", func(t *testing.T) { + msg := DefaultMsg(ErrAlreadyExists, "A cat") + assert.Equal(t, "A cat already exists", msg) + }) + + t.Run("unexpected error", func(t *testing.T) { + msg := DefaultMsg(ErrUnexpected, "A cat") + assert.Equal(t, "Unexpected error, please try again", msg) + }) +} + +func TestError(t *testing.T) { + t.Run("already exists", func(t *testing.T) { + err := Error(ErrAlreadyExists, "A cat") + assert.EqualError(t, err, "A cat already exists") + }) + + t.Run("unexpected error", func(t *testing.T) { + err := Error(ErrUnexpected, "A cat") + assert.EqualError(t, err, "Unexpected error, please try again") + }) + + t.Run("already exists german", func(t *testing.T) { + SetLang("de") + errGerman := Error(ErrAlreadyExists, "Eine Katze") + assert.EqualError(t, errGerman, "Eine Katze existiert bereits") + SetLang("") + errDefault := Error(ErrAlreadyExists, "A cat") + assert.EqualError(t, errDefault, "A cat already exists") + }) +} + +func TestLangError(t *testing.T) { + t.Run("already exists", func(t *testing.T) { + err := LangError(ErrAlreadyExists, English, "A cat") + assert.EqualError(t, err, "A cat already exists") + }) + + t.Run("unexpected error", func(t *testing.T) { + err := LangError(ErrUnexpected, English, "A cat") + assert.EqualError(t, err, "Unexpected error, please try again") + }) + + t.Run("already exists german", func(t *testing.T) { + err := LangError(ErrAlreadyExists, German, "Eine Katze") + assert.EqualError(t, err, "Eine Katze existiert bereits") + }) + + t.Run("unexpected error german", func(t *testing.T) { + err := LangError(ErrUnexpected, German, "Eine Katze") + assert.EqualError(t, err, "Unerwarteter Fehler, bitte erneut versuchen") + }) +} + +func TestDefaultError(t *testing.T) { + t.Run("already exists", func(t *testing.T) { + err := DefaultError(ErrAlreadyExists, "A cat") + assert.EqualError(t, err, "A cat already exists") + }) + + t.Run("unexpected error", func(t *testing.T) { + err := DefaultError(ErrUnexpected, "A cat") + assert.EqualError(t, err, "Unexpected error, please try again") + }) +} diff --git a/internal/i18n/lang-de.go b/internal/i18n/lang-de.go new file mode 100644 index 000000000..7031df759 --- /dev/null +++ b/internal/i18n/lang-de.go @@ -0,0 +1,33 @@ +package i18n + +var MsgGerman = MessageMap{ + ErrUnexpected: "Unerwarteter Fehler, bitte erneut versuchen", + ErrSaveFailed: "Daten gekonnten nicht gespeichert werden", + ErrBadRequest: "Ungültige Eingabe, bitte erneut versuchen", + ErrAlreadyExists: "%s existiert bereits", + ErrEntityNotFound: "Eintrag nicht gefunden", + ErrAccountNotFound: "Unbekannter Account", + ErrAlbumNotFound: "Album nicht gefunden - gelöscht?", + ErrReadOnly: "Funktion im 'read-only' Modus nicht verfügbar", + ErrUnauthorized: "Anmeldung erforderlich", + ErrUploadNSFW: "Inhalt könnte anstößig sein und wurde abgelehnt", + ErrNoItemsSelected: "Auswahl ist leer, bitte erneut versuchen", + ErrCreateFile: "Datei konnte nicht angelegt werden", + ErrCreateFolder: "Verzeichnis konnte nicht angelegt werden", + ErrConnectionFailed: "Could not connect, please try again", + + MsgChangesSaved: "Änderungen erfolgreich gespeichert", + MsgAlbumCreated: "Album erstellt", + MsgAlbumSaved: "Album gespeichert", + MsgAlbumDeleted: "Album %s gelöscht", + MsgAlbumCloned: "Album-Einträge kopiert", + MsgFileUngrouped: "Datei-Gruppierung aufgehoben", + MsgSelectionAddedTo: "Auswahl zu %s hinzugefügt", + MsgEntryAddedTo: "Ein Eintrag zu %s hinzugefügt", + MsgEntriesAddedTo: "%d Einträge zu %s hinzugefügt", + MsgEntryRemovedFrom: "Ein Eintrag aus %s entfernt", + MsgEntriesRemovedFrom: "%d Einträge aus %s entfernt", + MsgAccountCreated: "Server-Konfiguration angelegt", + MsgAccountSaved: "Server-Konfiguration gespeichert", + MsgAccountDeleted: "Server-Konfiguration gelöscht", +} diff --git a/internal/i18n/lang-en.go b/internal/i18n/lang-en.go new file mode 100644 index 000000000..26135706c --- /dev/null +++ b/internal/i18n/lang-en.go @@ -0,0 +1,67 @@ +package i18n + +const ( + ErrUnexpected Message = iota + 1 + ErrBadRequest + ErrSaveFailed + ErrAlreadyExists + ErrEntityNotFound + ErrAccountNotFound + ErrAlbumNotFound + ErrReadOnly + ErrUnauthorized + ErrUploadNSFW + ErrNoItemsSelected + ErrCreateFile + ErrCreateFolder + ErrConnectionFailed + + MsgChangesSaved + MsgAlbumCreated + MsgAlbumSaved + MsgAlbumDeleted + MsgAlbumCloned + MsgFileUngrouped + MsgSelectionAddedTo + MsgEntryAddedTo + MsgEntriesAddedTo + MsgEntryRemovedFrom + MsgEntriesRemovedFrom + MsgAccountCreated + MsgAccountSaved + MsgAccountDeleted + MsgSettingsSaved +) + +var MsgEnglish = MessageMap{ + ErrUnexpected: "Unexpected error, please try again", + ErrBadRequest: "Invalid request, please try again", + ErrSaveFailed: "Changes could not be saved", + ErrAlreadyExists: "%s already exists", + ErrEntityNotFound: "Unknown entity", + ErrAccountNotFound: "Unknown account", + ErrAlbumNotFound: "Album not found", + ErrReadOnly: "not available in read-only mode", + ErrUnauthorized: "Please log in and try again", + ErrUploadNSFW: "Upload might be offensive", + ErrNoItemsSelected: "No items selected", + ErrCreateFile: "Failed creating file, please check permissions", + ErrCreateFolder: "Failed creating folder, please check permissions", + ErrConnectionFailed: "Could not connect, please try again", + + MsgChangesSaved: "Changes successfully saved", + MsgAlbumCreated: "Album created", + MsgAlbumSaved: "Album saved", + MsgAlbumDeleted: "Album %s deleted", + MsgAlbumCloned: "Album contents cloned", + MsgFileUngrouped: "File successfully ungrouped", + MsgSelectionAddedTo: "Selection added to %s", + MsgEntryAddedTo: "One entry added to %s", + MsgEntriesAddedTo: "%d entries added to %s", + MsgEntryRemovedFrom: "One entry removed from %s", + MsgEntriesRemovedFrom: "%d entries removed from %s", + MsgAccountCreated: "Account created", + MsgAccountSaved: "Account saved", + MsgAccountDeleted: "Account deleted", + MsgSettingsSaved: "Settings saved", +} diff --git a/internal/i18n/lang-fr.go b/internal/i18n/lang-fr.go new file mode 100644 index 000000000..27a21b14b --- /dev/null +++ b/internal/i18n/lang-fr.go @@ -0,0 +1,32 @@ +package i18n + +var MsgFrench = MessageMap{ + ErrUnexpected: "Unexpected error, please try again", + ErrBadRequest: "Invalid request, please try again", + ErrSaveFailed: "Changes could not be saved", + ErrAlreadyExists: "%s already exists", + ErrEntityNotFound: "Unknown entity", + ErrAccountNotFound: "Unknown account", + ErrAlbumNotFound: "Album not found", + ErrReadOnly: "not available in read-only mode", + ErrUnauthorized: "please log in and try again", + ErrUploadNSFW: "Upload might be offensive", + ErrNoItemsSelected: "No items selected", + ErrCreateFile: "Failed creating file, please check permissions", + ErrCreateFolder: "Failed creating folder, please check permissions", + + MsgChangesSaved: "Changes successfully saved", + MsgAlbumCreated: "Album created", + MsgAlbumSaved: "Album saved", + MsgAlbumDeleted: "Album %s deleted", + MsgAlbumCloned: "Album contents cloned", + MsgFileUngrouped: "File successfully ungrouped", + MsgSelectionAddedTo: "Selection added to %s", + MsgEntryAddedTo: "One entry added to %s", + MsgEntriesAddedTo: "%d entries added to %s", + MsgEntryRemovedFrom: "One entry removed from %s", + MsgEntriesRemovedFrom: "%d entries removed from %s", + MsgAccountCreated: "Account created", + MsgAccountSaved: "Account saved", + MsgAccountDeleted: "Account deleted", +} diff --git a/internal/i18n/lang-nl.go b/internal/i18n/lang-nl.go new file mode 100644 index 000000000..9b3588961 --- /dev/null +++ b/internal/i18n/lang-nl.go @@ -0,0 +1,32 @@ +package i18n + +var MsgDutch = MessageMap{ + ErrUnexpected: "Unexpected error, please try again", + ErrBadRequest: "Invalid request, please try again", + ErrSaveFailed: "Changes could not be saved", + ErrAlreadyExists: "%s already exists", + ErrEntityNotFound: "Unknown entity", + ErrAccountNotFound: "Unknown account", + ErrAlbumNotFound: "Album not found", + ErrReadOnly: "not available in read-only mode", + ErrUnauthorized: "please log in and try again", + ErrUploadNSFW: "Upload might be offensive", + ErrNoItemsSelected: "No items selected", + ErrCreateFile: "Failed creating file, please check permissions", + ErrCreateFolder: "Failed creating folder, please check permissions", + + MsgChangesSaved: "Changes successfully saved", + MsgAlbumCreated: "Album created", + MsgAlbumSaved: "Album saved", + MsgAlbumDeleted: "Album %s deleted", + MsgAlbumCloned: "Album contents cloned", + MsgFileUngrouped: "File successfully ungrouped", + MsgSelectionAddedTo: "Selection added to %s", + MsgEntryAddedTo: "One entry added to %s", + MsgEntriesAddedTo: "%d entries added to %s", + MsgEntryRemovedFrom: "One entry removed from %s", + MsgEntriesRemovedFrom: "%d entries removed from %s", + MsgAccountCreated: "Account created", + MsgAccountSaved: "Account saved", + MsgAccountDeleted: "Account deleted", +} diff --git a/internal/i18n/lang-ru.go b/internal/i18n/lang-ru.go new file mode 100644 index 000000000..7704bbe9f --- /dev/null +++ b/internal/i18n/lang-ru.go @@ -0,0 +1,32 @@ +package i18n + +var MsgRussian = MessageMap{ + ErrUnexpected: "Unexpected error, please try again", + ErrBadRequest: "Invalid request, please try again", + ErrSaveFailed: "Changes could not be saved", + ErrAlreadyExists: "%s already exists", + ErrEntityNotFound: "Unknown entity", + ErrAccountNotFound: "Unknown account", + ErrAlbumNotFound: "Album not found", + ErrReadOnly: "not available in read-only mode", + ErrUnauthorized: "please log in and try again", + ErrUploadNSFW: "Upload might be offensive", + ErrNoItemsSelected: "No items selected", + ErrCreateFile: "Failed creating file, please check permissions", + ErrCreateFolder: "Failed creating folder, please check permissions", + + MsgChangesSaved: "Changes successfully saved", + MsgAlbumCreated: "Album created", + MsgAlbumSaved: "Album saved", + MsgAlbumDeleted: "Album %s deleted", + MsgAlbumCloned: "Album contents cloned", + MsgFileUngrouped: "File successfully ungrouped", + MsgSelectionAddedTo: "Selection added to %s", + MsgEntryAddedTo: "One entry added to %s", + MsgEntriesAddedTo: "%d entries added to %s", + MsgEntryRemovedFrom: "One entry removed from %s", + MsgEntriesRemovedFrom: "%d entries removed from %s", + MsgAccountCreated: "Account created", + MsgAccountSaved: "Account saved", + MsgAccountDeleted: "Account deleted", +} diff --git a/internal/i18n/lang.go b/internal/i18n/lang.go new file mode 100644 index 000000000..85d7646b5 --- /dev/null +++ b/internal/i18n/lang.go @@ -0,0 +1,35 @@ +package i18n + +import "strings" + +type Language string + +type LanguageMap map[Language]MessageMap + +const ( + English Language = "en" + Dutch Language = "nl" + French Language = "fr" + German Language = "de" + Russian Language = "ru" + Default = English +) + +var Languages = LanguageMap{ + English: MsgEnglish, + Dutch: MsgDutch, + French: MsgFrench, + German: MsgGerman, + Russian: MsgRussian, +} + +var Lang = Default + +func SetLang(s string) { + if len(s) != 2 { + Lang = Default + } else { + s = strings.ToLower(s) + Lang = Language(s) + } +} diff --git a/internal/i18n/lang_test.go b/internal/i18n/lang_test.go new file mode 100644 index 000000000..d3c628d94 --- /dev/null +++ b/internal/i18n/lang_test.go @@ -0,0 +1,18 @@ +package i18n + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetLang(t *testing.T) { + assert.Equal(t, English, Lang) + SetLang("D") + assert.Equal(t, English, Lang) + SetLang("De") + assert.Equal(t, German, Lang) + SetLang("") + assert.Equal(t, English, Lang) + assert.Equal(t, Default, Lang) +} diff --git a/internal/i18n/response.go b/internal/i18n/response.go new file mode 100644 index 000000000..8b351b0c7 --- /dev/null +++ b/internal/i18n/response.go @@ -0,0 +1,33 @@ +package i18n + +import "strings" + +type Response struct { + Code int `json:"code"` + Err string `json:"error,omitempty"` + Msg string `json:"success,omitempty"` +} + +func (r Response) String() string { + if r.Err != "" { + return r.Err + } else { + return r.Msg + } +} + +func (r Response) LowerString() string { + return strings.ToLower(r.String()) +} + +func (r Response) Error() string { + return r.Err +} + +func NewResponse(code int, id Message, params ...interface{}) Response { + if code < 400 { + return Response{Code: code, Msg: Msg(id, params...)} + } else { + return Response{Code: code, Err: Msg(id, params...)} + } +} diff --git a/internal/i18n/response_test.go b/internal/i18n/response_test.go new file mode 100644 index 000000000..1f33f95e6 --- /dev/null +++ b/internal/i18n/response_test.go @@ -0,0 +1,38 @@ +package i18n + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewResponse(t *testing.T) { + t.Run("already exists", func(t *testing.T) { + resp := NewResponse(http.StatusConflict, ErrAlreadyExists, "A cat") + assert.Equal(t, http.StatusConflict, resp.Code) + assert.Equal(t, "A cat already exists", resp.Err) + assert.Equal(t, "", resp.Msg) + }) + + t.Run("unexpected error", func(t *testing.T) { + resp := NewResponse(http.StatusInternalServerError, ErrUnexpected, "A cat") + assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.Equal(t, "Unexpected error, please try again", resp.Err) + assert.Equal(t, "", resp.Msg) + }) + + t.Run("changes saved", func(t *testing.T) { + resp := NewResponse(http.StatusOK, MsgChangesSaved) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "", resp.Err) + assert.Equal(t, "Changes successfully saved", resp.Msg) + + if s, err := json.Marshal(resp); err != nil { + t.Fatal(err) + } else { + assert.Equal(t, `{"code":200,"success":"Changes successfully saved"}`, string(s)) + } + }) +}