Initial i18n support

This commit is contained in:
Evan Goode 2025-07-06 15:10:21 -04:00
parent ecd485cc1d
commit 38d533581b
9 changed files with 145 additions and 132 deletions

View File

@ -23,7 +23,7 @@ install: build
install -Dm 755 drasl "$(prefix)/bin/drasl" install -Dm 755 drasl "$(prefix)/bin/drasl"
install -Dm 644 LICENSE "$(prefix)/share/licenses/drasl/LICENSE" install -Dm 644 LICENSE "$(prefix)/share/licenses/drasl/LICENSE"
mkdir -p "$(prefix)/share/drasl/" mkdir -p "$(prefix)/share/drasl/"
cp -R assets view public "$(prefix)/share/drasl/" cp -R assets view public locales "$(prefix)/share/drasl/"
clean: clean:
rm -f drasl rm -f drasl

178
front.go
View File

@ -10,9 +10,11 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jxskiss/base62" "github.com/jxskiss/base62"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/leonelquinteros/gotext"
"github.com/samber/mo" "github.com/samber/mo"
"github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/text/language"
"gorm.io/gorm" "gorm.io/gorm"
"html/template" "html/template"
"io" "io"
@ -28,6 +30,7 @@ import (
Web front end for creating user accounts, changing passwords, skins, player names, etc. Web front end for creating user accounts, changing passwords, skins, player names, etc.
*/ */
const CONTEXT_KEY_LOCALE = "DraslLocale"
const BROWSER_TOKEN_AGE_SEC = 24 * 60 * 60 const BROWSER_TOKEN_AGE_SEC = 24 * 60 * 60
const COOKIE_PREFIX = "__Host-" const COOKIE_PREFIX = "__Host-"
const BROWSER_TOKEN_COOKIE_NAME = COOKIE_PREFIX + "browserToken" const BROWSER_TOKEN_COOKIE_NAME = COOKIE_PREFIX + "browserToken"
@ -82,6 +85,21 @@ func NewTemplate(app *App) *Template {
return t return t
} }
func (app *App) GetLanguageMiddleware() func(echo.HandlerFunc) echo.HandlerFunc {
matcher := language.NewMatcher(app.LocaleTags)
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
header := c.Request().Header.Get("Accept-Language")
t, _, _ := language.ParseAcceptLanguage(header)
// Use only the returned index, not the returned tag: https://github.com/golang/go/issues/24211
_, localeTagIndex, _ := matcher.Match(t...)
l := app.Locales[app.LocaleTags[localeTagIndex]]
c.Set(CONTEXT_KEY_LOCALE, l)
return next(c)
}
}
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.Templates[name].ExecuteTemplate(w, "base", data) return t.Templates[name].ExecuteTemplate(w, "base", data)
} }
@ -138,15 +156,31 @@ func NewWebError(returnURL string, message string, args ...interface{}) error {
} }
} }
type errorContext struct { type baseContext struct {
App *App App *App
User *User L *gotext.Locale
URL string URL string
SuccessMessage string SuccessMessage string
WarningMessage string WarningMessage string
ErrorMessage string ErrorMessage string
Message string }
StatusCode int
func (app *App) NewBaseContext(c *echo.Context) baseContext {
return baseContext{
App: app,
L: (*c).Get(CONTEXT_KEY_LOCALE).(*gotext.Locale),
URL: (*c).Request().URL.RequestURI(),
SuccessMessage: app.lastSuccessMessage(c),
WarningMessage: app.lastWarningMessage(c),
ErrorMessage: app.lastErrorMessage(c),
}
}
type errorContext struct {
baseContext
User *User
Message string
StatusCode int
} }
// Set error message and redirect // Set error message and redirect
@ -182,14 +216,10 @@ func (app *App) HandleWebError(err error, c *echo.Context) error {
} }
if Contains(safeMethods, (*c).Request().Method) { if Contains(safeMethods, (*c).Request().Method) {
return (*c).Render(code, "error", errorContext{ return (*c).Render(code, "error", errorContext{
App: app, baseContext: app.NewBaseContext(c),
User: nil, User: nil,
URL: (*c).Request().URL.RequestURI(), Message: message,
Message: message, StatusCode: code,
SuccessMessage: app.lastSuccessMessage(c),
WarningMessage: app.lastWarningMessage(c),
ErrorMessage: app.lastErrorMessage(c),
StatusCode: code,
}) })
} else { } else {
returnURL := getReturnURL(app, c) returnURL := getReturnURL(app, c)
@ -301,13 +331,9 @@ func EncodeOIDCState(state oidcState) (string, error) {
// GET / // GET /
func FrontRoot(app *App) func(c echo.Context) error { func FrontRoot(app *App) func(c echo.Context) error {
type rootContext struct { type rootContext struct {
App *App baseContext
User *User User *User
URL string
Destination string Destination string
SuccessMessage string
WarningMessage string
ErrorMessage string
WebOIDCProviders []webOIDCProvider WebOIDCProviders []webOIDCProvider
} }
@ -348,13 +374,9 @@ func FrontRoot(app *App) func(c echo.Context) error {
} }
return c.Render(http.StatusOK, "root", rootContext{ return c.Render(http.StatusOK, "root", rootContext{
App: app, baseContext: app.NewBaseContext(&c),
User: user, User: user,
URL: c.Request().URL.RequestURI(),
Destination: destination, Destination: destination,
SuccessMessage: app.lastSuccessMessage(&c),
WarningMessage: app.lastWarningMessage(&c),
ErrorMessage: app.lastErrorMessage(&c),
WebOIDCProviders: webOIDCProviders, WebOIDCProviders: webOIDCProviders,
}) })
}) })
@ -408,12 +430,8 @@ type oidcState struct {
// GET /registration // GET /registration
func FrontRegistration(app *App) func(c echo.Context) error { func FrontRegistration(app *App) func(c echo.Context) error {
type registrationContext struct { type registrationContext struct {
App *App baseContext
User *User User *User
URL string
SuccessMessage string
WarningMessage string
ErrorMessage string
InviteCode string InviteCode string
WebOIDCProviders []webOIDCProvider WebOIDCProviders []webOIDCProvider
} }
@ -454,12 +472,8 @@ func FrontRegistration(app *App) func(c echo.Context) error {
} }
return c.Render(http.StatusOK, "registration", registrationContext{ return c.Render(http.StatusOK, "registration", registrationContext{
App: app, baseContext: app.NewBaseContext(&c),
User: user, User: user,
URL: c.Request().URL.RequestURI(),
SuccessMessage: app.lastSuccessMessage(&c),
WarningMessage: app.lastWarningMessage(&c),
ErrorMessage: app.lastErrorMessage(&c),
InviteCode: inviteCode, InviteCode: inviteCode,
WebOIDCProviders: webOIDCProviders, WebOIDCProviders: webOIDCProviders,
}) })
@ -502,12 +516,8 @@ func (app *App) getIDTokenCookie(c *echo.Context) (*OIDCProvider, string, oidc.I
func FrontCompleteRegistration(app *App) func(c echo.Context) error { func FrontCompleteRegistration(app *App) func(c echo.Context) error {
type completeRegistrationContext struct { type completeRegistrationContext struct {
App *App baseContext
User *User User *User
URL string
SuccessMessage string
WarningMessage string
ErrorMessage string
InviteCode string InviteCode string
AnyUnmigratedUsers bool AnyUnmigratedUsers bool
AllowChoosingPlayerName bool AllowChoosingPlayerName bool
@ -547,12 +557,8 @@ func FrontCompleteRegistration(app *App) func(c echo.Context) error {
} }
return c.Render(http.StatusOK, "complete-registration", completeRegistrationContext{ return c.Render(http.StatusOK, "complete-registration", completeRegistrationContext{
App: app, baseContext: app.NewBaseContext(&c),
User: user, User: user,
URL: c.Request().URL.RequestURI(),
SuccessMessage: app.lastSuccessMessage(&c),
WarningMessage: app.lastWarningMessage(&c),
ErrorMessage: app.lastErrorMessage(&c),
InviteCode: inviteCode, InviteCode: inviteCode,
PreferredPlayerName: preferredPlayerName, PreferredPlayerName: preferredPlayerName,
AllowChoosingPlayerName: provider.Config.AllowChoosingPlayerName, AllowChoosingPlayerName: provider.Config.AllowChoosingPlayerName,
@ -775,14 +781,10 @@ func FrontOIDCCallback(app *App) func(c echo.Context) error {
// GET /web/admin // GET /web/admin
func FrontAdmin(app *App) func(c echo.Context) error { func FrontAdmin(app *App) func(c echo.Context) error {
type adminContext struct { type adminContext struct {
App *App baseContext
User *User User *User
URL string Users []User
SuccessMessage string Invites []Invite
WarningMessage string
ErrorMessage string
Users []User
Invites []Invite
} }
return withBrowserAdmin(app, func(c echo.Context, user *User) error { return withBrowserAdmin(app, func(c echo.Context, user *User) error {
@ -799,14 +801,10 @@ func FrontAdmin(app *App) func(c echo.Context) error {
} }
return c.Render(http.StatusOK, "admin", adminContext{ return c.Render(http.StatusOK, "admin", adminContext{
App: app, baseContext: app.NewBaseContext(&c),
User: user, User: user,
URL: c.Request().URL.RequestURI(), Users: users,
SuccessMessage: app.lastSuccessMessage(&c), Invites: invites,
WarningMessage: app.lastWarningMessage(&c),
ErrorMessage: app.lastErrorMessage(&c),
Users: users,
Invites: invites,
}) })
}) })
} }
@ -925,12 +923,8 @@ func FrontNewInvite(app *App) func(c echo.Context) error {
// GET /web/user/:uuid // GET /web/user/:uuid
func FrontUser(app *App) func(c echo.Context) error { func FrontUser(app *App) func(c echo.Context) error {
type userContext struct { type userContext struct {
App *App baseContext
User *User User *User
URL string
SuccessMessage string
WarningMessage string
ErrorMessage string
TargetUser *User TargetUser *User
TargetUserID string TargetUserID string
SkinURL *string SkinURL *string
@ -1014,12 +1008,8 @@ func FrontUser(app *App) func(c echo.Context) error {
} }
return c.Render(http.StatusOK, "user", userContext{ return c.Render(http.StatusOK, "user", userContext{
App: app, baseContext: app.NewBaseContext(&c),
User: user, User: user,
URL: c.Request().URL.RequestURI(),
SuccessMessage: app.lastSuccessMessage(&c),
WarningMessage: app.lastWarningMessage(&c),
ErrorMessage: app.lastErrorMessage(&c),
TargetUser: targetUser, TargetUser: targetUser,
AdminView: adminView, AdminView: adminView,
LinkedOIDCProviderNames: linkedOIDCProviderNames.ToSlice(), LinkedOIDCProviderNames: linkedOIDCProviderNames.ToSlice(),
@ -1032,18 +1022,14 @@ func FrontUser(app *App) func(c echo.Context) error {
// GET /web/player/:uuid // GET /web/player/:uuid
func FrontPlayer(app *App) func(c echo.Context) error { func FrontPlayer(app *App) func(c echo.Context) error {
type playerContext struct { type playerContext struct {
App *App baseContext
User *User User *User
URL string PlayerUser *User
SuccessMessage string Player *Player
WarningMessage string PlayerID string
ErrorMessage string SkinURL *string
PlayerUser *User CapeURL *string
Player *Player AdminView bool
PlayerID string
SkinURL *string
CapeURL *string
AdminView bool
} }
return withBrowserAuthentication(app, true, func(c echo.Context, user *User) error { return withBrowserAuthentication(app, true, func(c echo.Context, user *User) error {
@ -1081,18 +1067,14 @@ func FrontPlayer(app *App) func(c echo.Context) error {
} }
return c.Render(http.StatusOK, "player", playerContext{ return c.Render(http.StatusOK, "player", playerContext{
App: app, baseContext: app.NewBaseContext(&c),
User: user, User: user,
URL: c.Request().URL.RequestURI(), PlayerUser: &playerUser,
SuccessMessage: app.lastSuccessMessage(&c), Player: &player,
WarningMessage: app.lastWarningMessage(&c), PlayerID: id,
ErrorMessage: app.lastErrorMessage(&c), SkinURL: skinURL,
PlayerUser: &playerUser, CapeURL: capeURL,
Player: &player, AdminView: adminView,
PlayerID: id,
SkinURL: skinURL,
CapeURL: capeURL,
AdminView: adminView,
}) })
}) })
} }
@ -1283,12 +1265,8 @@ func FrontRegisterChallenge(app *App) func(c echo.Context) error {
func frontChallenge(app *App, action string) func(c echo.Context) error { func frontChallenge(app *App, action string) func(c echo.Context) error {
type challengeContext struct { type challengeContext struct {
App *App baseContext
User *User User *User
URL string
SuccessMessage string
WarningMessage string
ErrorMessage string
PlayerName string PlayerName string
RegistrationProvider string RegistrationProvider string
SkinBase64 string SkinBase64 string
@ -1372,12 +1350,8 @@ func frontChallenge(app *App, action string) func(c echo.Context) error {
skinBase64 := base64.StdEncoding.EncodeToString(challengeSkinBytes) skinBase64 := base64.StdEncoding.EncodeToString(challengeSkinBytes)
return c.Render(http.StatusOK, "challenge", challengeContext{ return c.Render(http.StatusOK, "challenge", challengeContext{
App: app, baseContext: app.NewBaseContext(&c),
User: user, User: user,
URL: c.Request().URL.RequestURI(),
SuccessMessage: app.lastSuccessMessage(&c),
WarningMessage: app.lastWarningMessage(&c),
ErrorMessage: app.lastErrorMessage(&c),
PlayerName: playerName, PlayerName: playerName,
SkinBase64: skinBase64, SkinBase64: skinBase64,
SkinFilename: playerName + "-challenge.png", SkinFilename: playerName + "-challenge.png",

5
go.mod
View File

@ -1,8 +1,8 @@
module unmojang.org/drasl module unmojang.org/drasl
go 1.23.0 go 1.23.5
toolchain go1.23.2 toolchain go1.23.10
require ( require (
github.com/BurntSushi/toml v1.3.2 github.com/BurntSushi/toml v1.3.2
@ -38,6 +38,7 @@ require (
github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/leonelquinteros/gotext v1.7.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.18 // indirect github.com/mattn/go-sqlite3 v1.14.18 // indirect

2
go.sum
View File

@ -64,6 +64,8 @@ github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zG
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=

0
locales/en-US/default.po Normal file
View File

7
locales/es/default.po Normal file
View File

@ -0,0 +1,7 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language: es\n"
msgid "Register"
msgstr "Registrarse"

79
main.go
View File

@ -13,8 +13,10 @@ import (
"github.com/dgraph-io/ristretto" "github.com/dgraph-io/ristretto"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
"github.com/leonelquinteros/gotext"
"github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v3/pkg/http"
"golang.org/x/text/language"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"gorm.io/gorm" "gorm.io/gorm"
"image" "image"
@ -25,6 +27,7 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -71,6 +74,8 @@ type App struct {
OIDCProvidersByName map[string]*OIDCProvider OIDCProvidersByName map[string]*OIDCProvider
OIDCProvidersByIssuer map[string]*OIDCProvider OIDCProvidersByIssuer map[string]*OIDCProvider
FallbackAPIServers []FallbackAPIServer FallbackAPIServers []FallbackAPIServer
Locales map[language.Tag]*gotext.Locale
LocaleTags []language.Tag
} }
func LogInfo(args ...interface{}) { func LogInfo(args ...interface{}) {
@ -195,31 +200,36 @@ func (app *App) MakeServer() *echo.Echo {
t := NewTemplate(app) t := NewTemplate(app)
e.Renderer = t e.Renderer = t
frontUser := FrontUser(app) frontUser := FrontUser(app)
e.GET("/", FrontRoot(app))
e.GET("/web/admin", FrontAdmin(app)) getLanguage := app.GetLanguageMiddleware()
e.GET("/web/complete-registration", FrontCompleteRegistration(app))
e.GET("/web/create-player-challenge", FrontCreatePlayerChallenge(app)) e.GET("/", FrontRoot(app), getLanguage)
e.GET("/web/manifest.webmanifest", FrontWebManifest(app)) g := e.Group("/web")
e.GET("/web/oidc-callback/:providerName", FrontOIDCCallback(app)) g.Use(getLanguage)
e.GET("/web/player/:uuid", FrontPlayer(app)) g.GET("/admin", FrontAdmin(app))
e.GET("/web/register-challenge", FrontRegisterChallenge(app)) g.GET("/complete-registration", FrontCompleteRegistration(app))
e.GET("/web/registration", FrontRegistration(app)) g.GET("/create-player-challenge", FrontCreatePlayerChallenge(app))
e.GET("/web/user", frontUser) g.GET("/manifest.webmanifest", FrontWebManifest(app))
e.GET("/web/user/:uuid", frontUser) g.GET("/oidc-callback/:providerName", FrontOIDCCallback(app))
e.POST("/web/admin/delete-invite", FrontDeleteInvite(app)) g.GET("/player/:uuid", FrontPlayer(app))
e.POST("/web/admin/new-invite", FrontNewInvite(app)) g.GET("/register-challenge", FrontRegisterChallenge(app))
e.POST("/web/admin/update-users", FrontUpdateUsers(app)) g.GET("/registration", FrontRegistration(app))
e.POST("/web/create-player", FrontCreatePlayer(app)) g.GET("/user", frontUser)
e.POST("/web/delete-player", FrontDeletePlayer(app)) g.GET("/user/:uuid", frontUser)
e.POST("/web/delete-user", FrontDeleteUser(app)) g.POST("/admin/delete-invite", FrontDeleteInvite(app))
e.POST("/web/login", FrontLogin(app)) g.POST("/admin/new-invite", FrontNewInvite(app))
e.POST("/web/logout", FrontLogout(app)) g.POST("/admin/update-users", FrontUpdateUsers(app))
e.POST("/web/oidc-migrate", app.FrontOIDCMigrate()) g.POST("/create-player", FrontCreatePlayer(app))
e.POST("/web/oidc-unlink", app.FrontOIDCUnlink()) g.POST("/delete-player", FrontDeletePlayer(app))
e.POST("/web/register", FrontRegister(app)) g.POST("/delete-user", FrontDeleteUser(app))
e.POST("/web/update-player", FrontUpdatePlayer(app)) g.POST("/login", FrontLogin(app))
e.POST("/web/update-user", FrontUpdateUser(app)) g.POST("/logout", FrontLogout(app))
e.Static("/web/public", path.Join(app.Config.DataDirectory, "public")) g.POST("/oidc-migrate", app.FrontOIDCMigrate())
g.POST("/oidc-unlink", app.FrontOIDCUnlink())
g.POST("/register", FrontRegister(app))
g.POST("/update-player", FrontUpdatePlayer(app))
g.POST("/update-user", FrontUpdateUser(app))
g.Static("/public", path.Join(app.Config.DataDirectory, "public"))
} }
e.Static("/web/texture/cape", path.Join(app.Config.StateDirectory, "cape")) e.Static("/web/texture/cape", path.Join(app.Config.StateDirectory, "cape"))
e.Static("/web/texture/default-cape", path.Join(app.Config.StateDirectory, "default-cape")) e.Static("/web/texture/default-cape", path.Join(app.Config.StateDirectory, "default-cape"))
@ -425,6 +435,23 @@ func setup(config *Config) *App {
log.Fatalf("Couldn't access DataDirectory: %s", err) log.Fatalf("Couldn't access DataDirectory: %s", err)
} }
// Locales
locales := map[language.Tag]*gotext.Locale{}
localeTags := make([]language.Tag, 0)
locales_path := path.Join(config.DataDirectory, "locales")
lang_paths, err := filepath.Glob(path.Join(locales_path, "*"))
for _, lang_path := range lang_paths {
lang := filepath.Base(lang_path)
tag, err := language.Parse(lang)
if err != nil {
log.Fatalf("Unrecognized language tag: %s", lang)
}
l := gotext.NewLocale(locales_path, lang)
l.AddDomain("default")
localeTags = append(localeTags, tag)
locales[tag] = l
}
// Crypto // Crypto
key := ReadOrCreateKey(config) key := ReadOrCreateKey(config)
keyBytes := Unwrap(x509.MarshalPKCS8PrivateKey(key)) keyBytes := Unwrap(x509.MarshalPKCS8PrivateKey(key))
@ -575,6 +602,8 @@ func setup(config *Config) *App {
OIDCProvidersByName: oidcProvidersByName, OIDCProvidersByName: oidcProvidersByName,
OIDCProvidersByIssuer: oidcProvidersByIssuer, OIDCProvidersByIssuer: oidcProvidersByIssuer,
FallbackAPIServers: fallbackAPIServers, FallbackAPIServers: fallbackAPIServers,
Locales: locales,
LocaleTags: localeTags,
} }
// Post-setup // Post-setup

View File

@ -92,7 +92,7 @@ a:visited {
} }
.logo { .logo {
font-family: Helvetica, Arial, sans-serif; font-family: Arial, sans-serif;
color: white; color: white;
text-decoration: none; text-decoration: none;
font-size: 2rem; font-size: 2rem;

View File

@ -24,7 +24,7 @@
<input type="submit" value="Log out" /> <input type="submit" value="Log out" />
</form> </form>
{{ else }} {{ else }}
<a href="{{ .App.FrontEndURL }}/web/registration">Register</a> <a href="{{ .App.FrontEndURL }}/web/registration">{{ .L.Get "Register" }}</a>
{{ end }} {{ end }}
</div> </div>
</nav> </nav>