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" 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 { func (l *Locale) Tr(key string, args ...any) template.HTML {
message := l.Messages[key] 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-unlisted-button: Create unlisted gist
gist.new.create-private-button: Create private gist gist.new.create-private-button: Create private gist
gist.new.preview: Preview gist.new.preview: Preview
gist.new.create-a-new-gist: Create a new gist
gist.edit.editing: Editing gist.edit.editing: Editing
gist.edit.edit-gist: Edit %s
gist.edit.change-visibility: Make gist.edit.change-visibility: Make
gist.edit.delete: Delete gist.edit.delete: Delete
gist.edit.cancel: Cancel gist.edit.cancel: Cancel
@ -68,6 +70,9 @@ gist.list.forks: forks
gist.list.files: files gist.list.files: files
gist.list.last-active: Last active gist.list.last-active: Last active
gist.list.no-gists: No gists 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.found: gists found
gist.search.no-results: No 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: Forks
gist.forks.view: View fork gist.forks.view: View fork
gist.forks.no: No public forks gist.forks.no: No public forks
gist.forks.for: Forks for %s
gist.likes: Likes gist.likes: Likes
gist.likes.no: No likes yet gist.likes.no: No likes yet
gist.likes.for: Likes for %s
gist.revisions: Revisions gist.revisions: Revisions
gist.revision.revised: revised this gist 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.empty-file: Empty file
gist.revision.no-changes: No changes gist.revision.no-changes: No changes
gist.revision.no-revisions: No revisions to show gist.revision.no-revisions: No revisions to show
gist.revision-of: Revision of %s
settings: Settings settings: Settings
settings.email: Email settings.email: Email
@ -136,6 +144,16 @@ auth.login-instead: Login instead
auth.oauth: Continue with %s account auth.oauth: Continue with %s account
error: Error 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.all: All
header.menu.new: New header.menu.new: New
@ -205,3 +223,44 @@ admin.invitations.code: Code
admin.invitations.copy_link: Copy link admin.invitations.copy_link: Copy link
admin.invitations.uses: Uses 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 ( import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/thomiceli/opengist/internal/i18n"
"regexp" "regexp"
"strings" "strings"
) )
@ -26,26 +27,26 @@ func (cv *OpengistValidator) Var(field interface{}, tag string) error {
return cv.v.Var(field, tag) return cv.v.Var(field, tag)
} }
func ValidationMessages(err *error) string { func ValidationMessages(err *error, locale *i18n.Locale) string {
errs := (*err).(validator.ValidationErrors) errs := (*err).(validator.ValidationErrors)
messages := make([]string, len(errs)) messages := make([]string, len(errs))
for i, e := range errs { for i, e := range errs {
switch e.Tag() { switch e.Tag() {
case "max": case "max":
messages[i] = e.Field() + " is too long" messages[i] = locale.String("validation.is-too-long", e.Field())
case "required": case "required":
messages[i] = e.Field() + " should not be empty" messages[i] = locale.String("validation.should-not-be-empty", e.Field())
case "excludes": 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": 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 "alphanumdash":
case "alphanumdashorempty": 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": case "min":
messages[i] = "Not enough " + e.Field() messages[i] = locale.String("validation.not-enough", e.Field())
case "notreserved": 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 { func adminIndex(ctx echo.Context) error {
setData(ctx, "title", "Admin panel") setData(ctx, "htmlTitle", trH(ctx, "admin.admin_panel"))
setData(ctx, "htmlTitle", "Admin panel")
setData(ctx, "adminHeaderPage", "index") setData(ctx, "adminHeaderPage", "index")
setData(ctx, "opengistVersion", config.OpengistVersion) setData(ctx, "opengistVersion", config.OpengistVersion)
@ -52,8 +51,7 @@ func adminIndex(ctx echo.Context) error {
} }
func adminUsers(ctx echo.Context) error { func adminUsers(ctx echo.Context) error {
setData(ctx, "title", "Users") setData(ctx, "htmlTitle", trH(ctx, "admin.users")+" - "+trH(ctx, "admin.admin_panel"))
setData(ctx, "htmlTitle", "Users - Admin panel")
setData(ctx, "adminHeaderPage", "users") setData(ctx, "adminHeaderPage", "users")
pageInt := getPage(ctx) 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 { 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") return html(ctx, "admin_users.html")
} }
func adminGists(ctx echo.Context) error { func adminGists(ctx echo.Context) error {
setData(ctx, "title", "Gists") setData(ctx, "htmlTitle", trH(ctx, "admin.gists")+" - "+trH(ctx, "admin.admin_panel"))
setData(ctx, "htmlTitle", "Gists - Admin panel")
setData(ctx, "adminHeaderPage", "gists") setData(ctx, "adminHeaderPage", "gists")
pageInt := getPage(ctx) 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 { 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") return html(ctx, "admin_gists.html")
@ -100,7 +97,7 @@ func adminUserDelete(ctx echo.Context) error {
return errorRes(500, "Cannot delete this user", err) 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") return redirect(ctx, "/admin-panel/users")
} }
@ -120,49 +117,48 @@ func adminGistDelete(ctx echo.Context) error {
gist.RemoveFromIndex() gist.RemoveFromIndex()
addFlash(ctx, "Gist has been deleted", "success") addFlash(ctx, tr(ctx, "flash.admin.gist-deleted"), "success")
return redirect(ctx, "/admin-panel/gists") return redirect(ctx, "/admin-panel/gists")
} }
func adminSyncReposFromFS(ctx echo.Context) error { 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) go actions.Run(actions.SyncReposFromFS)
return redirect(ctx, "/admin-panel") return redirect(ctx, "/admin-panel")
} }
func adminSyncReposFromDB(ctx echo.Context) error { 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) go actions.Run(actions.SyncReposFromDB)
return redirect(ctx, "/admin-panel") return redirect(ctx, "/admin-panel")
} }
func adminGcRepos(ctx echo.Context) error { 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) go actions.Run(actions.GitGcRepos)
return redirect(ctx, "/admin-panel") return redirect(ctx, "/admin-panel")
} }
func adminSyncGistPreviews(ctx echo.Context) error { 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) go actions.Run(actions.SyncGistPreviews)
return redirect(ctx, "/admin-panel") return redirect(ctx, "/admin-panel")
} }
func adminResetHooks(ctx echo.Context) error { 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) go actions.Run(actions.ResetHooks)
return redirect(ctx, "/admin-panel") return redirect(ctx, "/admin-panel")
} }
func adminIndexGists(ctx echo.Context) error { 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) go actions.Run(actions.IndexGists)
return redirect(ctx, "/admin-panel") return redirect(ctx, "/admin-panel")
} }
func adminConfig(ctx echo.Context) error { func adminConfig(ctx echo.Context) error {
setData(ctx, "title", "Configuration") setData(ctx, "htmlTitle", trH(ctx, "admin.configuration")+" - "+trH(ctx, "admin.admin_panel"))
setData(ctx, "htmlTitle", "Configuration - Admin panel")
setData(ctx, "adminHeaderPage", "config") setData(ctx, "adminHeaderPage", "config")
return html(ctx, "admin_config.html") return html(ctx, "admin_config.html")
@ -182,8 +178,7 @@ func adminSetConfig(ctx echo.Context) error {
} }
func adminInvitations(ctx echo.Context) error { func adminInvitations(ctx echo.Context) error {
setData(ctx, "title", "Invitations") setData(ctx, "htmlTitle", trH(ctx, "admin.invitations")+" - "+trH(ctx, "admin.admin_panel"))
setData(ctx, "htmlTitle", "Invitations - Admin panel")
setData(ctx, "adminHeaderPage", "invitations") setData(ctx, "adminHeaderPage", "invitations")
var invitations []*db.Invitation var invitations []*db.Invitation
@ -218,7 +213,7 @@ func adminInvitationsCreate(ctx echo.Context) error {
return errorRes(500, "Cannot create invitation", err) 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") return redirect(ctx, "/admin-panel/invitations")
} }
@ -233,6 +228,6 @@ func adminInvitationsDelete(ctx echo.Context) error {
return errorRes(500, "Cannot delete this invitation", err) 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") return redirect(ctx, "/admin-panel/invitations")
} }

View file

@ -16,6 +16,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/utils" "github.com/thomiceli/opengist/internal/utils"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "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, "title", trH(ctx, "auth.new-account"))
setData(ctx, "htmlTitle", "New account") setData(ctx, "htmlTitle", trH(ctx, "auth.new-account"))
setData(ctx, "disableForm", disableForm) setData(ctx, "disableForm", disableForm)
setData(ctx, "disableSignup", disableSignup) setData(ctx, "disableSignup", disableSignup)
setData(ctx, "isLoginPage", false) setData(ctx, "isLoginPage", false)
@ -68,30 +69,30 @@ func processRegister(ctx echo.Context) error {
} }
if disableSignup == true { 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 { 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, "title", trH(ctx, "auth.new-account"))
setData(ctx, "htmlTitle", "New account") setData(ctx, "htmlTitle", trH(ctx, "auth.new-account"))
sess := getSession(ctx) sess := getSession(ctx)
dto := new(db.UserDTO) dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil { 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 { 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") return html(ctx, "auth_form.html")
} }
if exists, err := db.UserExists(dto.Username); err != nil || exists { 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") return html(ctx, "auth_form.html")
} }
@ -126,8 +127,8 @@ func processRegister(ctx echo.Context) error {
} }
func login(ctx echo.Context) error { func login(ctx echo.Context) error {
setData(ctx, "title", tr(ctx, "auth.login")) setData(ctx, "title", trH(ctx, "auth.login"))
setData(ctx, "htmlTitle", "Login") setData(ctx, "htmlTitle", trH(ctx, "auth.login"))
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm")) setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
setData(ctx, "isLoginPage", true) setData(ctx, "isLoginPage", true)
return html(ctx, "auth_form.html") return html(ctx, "auth_form.html")
@ -135,7 +136,7 @@ func login(ctx echo.Context) error {
func processLogin(ctx echo.Context) error { func processLogin(ctx echo.Context) error {
if getData(ctx, "DisableLoginForm") == true { 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 var err error
@ -143,7 +144,7 @@ func processLogin(ctx echo.Context) error {
dto := &db.UserDTO{} dto := &db.UserDTO{}
if err = ctx.Bind(dto); err != nil { 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 password := dto.Password
@ -154,7 +155,7 @@ func processLogin(ctx echo.Context) error {
return errorRes(500, "Cannot get user", err) return errorRes(500, "Cannot get user", err)
} }
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) 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") return redirect(ctx, "/login")
} }
@ -163,7 +164,7 @@ func processLogin(ctx echo.Context) error {
return errorRes(500, "Cannot check for password", err) return errorRes(500, "Cannot check for password", err)
} }
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) 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") return redirect(ctx, "/login")
} }
@ -178,7 +179,7 @@ func processLogin(ctx echo.Context) error {
func oauthCallback(ctx echo.Context) error { func oauthCallback(ctx echo.Context) error {
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request()) user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
if err != nil { 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) 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) 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") return redirect(ctx, "/settings")
} }
@ -198,7 +199,7 @@ func oauthCallback(ctx echo.Context) error {
userDB, err := db.GetUserByProvider(user.UserID, user.Provider) userDB, err := db.GetUserByProvider(user.UserID, user.Provider)
if err != nil { if err != nil {
if getData(ctx, "DisableSignup") == true { 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) { if !errors.Is(err, gorm.ErrRecordNotFound) {
@ -216,7 +217,7 @@ func oauthCallback(ctx echo.Context) error {
if err = userDB.Create(); err != nil { if err = userDB.Create(); err != nil {
if db.IsUniqueConstraintViolation(err) { 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") return redirect(ctx, "/login")
} }
@ -246,7 +247,7 @@ func oauthCallback(ctx echo.Context) error {
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { 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") 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 { 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") 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) 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") return redirect(ctx, "/settings")
} }
} }
@ -368,7 +369,7 @@ func oauth(ctx echo.Context) error {
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider) ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
ctx.SetRequest(ctx.Request().WithContext(ctxValue)) ctx.SetRequest(ctx.Request().WithContext(ctxValue))
if provider != GitHubProvider && provider != GitLabProvider && provider != GiteaProvider && provider != OpenIDConnect { 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()) gothic.BeginAuthHandler(ctx.Response(), ctx.Request())

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/index" "github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/render" "github.com/thomiceli/opengist/internal/render"
"github.com/thomiceli/opengist/internal/utils" "github.com/thomiceli/opengist/internal/utils"
@ -139,18 +140,18 @@ func allGists(ctx echo.Context) error {
pageInt := getPage(ctx) pageInt := getPage(ctx)
sort := "created" sort := "created"
sortText := tr(ctx, "gist.list.sort-by-created") sortText := trH(ctx, "gist.list.sort-by-created")
order := "desc" order := "desc"
orderText := tr(ctx, "gist.list.order-by-desc") orderText := trH(ctx, "gist.list.order-by-desc")
if ctx.QueryParam("sort") == "updated" { if ctx.QueryParam("sort") == "updated" {
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" { if ctx.QueryParam("order") == "asc" {
order = "asc" order = "asc"
orderText = tr(ctx, "gist.list.order-by-asc") orderText = trH(ctx, "gist.list.order-by-asc")
} }
setData(ctx, "sort", sortText) setData(ctx, "sort", sortText)
@ -167,14 +168,14 @@ func allGists(ctx echo.Context) error {
if fromUserStr == "" { if fromUserStr == "" {
urlctx := ctx.Request().URL.Path urlctx := ctx.Request().URL.Path
if strings.HasSuffix(urlctx, "search") { 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, "mode", "search")
setData(ctx, "searchQuery", ctx.QueryParam("q")) setData(ctx, "searchQuery", ctx.QueryParam("q"))
setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q"))) setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
urlPage = "search" urlPage = "search"
gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order) gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
} else if strings.HasSuffix(urlctx, "all") { } else if strings.HasSuffix(urlctx, "all") {
setData(ctx, "htmlTitle", "All gists") setData(ctx, "htmlTitle", trH(ctx, "gist.list.all"))
setData(ctx, "mode", "all") setData(ctx, "mode", "all")
urlPage = "all" urlPage = "all"
gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order) gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
@ -224,17 +225,17 @@ func allGists(ctx echo.Context) error {
if liked { if liked {
urlPage = fromUserStr + "/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") setData(ctx, "mode", "liked")
gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order) gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
} else if forked { } else if forked {
urlPage = fromUserStr + "/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") setData(ctx, "mode", "forked")
gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order) gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
} else { } else {
urlPage = fromUserStr urlPage = fromUserStr
setData(ctx, "htmlTitle", "All gists from "+fromUserStr) setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-from", fromUserStr))
setData(ctx, "mode", "fromUser") setData(ctx, "mode", "fromUser")
gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order) 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 { 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) setData(ctx, "urlPage", urlPage)
@ -312,11 +313,11 @@ func search(ctx echo.Context) error {
if 10*pageInt < int(nbHits) { if 10*pageInt < int(nbHits) {
setData(ctx, "nextPage", pageInt+1) setData(ctx, "nextPage", pageInt+1)
} }
setData(ctx, "prevLabel", tr(ctx, "pagination.previous")) setData(ctx, "prevLabel", trH(ctx, "pagination.previous"))
setData(ctx, "nextLabel", tr(ctx, "pagination.next")) setData(ctx, "nextLabel", trH(ctx, "pagination.next"))
setData(ctx, "urlPage", "search") setData(ctx, "urlPage", "search")
setData(ctx, "urlParams", template.URL("&q="+ctx.QueryParam("q"))) 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, "nbHits", nbHits)
setData(ctx, "gists", renderedGists) setData(ctx, "gists", renderedGists)
setData(ctx, "langs", langs) 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 { 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{}{} emailsSet := map[string]struct{}{}
@ -468,13 +469,13 @@ func revisions(ctx echo.Context) error {
setData(ctx, "page", "revisions") setData(ctx, "page", "revisions")
setData(ctx, "revision", "HEAD") setData(ctx, "revision", "HEAD")
setData(ctx, "emails", emailsUsers) 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") return html(ctx, "revisions.html")
} }
func create(ctx echo.Context) error { 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") return html(ctx, "create.html")
} }
@ -486,21 +487,21 @@ func processCreate(ctx echo.Context) error {
err := ctx.Request().ParseForm() err := ctx.Request().ParseForm()
if err != nil { if err != nil {
return errorRes(400, "Bad request", err) return errorRes(400, tr(ctx, "error.bad-request"), err)
} }
dto := new(db.GistDTO) dto := new(db.GistDTO)
var gist *db.Gist var gist *db.Gist
if isCreate { if isCreate {
setData(ctx, "htmlTitle", "Create a new gist") setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist"))
} else { } else {
gist = getData(ctx, "gist").(*db.Gist) 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 { 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) dto.Files = make([]db.FileDTO, 0)
@ -516,7 +517,7 @@ func processCreate(ctx echo.Context) error {
escapedValue, err := url.QueryUnescape(content) escapedValue, err := url.QueryUnescape(content)
if err != nil { 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{ dto.Files = append(dto.Files, db.FileDTO{
@ -527,7 +528,7 @@ func processCreate(ctx echo.Context) error {
err = ctx.Validate(dto) err = ctx.Validate(dto)
if err != nil { if err != nil {
addFlash(ctx, utils.ValidationMessages(&err), "error") addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
if isCreate { if isCreate {
return html(ctx, "create.html") return html(ctx, "create.html")
} else { } else {
@ -610,7 +611,7 @@ func toggleVisibility(ctx echo.Context) error {
return errorRes(500, "Error updating this gist", err) 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()) return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
} }
@ -622,7 +623,7 @@ func deleteGist(ctx echo.Context) error {
} }
gist.RemoveFromIndex() gist.RemoveFromIndex()
addFlash(ctx, "Gist has been deleted", "success") addFlash(ctx, tr(ctx, "flash.gist.deleted"), "success")
return redirect(ctx, "/") return redirect(ctx, "/")
} }
@ -662,7 +663,7 @@ func fork(ctx echo.Context) error {
} }
if gist.User.ID == currentUser.ID { 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()) 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) 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()) return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Identifier())
} }
@ -749,7 +750,7 @@ func edit(ctx echo.Context) error {
} }
setData(ctx, "files", files) 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") 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 { 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") setData(ctx, "revision", "HEAD")
return html(ctx, "likes.html") 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 { 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") setData(ctx, "revision", "HEAD")
return html(ctx, "forks.html") return html(ctx, "forks.html")
} }
@ -848,7 +849,7 @@ func checkbox(ctx echo.Context) error {
i, err := strconv.Atoi(checkboxNb) i, err := strconv.Atoi(checkboxNb)
if err != nil { if err != nil {
return errorRes(400, "Invalid number", nil) return errorRes(400, tr(ctx, "error.invalid-number"), nil)
} }
gist := getData(ctx, "gist").(*db.Gist) gist := getData(ctx, "gist").(*db.Gist)

View file

@ -523,7 +523,7 @@ func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
require := getData(ctx, "RequireLogin") require := getData(ctx, "RequireLogin")
if require == true { 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 redirect(ctx, "/login")
} }
return next(ctx) return next(ctx)

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/utils" "github.com/thomiceli/opengist/internal/utils"
"os" "os"
"path/filepath" "path/filepath"
@ -28,7 +29,7 @@ func userSettings(ctx echo.Context) error {
setData(ctx, "email", user.Email) setData(ctx, "email", user.Email)
setData(ctx, "sshKeys", keys) setData(ctx, "sshKeys", keys)
setData(ctx, "hasPassword", user.Password != "") setData(ctx, "hasPassword", user.Password != "")
setData(ctx, "htmlTitle", "Settings") setData(ctx, "htmlTitle", trH(ctx, "settings"))
return html(ctx, "settings.html") return html(ctx, "settings.html")
} }
@ -51,7 +52,7 @@ func emailProcess(ctx echo.Context) error {
return errorRes(500, "Cannot update email", err) 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") return redirect(ctx, "/settings")
} }
@ -70,11 +71,11 @@ func sshKeysProcess(ctx echo.Context) error {
dto := new(db.SSHKeyDTO) dto := new(db.SSHKeyDTO)
if err := ctx.Bind(dto); err != nil { 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 { 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") return redirect(ctx, "/settings")
} }
key := dto.ToSSHKey() key := dto.ToSSHKey()
@ -83,7 +84,7 @@ func sshKeysProcess(ctx echo.Context) error {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content)) pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
if err != nil { if err != nil {
addFlash(ctx, "Invalid SSH key", "error") addFlash(ctx, tr(ctx, "flash.user.invalid-ssh-key"), "error")
return redirect(ctx, "/settings") return redirect(ctx, "/settings")
} }
key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey))) 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) 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") return redirect(ctx, "/settings")
} }
@ -113,7 +114,7 @@ func sshKeysDelete(ctx echo.Context) error {
return errorRes(500, "Cannot delete SSH key", err) 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") return redirect(ctx, "/settings")
} }
@ -122,12 +123,12 @@ func passwordProcess(ctx echo.Context) error {
dto := new(db.UserDTO) dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil { 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 dto.Username = user.Username
if err := ctx.Validate(dto); err != nil { 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") return html(ctx, "settings.html")
} }
@ -141,7 +142,7 @@ func passwordProcess(ctx echo.Context) error {
return errorRes(500, "Cannot update password", err) 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") return redirect(ctx, "/settings")
} }
@ -150,17 +151,17 @@ func usernameProcess(ctx echo.Context) error {
dto := new(db.UserDTO) dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil { 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 dto.Password = user.Password
if err := ctx.Validate(dto); err != nil { 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") return redirect(ctx, "/settings")
} }
if exists, err := db.UserExists(dto.Username); err != nil || exists { 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") return redirect(ctx, "/settings")
} }
@ -180,6 +181,6 @@ func usernameProcess(ctx echo.Context) error {
return errorRes(500, "Cannot update username", err) 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") 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 { switch labels {
case 1: case 1:
setData(ctx, "prevLabel", tr(ctx, "pagination.previous")) setData(ctx, "prevLabel", trH(ctx, "pagination.previous"))
setData(ctx, "nextLabel", tr(ctx, "pagination.next")) setData(ctx, "nextLabel", trH(ctx, "pagination.next"))
case 2: case 2:
setData(ctx, "prevLabel", tr(ctx, "pagination.newer")) setData(ctx, "prevLabel", trH(ctx, "pagination.newer"))
setData(ctx, "nextLabel", tr(ctx, "pagination.older")) setData(ctx, "nextLabel", trH(ctx, "pagination.older"))
} }
setData(ctx, "urlPage", urlPage) setData(ctx, "urlPage", urlPage)
@ -170,9 +170,14 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp
return nil 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) 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) { func parseSearchQueryStr(query string) (string, map[string]string) {

View file

@ -4,10 +4,19 @@ import './opengist.svg';
import './default.png'; import './default.png';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime'; 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'; import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
dayjs.locale(window.opengist_locale || 'en');
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const themeMenu = document.getElementById('theme-menu')!; const themeMenu = document.getElementById('theme-menu')!;

View file

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