diff --git a/internal/i18n/locale.go b/internal/i18n/locale.go index 442b1da..e4721cd 100644 --- a/internal/i18n/locale.go +++ b/internal/i18n/locale.go @@ -112,6 +112,20 @@ func (store *LocaleStore) MatchTag(langs []language.Tag) string { return "en-US" } +func (l *Locale) String(key string, args ...any) string { + message := l.Messages[key] + + if message == "" { + return Locales.Locales["en-US"].String(key, args...) + } + + if len(args) == 0 { + return message + } + + return fmt.Sprintf(message, args...) +} + func (l *Locale) Tr(key string, args ...any) template.HTML { message := l.Messages[key] diff --git a/internal/i18n/locales/en-US.yml b/internal/i18n/locales/en-US.yml index f0a54e9..0ed41ca 100644 --- a/internal/i18n/locales/en-US.yml +++ b/internal/i18n/locales/en-US.yml @@ -44,8 +44,10 @@ gist.new.create-public-button: Create public gist gist.new.create-unlisted-button: Create unlisted gist gist.new.create-private-button: Create private gist gist.new.preview: Preview +gist.new.create-a-new-gist: Create a new gist gist.edit.editing: Editing +gist.edit.edit-gist: Edit %s gist.edit.change-visibility: Make gist.edit.delete: Delete gist.edit.cancel: Cancel @@ -68,6 +70,9 @@ gist.list.forks: forks gist.list.files: files gist.list.last-active: Last active gist.list.no-gists: No gists +gist.list.all-liked-by: All gists liked by %s +gist.list.all-forked-by: All gists forked by %s +gist.list.all-from: All gists from %s gist.search.found: gists found gist.search.no-results: No gists found @@ -80,9 +85,11 @@ gist.search.help.language: gists having files with given language gist.forks: Forks gist.forks.view: View fork gist.forks.no: No public forks +gist.forks.for: Forks for %s gist.likes: Likes gist.likes.no: No likes yet +gist.likes.for: Likes for %s gist.revisions: Revisions gist.revision.revised: revised this gist @@ -95,6 +102,7 @@ gist.revision.file-renamed-no-changes: File renamed without changes gist.revision.empty-file: Empty file gist.revision.no-changes: No changes gist.revision.no-revisions: No revisions to show +gist.revision-of: Revision of %s settings: Settings settings.email: Email @@ -136,6 +144,16 @@ auth.login-instead: Login instead auth.oauth: Continue with %s account error: Error +error.page-not-found: Page not found +error.bad-request: Bad request +error.signup-disabled: Signing up is disabled +error.signup-disabled-form: Signing up via registration form is disabled +error.login-disabled-form: Logging in via login form is disabled +error.complete-oauth-login: "Cannot complete user auth: %s" +error.oauth-unsupported: Unsupported provider +error.cannot-bind-data: Cannot bind data +error.invalid-number: Invalid number +error.invalid-character-unescaped: Invalid character unescaped header.menu.all: All header.menu.new: New @@ -204,4 +222,45 @@ admin.invitations.expires_at: Expires at admin.invitations.code: Code admin.invitations.copy_link: Copy link admin.invitations.uses: Uses -admin.invitations.expired: Expired \ No newline at end of file +admin.invitations.expired: Expired + +flash.admin.user-deleted: User has been deleted +flash.admin.gist-deleted: Gist has been deleted +flash.admin.invitation-created: Invitation has been created +flash.admin.invitation-deleted: Invitation has been deleted +flash.admin.sync-fs: Syncing repositories from filesystem... +flash.admin.sync-db: Syncing repositories from database... +flash.admin.git-gc: Garbage collecting repositories... +flash.admin.sync-previews: Syncing Gist previews... +flash.admin.reset-hooks: Resetting Git server hooks for all repositories... +flash.admin.index-gists: Indexing all gists... + +flash.auth.username-exists: Username already exists +flash.auth.invalid-credentials: Invalid credentials +flash.auth.account-linked-oauth: Account linked to %s +flash.auth.account-unlinked-oauth: Account unlinked from %s +flash.auth.user-sshkeys-not-retrievable: Could not get user keys +flash.auth.user-sshkeys-not-created: Could not create ssh key +flash.auth.must-be-logged-in: You must be logged in to access gists + +flash.gist.visibility-changed: Gist visibility has been changed +flash.gist.deleted: Gist has been deleted +flash.gist.fork-own-gist: Unable to fork own gists +flash.gist.forked: Gist has been forked + +flash.user.email-updated: Email updated +flash.user.invalid-ssh-key: Invalid SSH key +flash.user.ssh-key-added: SSH key added +flash.user.ssh-key-deleted: SSH key deleted +flash.user.password-updated: Password updated +flash.user.username-updated: Username updated + +validation.is-too-long: Field %s is too long +validation.should-not-be-empty: Field %s should not be empty +validation.should-not-include-sub-directory: Field %s should not include a sub directory +validation.should-only-contain-alphanumeric-characters: Field %s should only contain alphanumeric characters +validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes +validation.not-enough: Not enough %s +validation.invalid: Invalid %s + +html.title.admin-panel: Admin panel \ No newline at end of file diff --git a/internal/utils/validator.go b/internal/utils/validator.go index 23aeb5f..04cbbc3 100644 --- a/internal/utils/validator.go +++ b/internal/utils/validator.go @@ -2,6 +2,7 @@ package utils import ( "github.com/go-playground/validator/v10" + "github.com/thomiceli/opengist/internal/i18n" "regexp" "strings" ) @@ -26,26 +27,26 @@ func (cv *OpengistValidator) Var(field interface{}, tag string) error { return cv.v.Var(field, tag) } -func ValidationMessages(err *error) string { +func ValidationMessages(err *error, locale *i18n.Locale) string { errs := (*err).(validator.ValidationErrors) messages := make([]string, len(errs)) for i, e := range errs { switch e.Tag() { case "max": - messages[i] = e.Field() + " is too long" + messages[i] = locale.String("validation.is-too-long", e.Field()) case "required": - messages[i] = e.Field() + " should not be empty" + messages[i] = locale.String("validation.should-not-be-empty", e.Field()) case "excludes": - messages[i] = e.Field() + " should not include a sub directory" + messages[i] = locale.String("validation.should-not-include-sub-directory", e.Field()) case "alphanum": - messages[i] = e.Field() + " should only contain alphanumeric characters" + messages[i] = locale.String("validation.should-only-contain-alphanumeric-characters", e.Field()) case "alphanumdash": case "alphanumdashorempty": - messages[i] = e.Field() + " should only contain alphanumeric characters and dashes" + messages[i] = locale.String("validation.should-only-contain-alphanumeric-characters-and-dashes", e.Field()) case "min": - messages[i] = "Not enough " + e.Field() + messages[i] = locale.String("validation.not-enough", e.Field()) case "notreserved": - messages[i] = "Invalid " + e.Field() + messages[i] = locale.String("validation.invalid", e.Field()) } } diff --git a/internal/web/admin.go b/internal/web/admin.go index f660ba4..22ef2d3 100644 --- a/internal/web/admin.go +++ b/internal/web/admin.go @@ -12,8 +12,7 @@ import ( ) func adminIndex(ctx echo.Context) error { - setData(ctx, "title", "Admin panel") - setData(ctx, "htmlTitle", "Admin panel") + setData(ctx, "htmlTitle", trH(ctx, "admin.admin_panel")) setData(ctx, "adminHeaderPage", "index") setData(ctx, "opengistVersion", config.OpengistVersion) @@ -52,8 +51,7 @@ func adminIndex(ctx echo.Context) error { } func adminUsers(ctx echo.Context) error { - setData(ctx, "title", "Users") - setData(ctx, "htmlTitle", "Users - Admin panel") + setData(ctx, "htmlTitle", trH(ctx, "admin.users")+" - "+trH(ctx, "admin.admin_panel")) setData(ctx, "adminHeaderPage", "users") pageInt := getPage(ctx) @@ -64,15 +62,14 @@ func adminUsers(ctx echo.Context) error { } if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil { - return errorRes(404, "Page not found", nil) + return errorRes(404, tr(ctx, "error.page-not-found"), nil) } return html(ctx, "admin_users.html") } func adminGists(ctx echo.Context) error { - setData(ctx, "title", "Gists") - setData(ctx, "htmlTitle", "Gists - Admin panel") + setData(ctx, "htmlTitle", trH(ctx, "admin.gists")+" - "+trH(ctx, "admin.admin_panel")) setData(ctx, "adminHeaderPage", "gists") pageInt := getPage(ctx) @@ -83,7 +80,7 @@ func adminGists(ctx echo.Context) error { } if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil { - return errorRes(404, "Page not found", nil) + return errorRes(404, tr(ctx, "error.page-not-found"), nil) } return html(ctx, "admin_gists.html") @@ -100,7 +97,7 @@ func adminUserDelete(ctx echo.Context) error { return errorRes(500, "Cannot delete this user", err) } - addFlash(ctx, "User has been deleted", "success") + addFlash(ctx, tr(ctx, "flash.admin.user-deleted"), "success") return redirect(ctx, "/admin-panel/users") } @@ -120,49 +117,48 @@ func adminGistDelete(ctx echo.Context) error { gist.RemoveFromIndex() - addFlash(ctx, "Gist has been deleted", "success") + addFlash(ctx, tr(ctx, "flash.admin.gist-deleted"), "success") return redirect(ctx, "/admin-panel/gists") } func adminSyncReposFromFS(ctx echo.Context) error { - addFlash(ctx, "Syncing repositories from filesystem...", "success") + addFlash(ctx, tr(ctx, "flash.admin.sync-fs"), "success") go actions.Run(actions.SyncReposFromFS) return redirect(ctx, "/admin-panel") } func adminSyncReposFromDB(ctx echo.Context) error { - addFlash(ctx, "Syncing repositories from database...", "success") + addFlash(ctx, tr(ctx, "flash.admin.sync-db"), "success") go actions.Run(actions.SyncReposFromDB) return redirect(ctx, "/admin-panel") } func adminGcRepos(ctx echo.Context) error { - addFlash(ctx, "Garbage collecting repositories...", "success") + addFlash(ctx, tr(ctx, "flash.admin.git-gc"), "success") go actions.Run(actions.GitGcRepos) return redirect(ctx, "/admin-panel") } func adminSyncGistPreviews(ctx echo.Context) error { - addFlash(ctx, "Syncing Gist previews...", "success") + addFlash(ctx, tr(ctx, "flash.admin.sync-previews"), "success") go actions.Run(actions.SyncGistPreviews) return redirect(ctx, "/admin-panel") } func adminResetHooks(ctx echo.Context) error { - addFlash(ctx, "Resetting Git server hooks for all repositories...", "success") + addFlash(ctx, tr(ctx, "flash.admin.reset-hooks"), "success") go actions.Run(actions.ResetHooks) return redirect(ctx, "/admin-panel") } func adminIndexGists(ctx echo.Context) error { - addFlash(ctx, "Indexing all gists...", "success") + addFlash(ctx, tr(ctx, "flash.admin.index-gists"), "success") go actions.Run(actions.IndexGists) return redirect(ctx, "/admin-panel") } func adminConfig(ctx echo.Context) error { - setData(ctx, "title", "Configuration") - setData(ctx, "htmlTitle", "Configuration - Admin panel") + setData(ctx, "htmlTitle", trH(ctx, "admin.configuration")+" - "+trH(ctx, "admin.admin_panel")) setData(ctx, "adminHeaderPage", "config") return html(ctx, "admin_config.html") @@ -182,8 +178,7 @@ func adminSetConfig(ctx echo.Context) error { } func adminInvitations(ctx echo.Context) error { - setData(ctx, "title", "Invitations") - setData(ctx, "htmlTitle", "Invitations - Admin panel") + setData(ctx, "htmlTitle", trH(ctx, "admin.invitations")+" - "+trH(ctx, "admin.admin_panel")) setData(ctx, "adminHeaderPage", "invitations") var invitations []*db.Invitation @@ -218,7 +213,7 @@ func adminInvitationsCreate(ctx echo.Context) error { return errorRes(500, "Cannot create invitation", err) } - addFlash(ctx, "Invitation has been created", "success") + addFlash(ctx, tr(ctx, "flash.admin.invitation-created"), "success") return redirect(ctx, "/admin-panel/invitations") } @@ -233,6 +228,6 @@ func adminInvitationsDelete(ctx echo.Context) error { return errorRes(500, "Cannot delete this invitation", err) } - addFlash(ctx, "Invitation has been deleted", "success") + addFlash(ctx, tr(ctx, "flash.admin.invitation-deleted"), "success") return redirect(ctx, "/admin-panel/invitations") } diff --git a/internal/web/auth.go b/internal/web/auth.go index 122d677..f36508e 100644 --- a/internal/web/auth.go +++ b/internal/web/auth.go @@ -16,6 +16,7 @@ import ( "github.com/rs/zerolog/log" "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/db" + "github.com/thomiceli/opengist/internal/i18n" "github.com/thomiceli/opengist/internal/utils" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -48,8 +49,8 @@ func register(ctx echo.Context) error { } } - setData(ctx, "title", tr(ctx, "auth.new-account")) - setData(ctx, "htmlTitle", "New account") + setData(ctx, "title", trH(ctx, "auth.new-account")) + setData(ctx, "htmlTitle", trH(ctx, "auth.new-account")) setData(ctx, "disableForm", disableForm) setData(ctx, "disableSignup", disableSignup) setData(ctx, "isLoginPage", false) @@ -68,30 +69,30 @@ func processRegister(ctx echo.Context) error { } if disableSignup == true { - return errorRes(403, "Signing up is disabled", nil) + return errorRes(403, tr(ctx, "error.signup-disabled"), nil) } if getData(ctx, "DisableLoginForm") == true { - return errorRes(403, "Signing up via registration form is disabled", nil) + return errorRes(403, tr(ctx, "error.signup-disabled-form"), nil) } - setData(ctx, "title", "New account") - setData(ctx, "htmlTitle", "New account") + setData(ctx, "title", trH(ctx, "auth.new-account")) + setData(ctx, "htmlTitle", trH(ctx, "auth.new-account")) sess := getSession(ctx) dto := new(db.UserDTO) if err := ctx.Bind(dto); err != nil { - return errorRes(400, "Cannot bind data", err) + return errorRes(400, tr(ctx, "error.cannot-bind-data"), err) } if err := ctx.Validate(dto); err != nil { - addFlash(ctx, utils.ValidationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error") return html(ctx, "auth_form.html") } if exists, err := db.UserExists(dto.Username); err != nil || exists { - addFlash(ctx, "Username already exists", "error") + addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error") return html(ctx, "auth_form.html") } @@ -126,8 +127,8 @@ func processRegister(ctx echo.Context) error { } func login(ctx echo.Context) error { - setData(ctx, "title", tr(ctx, "auth.login")) - setData(ctx, "htmlTitle", "Login") + setData(ctx, "title", trH(ctx, "auth.login")) + setData(ctx, "htmlTitle", trH(ctx, "auth.login")) setData(ctx, "disableForm", getData(ctx, "DisableLoginForm")) setData(ctx, "isLoginPage", true) return html(ctx, "auth_form.html") @@ -135,7 +136,7 @@ func login(ctx echo.Context) error { func processLogin(ctx echo.Context) error { if getData(ctx, "DisableLoginForm") == true { - return errorRes(403, "Logging in via login form is disabled", nil) + return errorRes(403, tr(ctx, "error.login-disabled-form"), nil) } var err error @@ -143,7 +144,7 @@ func processLogin(ctx echo.Context) error { dto := &db.UserDTO{} if err = ctx.Bind(dto); err != nil { - return errorRes(400, "Cannot bind data", err) + return errorRes(400, tr(ctx, "error.cannot-bind-data"), err) } password := dto.Password @@ -154,7 +155,7 @@ func processLogin(ctx echo.Context) error { return errorRes(500, "Cannot get user", err) } log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) - addFlash(ctx, "Invalid credentials", "error") + addFlash(ctx, tr(ctx, "flash.auth.invalid-credentials"), "error") return redirect(ctx, "/login") } @@ -163,7 +164,7 @@ func processLogin(ctx echo.Context) error { return errorRes(500, "Cannot check for password", err) } log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) - addFlash(ctx, "Invalid credentials", "error") + addFlash(ctx, tr(ctx, "flash.auth.invalid-credentials"), "error") return redirect(ctx, "/login") } @@ -178,7 +179,7 @@ func processLogin(ctx echo.Context) error { func oauthCallback(ctx echo.Context) error { user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request()) if err != nil { - return errorRes(400, "Cannot complete user auth: "+err.Error(), err) + return errorRes(400, tr(ctx, "error.complete-oauth-login", err.Error()), err) } currUser := getUserLogged(ctx) @@ -190,7 +191,7 @@ func oauthCallback(ctx echo.Context) error { return errorRes(500, "Cannot update user "+title.String(user.Provider)+" id", err) } - addFlash(ctx, "Account linked to "+title.String(user.Provider), "success") + addFlash(ctx, tr(ctx, "flash.auth.account-linked-oauth", title.String(user.Provider)), "success") return redirect(ctx, "/settings") } @@ -198,7 +199,7 @@ func oauthCallback(ctx echo.Context) error { userDB, err := db.GetUserByProvider(user.UserID, user.Provider) if err != nil { if getData(ctx, "DisableSignup") == true { - return errorRes(403, "Signing up is disabled", nil) + return errorRes(403, tr(ctx, "error.signup-disabled"), nil) } if !errors.Is(err, gorm.ErrRecordNotFound) { @@ -216,7 +217,7 @@ func oauthCallback(ctx echo.Context) error { if err = userDB.Create(); err != nil { if db.IsUniqueConstraintViolation(err) { - addFlash(ctx, "Username "+user.NickName+" already exists in Opengist", "error") + addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error") return redirect(ctx, "/login") } @@ -246,7 +247,7 @@ func oauthCallback(ctx echo.Context) error { body, err := io.ReadAll(resp.Body) if err != nil { - addFlash(ctx, "Could not get user keys", "error") + addFlash(ctx, tr(ctx, "flash.auth.user-sshkeys-not-retrievable"), "error") log.Error().Err(err).Msg("Could not get user keys") } @@ -262,7 +263,7 @@ func oauthCallback(ctx echo.Context) error { } if err = sshKey.Create(); err != nil { - addFlash(ctx, "Could not create ssh key", "error") + addFlash(ctx, tr(ctx, "flash.auth.user-sshkeys-not-created"), "error") log.Error().Err(err).Msg("Could not create ssh key") } } @@ -360,7 +361,7 @@ func oauth(ctx echo.Context) error { return errorRes(500, "Cannot unlink account from "+title.String(provider), err) } - addFlash(ctx, "Account unlinked from "+title.String(provider), "success") + addFlash(ctx, tr(ctx, "flash.auth.account-unlinked-oauth", title.String(provider)), "success") return redirect(ctx, "/settings") } } @@ -368,7 +369,7 @@ func oauth(ctx echo.Context) error { ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider) ctx.SetRequest(ctx.Request().WithContext(ctxValue)) if provider != GitHubProvider && provider != GitLabProvider && provider != GiteaProvider && provider != OpenIDConnect { - return errorRes(400, "Unsupported provider", nil) + return errorRes(400, tr(ctx, "error.oauth-unsupported"), nil) } gothic.BeginAuthHandler(ctx.Response(), ctx.Request()) diff --git a/internal/web/gist.go b/internal/web/gist.go index 024c200..4161d59 100644 --- a/internal/web/gist.go +++ b/internal/web/gist.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/rs/zerolog/log" "github.com/thomiceli/opengist/internal/git" + "github.com/thomiceli/opengist/internal/i18n" "github.com/thomiceli/opengist/internal/index" "github.com/thomiceli/opengist/internal/render" "github.com/thomiceli/opengist/internal/utils" @@ -139,18 +140,18 @@ func allGists(ctx echo.Context) error { pageInt := getPage(ctx) sort := "created" - sortText := tr(ctx, "gist.list.sort-by-created") + sortText := trH(ctx, "gist.list.sort-by-created") order := "desc" - orderText := tr(ctx, "gist.list.order-by-desc") + orderText := trH(ctx, "gist.list.order-by-desc") if ctx.QueryParam("sort") == "updated" { sort = "updated" - sortText = tr(ctx, "gist.list.sort-by-updated") + sortText = trH(ctx, "gist.list.sort-by-updated") } if ctx.QueryParam("order") == "asc" { order = "asc" - orderText = tr(ctx, "gist.list.order-by-asc") + orderText = trH(ctx, "gist.list.order-by-asc") } setData(ctx, "sort", sortText) @@ -167,14 +168,14 @@ func allGists(ctx echo.Context) error { if fromUserStr == "" { urlctx := ctx.Request().URL.Path if strings.HasSuffix(urlctx, "search") { - setData(ctx, "htmlTitle", "Search results") + setData(ctx, "htmlTitle", trH(ctx, "gist.list.search-results")) setData(ctx, "mode", "search") setData(ctx, "searchQuery", ctx.QueryParam("q")) setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q"))) urlPage = "search" gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order) } else if strings.HasSuffix(urlctx, "all") { - setData(ctx, "htmlTitle", "All gists") + setData(ctx, "htmlTitle", trH(ctx, "gist.list.all")) setData(ctx, "mode", "all") urlPage = "all" gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order) @@ -224,17 +225,17 @@ func allGists(ctx echo.Context) error { if liked { urlPage = fromUserStr + "/liked" - setData(ctx, "htmlTitle", "All gists liked by "+fromUserStr) + setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-liked-by", fromUserStr)) setData(ctx, "mode", "liked") gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order) } else if forked { urlPage = fromUserStr + "/forked" - setData(ctx, "htmlTitle", "All gists forked by "+fromUserStr) + setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-forked-by", fromUserStr)) setData(ctx, "mode", "forked") gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order) } else { urlPage = fromUserStr - setData(ctx, "htmlTitle", "All gists from "+fromUserStr) + setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-from", fromUserStr)) setData(ctx, "mode", "fromUser") gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order) } @@ -254,7 +255,7 @@ func allGists(ctx echo.Context) error { } if err = paginate(ctx, renderedGists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil { - return errorRes(404, "Page not found", nil) + return errorRes(404, tr(ctx, "error.page-not-found"), nil) } setData(ctx, "urlPage", urlPage) @@ -312,11 +313,11 @@ func search(ctx echo.Context) error { if 10*pageInt < int(nbHits) { setData(ctx, "nextPage", pageInt+1) } - setData(ctx, "prevLabel", tr(ctx, "pagination.previous")) - setData(ctx, "nextLabel", tr(ctx, "pagination.next")) + setData(ctx, "prevLabel", trH(ctx, "pagination.previous")) + setData(ctx, "nextLabel", trH(ctx, "pagination.next")) setData(ctx, "urlPage", "search") setData(ctx, "urlParams", template.URL("&q="+ctx.QueryParam("q"))) - setData(ctx, "htmlTitle", "Search results") + setData(ctx, "htmlTitle", trH(ctx, "gist.list.search-results")) setData(ctx, "nbHits", nbHits) setData(ctx, "gists", renderedGists) setData(ctx, "langs", langs) @@ -449,7 +450,7 @@ func revisions(ctx echo.Context) error { } if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil { - return errorRes(404, "Page not found", nil) + return errorRes(404, tr(ctx, "error.page-not-found"), nil) } emailsSet := map[string]struct{}{} @@ -468,13 +469,13 @@ func revisions(ctx echo.Context) error { setData(ctx, "page", "revisions") setData(ctx, "revision", "HEAD") setData(ctx, "emails", emailsUsers) - setData(ctx, "htmlTitle", "Revision of "+gist.Title) + setData(ctx, "htmlTitle", trH(ctx, "gist.revision-of", gist.Title)) return html(ctx, "revisions.html") } func create(ctx echo.Context) error { - setData(ctx, "htmlTitle", "Create a new gist") + setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist")) return html(ctx, "create.html") } @@ -486,21 +487,21 @@ func processCreate(ctx echo.Context) error { err := ctx.Request().ParseForm() if err != nil { - return errorRes(400, "Bad request", err) + return errorRes(400, tr(ctx, "error.bad-request"), err) } dto := new(db.GistDTO) var gist *db.Gist if isCreate { - setData(ctx, "htmlTitle", "Create a new gist") + setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist")) } else { gist = getData(ctx, "gist").(*db.Gist) - setData(ctx, "htmlTitle", "Edit "+gist.Title) + setData(ctx, "htmlTitle", trH(ctx, "gist.edit.edit-gist", gist.Title)) } if err := ctx.Bind(dto); err != nil { - return errorRes(400, "Cannot bind data", err) + return errorRes(400, tr(ctx, "error.cannot-bind-data"), err) } dto.Files = make([]db.FileDTO, 0) @@ -516,7 +517,7 @@ func processCreate(ctx echo.Context) error { escapedValue, err := url.QueryUnescape(content) if err != nil { - return errorRes(400, "Invalid character unescaped", err) + return errorRes(400, tr(ctx, "error.invalid-character-unescaped"), err) } dto.Files = append(dto.Files, db.FileDTO{ @@ -527,7 +528,7 @@ func processCreate(ctx echo.Context) error { err = ctx.Validate(dto) if err != nil { - addFlash(ctx, utils.ValidationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error") if isCreate { return html(ctx, "create.html") } else { @@ -610,7 +611,7 @@ func toggleVisibility(ctx echo.Context) error { return errorRes(500, "Error updating this gist", err) } - addFlash(ctx, "Gist visibility has been changed", "success") + addFlash(ctx, tr(ctx, "flash.gist.visibility-changed"), "success") return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier()) } @@ -622,7 +623,7 @@ func deleteGist(ctx echo.Context) error { } gist.RemoveFromIndex() - addFlash(ctx, "Gist has been deleted", "success") + addFlash(ctx, tr(ctx, "flash.gist.deleted"), "success") return redirect(ctx, "/") } @@ -662,7 +663,7 @@ func fork(ctx echo.Context) error { } if gist.User.ID == currentUser.ID { - addFlash(ctx, "Unable to fork own gists", "error") + addFlash(ctx, tr(ctx, "flash.gist.fork-own-gist"), "error") return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier()) } @@ -698,7 +699,7 @@ func fork(ctx echo.Context) error { return errorRes(500, "Error incrementing the fork count", err) } - addFlash(ctx, "Gist has been forked", "success") + addFlash(ctx, tr(ctx, "flash.gist.forked"), "success") return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Identifier()) } @@ -749,7 +750,7 @@ func edit(ctx echo.Context) error { } setData(ctx, "files", files) - setData(ctx, "htmlTitle", "Edit "+gist.Title) + setData(ctx, "htmlTitle", trH(ctx, "gist.edit.edit-gist", gist.Title)) return html(ctx, "edit.html") } @@ -810,10 +811,10 @@ func likes(ctx echo.Context) error { } if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Identifier()+"/likes", 1); err != nil { - return errorRes(404, "Page not found", nil) + return errorRes(404, tr(ctx, "error.page-not-found"), nil) } - setData(ctx, "htmlTitle", "Like for "+gist.Title) + setData(ctx, "htmlTitle", trH(ctx, "gist.likes.for", gist.Title)) setData(ctx, "revision", "HEAD") return html(ctx, "likes.html") } @@ -834,10 +835,10 @@ func forks(ctx echo.Context) error { } if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Identifier()+"/forks", 2); err != nil { - return errorRes(404, "Page not found", nil) + return errorRes(404, tr(ctx, "error.page-not-found"), nil) } - setData(ctx, "htmlTitle", "Forks for "+gist.Title) + setData(ctx, "htmlTitle", trH(ctx, "gist.forks.for: Forks for %s", gist.Title)) setData(ctx, "revision", "HEAD") return html(ctx, "forks.html") } @@ -848,7 +849,7 @@ func checkbox(ctx echo.Context) error { i, err := strconv.Atoi(checkboxNb) if err != nil { - return errorRes(400, "Invalid number", nil) + return errorRes(400, tr(ctx, "error.invalid-number"), nil) } gist := getData(ctx, "gist").(*db.Gist) diff --git a/internal/web/server.go b/internal/web/server.go index 8a04b58..384fe81 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -523,7 +523,7 @@ func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc { require := getData(ctx, "RequireLogin") if require == true { - addFlash(ctx, "You must be logged in to access gists", "error") + addFlash(ctx, tr(ctx, "flash.auth.must-be-logged-in"), "error") return redirect(ctx, "/login") } return next(ctx) diff --git a/internal/web/settings.go b/internal/web/settings.go index d09c284..14d6480 100644 --- a/internal/web/settings.go +++ b/internal/web/settings.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/git" + "github.com/thomiceli/opengist/internal/i18n" "github.com/thomiceli/opengist/internal/utils" "os" "path/filepath" @@ -28,7 +29,7 @@ func userSettings(ctx echo.Context) error { setData(ctx, "email", user.Email) setData(ctx, "sshKeys", keys) setData(ctx, "hasPassword", user.Password != "") - setData(ctx, "htmlTitle", "Settings") + setData(ctx, "htmlTitle", trH(ctx, "settings")) return html(ctx, "settings.html") } @@ -51,7 +52,7 @@ func emailProcess(ctx echo.Context) error { return errorRes(500, "Cannot update email", err) } - addFlash(ctx, "Email updated", "success") + addFlash(ctx, tr(ctx, "flash.user.email-updated"), "success") return redirect(ctx, "/settings") } @@ -70,11 +71,11 @@ func sshKeysProcess(ctx echo.Context) error { dto := new(db.SSHKeyDTO) if err := ctx.Bind(dto); err != nil { - return errorRes(400, "Cannot bind data", err) + return errorRes(400, tr(ctx, "error.cannot-bind-data"), err) } if err := ctx.Validate(dto); err != nil { - addFlash(ctx, utils.ValidationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error") return redirect(ctx, "/settings") } key := dto.ToSSHKey() @@ -83,7 +84,7 @@ func sshKeysProcess(ctx echo.Context) error { pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content)) if err != nil { - addFlash(ctx, "Invalid SSH key", "error") + addFlash(ctx, tr(ctx, "flash.user.invalid-ssh-key"), "error") return redirect(ctx, "/settings") } key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey))) @@ -92,7 +93,7 @@ func sshKeysProcess(ctx echo.Context) error { return errorRes(500, "Cannot add SSH key", err) } - addFlash(ctx, "SSH key added", "success") + addFlash(ctx, tr(ctx, "flash.user.ssh-key-added"), "success") return redirect(ctx, "/settings") } @@ -113,7 +114,7 @@ func sshKeysDelete(ctx echo.Context) error { return errorRes(500, "Cannot delete SSH key", err) } - addFlash(ctx, "SSH key deleted", "success") + addFlash(ctx, tr(ctx, "flash.user.ssh-key-deleted"), "success") return redirect(ctx, "/settings") } @@ -122,12 +123,12 @@ func passwordProcess(ctx echo.Context) error { dto := new(db.UserDTO) if err := ctx.Bind(dto); err != nil { - return errorRes(400, "Cannot bind data", err) + return errorRes(400, tr(ctx, "error.cannot-bind-data"), err) } dto.Username = user.Username if err := ctx.Validate(dto); err != nil { - addFlash(ctx, utils.ValidationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error") return html(ctx, "settings.html") } @@ -141,7 +142,7 @@ func passwordProcess(ctx echo.Context) error { return errorRes(500, "Cannot update password", err) } - addFlash(ctx, "Password updated", "success") + addFlash(ctx, tr(ctx, "flash.user.password-updated"), "success") return redirect(ctx, "/settings") } @@ -150,17 +151,17 @@ func usernameProcess(ctx echo.Context) error { dto := new(db.UserDTO) if err := ctx.Bind(dto); err != nil { - return errorRes(400, "Cannot bind data", err) + return errorRes(400, tr(ctx, "error.cannot-bind-data"), err) } dto.Password = user.Password if err := ctx.Validate(dto); err != nil { - addFlash(ctx, utils.ValidationMessages(&err), "error") + addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error") return redirect(ctx, "/settings") } if exists, err := db.UserExists(dto.Username); err != nil || exists { - addFlash(ctx, "Username already exists", "error") + addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error") return redirect(ctx, "/settings") } @@ -180,6 +181,6 @@ func usernameProcess(ctx echo.Context) error { return errorRes(500, "Cannot update username", err) } - addFlash(ctx, "Username updated", "success") + addFlash(ctx, tr(ctx, "flash.user.username-updated"), "success") return redirect(ctx, "/settings") } diff --git a/internal/web/util.go b/internal/web/util.go index ecc7cd3..3935c41 100644 --- a/internal/web/util.go +++ b/internal/web/util.go @@ -158,11 +158,11 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp switch labels { case 1: - setData(ctx, "prevLabel", tr(ctx, "pagination.previous")) - setData(ctx, "nextLabel", tr(ctx, "pagination.next")) + setData(ctx, "prevLabel", trH(ctx, "pagination.previous")) + setData(ctx, "nextLabel", trH(ctx, "pagination.next")) case 2: - setData(ctx, "prevLabel", tr(ctx, "pagination.newer")) - setData(ctx, "nextLabel", tr(ctx, "pagination.older")) + setData(ctx, "prevLabel", trH(ctx, "pagination.newer")) + setData(ctx, "nextLabel", trH(ctx, "pagination.older")) } setData(ctx, "urlPage", urlPage) @@ -170,9 +170,14 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp return nil } -func tr(ctx echo.Context, key string) template.HTML { +func trH(ctx echo.Context, key string, args ...any) template.HTML { l := getData(ctx, "locale").(*i18n.Locale) - return l.Tr(key) + return l.Tr(key, args...) +} + +func tr(ctx echo.Context, key string, args ...any) string { + l := getData(ctx, "locale").(*i18n.Locale) + return l.String(key, args...) } func parseSearchQueryStr(query string) (string, map[string]string) { diff --git a/public/main.ts b/public/main.ts index ec8de35..e9934aa 100644 --- a/public/main.ts +++ b/public/main.ts @@ -4,10 +4,19 @@ import './opengist.svg'; import './default.png'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; +import 'dayjs/locale/cs'; +import 'dayjs/locale/de'; +import 'dayjs/locale/es'; +import 'dayjs/locale/fr'; +import 'dayjs/locale/hu'; +import 'dayjs/locale/pt'; +import 'dayjs/locale/ru'; +import 'dayjs/locale/zh'; import localizedFormat from 'dayjs/plugin/localizedFormat'; dayjs.extend(relativeTime); dayjs.extend(localizedFormat); +dayjs.locale(window.opengist_locale || 'en'); document.addEventListener('DOMContentLoaded', () => { const themeMenu = document.getElementById('theme-menu')!; diff --git a/templates/base/base_header.html b/templates/base/base_header.html index a77ec19..232014f 100644 --- a/templates/base/base_header.html +++ b/templates/base/base_header.html @@ -11,6 +11,7 @@