Backend: Add translations for API messages

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-07-04 12:54:35 +02:00
parent 44c0c4a58b
commit 68843a626d
41 changed files with 802 additions and 211 deletions

View file

@ -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)
})

View file

@ -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)
})

View file

@ -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))

View file

@ -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) {

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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()

View file

@ -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
}

View file

@ -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)
})
}

View file

@ -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
}

View file

@ -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)
})

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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.

View file

@ -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())

View file

@ -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,

97
internal/i18n/i18n.go Normal file
View file

@ -0,0 +1,97 @@
/*
Package i18n contains PhotoPrism status and error message strings.
Copyright (c) 2018 - 2020 Michael Mayer <hello@photoprism.org>
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 <https://www.gnu.org/licenses/>.
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)
}
*/

121
internal/i18n/i18n_test.go Normal file
View file

@ -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")
})
}

33
internal/i18n/lang-de.go Normal file
View file

@ -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",
}

67
internal/i18n/lang-en.go Normal file
View file

@ -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",
}

32
internal/i18n/lang-fr.go Normal file
View file

@ -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",
}

32
internal/i18n/lang-nl.go Normal file
View file

@ -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",
}

32
internal/i18n/lang-ru.go Normal file
View file

@ -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",
}

35
internal/i18n/lang.go Normal file
View file

@ -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)
}
}

View file

@ -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)
}

33
internal/i18n/response.go Normal file
View file

@ -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...)}
}
}

View file

@ -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))
}
})
}