Add translation strings (#269)

This commit is contained in:
Thomas Miceli 2024-05-05 00:24:25 +02:00 committed by GitHub
parent 1aa94292db
commit e439d96e43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 194 additions and 107 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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')!;

View file

@ -11,6 +11,7 @@
<script>
window.opengist_base_url = "{{ $.c.ExternalUrl }}";
window.opengist_locale = "{{ .locale.Code }}".substring(0, 2);
const checkTheme = () => {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')