diff --git a/Makefile b/Makefile
index 76bbceb..f33dd20 100644
--- a/Makefile
+++ b/Makefile
@@ -23,7 +23,7 @@ install: build
install -Dm 755 drasl "$(prefix)/bin/drasl"
install -Dm 644 LICENSE "$(prefix)/share/licenses/drasl/LICENSE"
mkdir -p "$(prefix)/share/drasl/"
- cp -R assets view public "$(prefix)/share/drasl/"
+ cp -R assets view public locales "$(prefix)/share/drasl/"
clean:
rm -f drasl
diff --git a/front.go b/front.go
index 762198e..8ca4665 100644
--- a/front.go
+++ b/front.go
@@ -10,9 +10,11 @@ import (
"github.com/google/uuid"
"github.com/jxskiss/base62"
"github.com/labstack/echo/v4"
+ "github.com/leonelquinteros/gotext"
"github.com/samber/mo"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
+ "golang.org/x/text/language"
"gorm.io/gorm"
"html/template"
"io"
@@ -28,6 +30,7 @@ import (
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 COOKIE_PREFIX = "__Host-"
const BROWSER_TOKEN_COOKIE_NAME = COOKIE_PREFIX + "browserToken"
@@ -82,6 +85,21 @@ func NewTemplate(app *App) *Template {
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 {
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
- User *User
+ L *gotext.Locale
URL string
SuccessMessage string
WarningMessage 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
@@ -182,14 +216,10 @@ func (app *App) HandleWebError(err error, c *echo.Context) error {
}
if Contains(safeMethods, (*c).Request().Method) {
return (*c).Render(code, "error", errorContext{
- App: app,
- User: nil,
- URL: (*c).Request().URL.RequestURI(),
- Message: message,
- SuccessMessage: app.lastSuccessMessage(c),
- WarningMessage: app.lastWarningMessage(c),
- ErrorMessage: app.lastErrorMessage(c),
- StatusCode: code,
+ baseContext: app.NewBaseContext(c),
+ User: nil,
+ Message: message,
+ StatusCode: code,
})
} else {
returnURL := getReturnURL(app, c)
@@ -301,13 +331,9 @@ func EncodeOIDCState(state oidcState) (string, error) {
// GET /
func FrontRoot(app *App) func(c echo.Context) error {
type rootContext struct {
- App *App
+ baseContext
User *User
- URL string
Destination string
- SuccessMessage string
- WarningMessage string
- ErrorMessage string
WebOIDCProviders []webOIDCProvider
}
@@ -348,13 +374,9 @@ func FrontRoot(app *App) func(c echo.Context) error {
}
return c.Render(http.StatusOK, "root", rootContext{
- App: app,
+ baseContext: app.NewBaseContext(&c),
User: user,
- URL: c.Request().URL.RequestURI(),
Destination: destination,
- SuccessMessage: app.lastSuccessMessage(&c),
- WarningMessage: app.lastWarningMessage(&c),
- ErrorMessage: app.lastErrorMessage(&c),
WebOIDCProviders: webOIDCProviders,
})
})
@@ -408,12 +430,8 @@ type oidcState struct {
// GET /registration
func FrontRegistration(app *App) func(c echo.Context) error {
type registrationContext struct {
- App *App
+ baseContext
User *User
- URL string
- SuccessMessage string
- WarningMessage string
- ErrorMessage string
InviteCode string
WebOIDCProviders []webOIDCProvider
}
@@ -454,12 +472,8 @@ func FrontRegistration(app *App) func(c echo.Context) error {
}
return c.Render(http.StatusOK, "registration", registrationContext{
- App: app,
+ baseContext: app.NewBaseContext(&c),
User: user,
- URL: c.Request().URL.RequestURI(),
- SuccessMessage: app.lastSuccessMessage(&c),
- WarningMessage: app.lastWarningMessage(&c),
- ErrorMessage: app.lastErrorMessage(&c),
InviteCode: inviteCode,
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 {
type completeRegistrationContext struct {
- App *App
+ baseContext
User *User
- URL string
- SuccessMessage string
- WarningMessage string
- ErrorMessage string
InviteCode string
AnyUnmigratedUsers bool
AllowChoosingPlayerName bool
@@ -547,12 +557,8 @@ func FrontCompleteRegistration(app *App) func(c echo.Context) error {
}
return c.Render(http.StatusOK, "complete-registration", completeRegistrationContext{
- App: app,
+ baseContext: app.NewBaseContext(&c),
User: user,
- URL: c.Request().URL.RequestURI(),
- SuccessMessage: app.lastSuccessMessage(&c),
- WarningMessage: app.lastWarningMessage(&c),
- ErrorMessage: app.lastErrorMessage(&c),
InviteCode: inviteCode,
PreferredPlayerName: preferredPlayerName,
AllowChoosingPlayerName: provider.Config.AllowChoosingPlayerName,
@@ -775,14 +781,10 @@ func FrontOIDCCallback(app *App) func(c echo.Context) error {
// GET /web/admin
func FrontAdmin(app *App) func(c echo.Context) error {
type adminContext struct {
- App *App
- User *User
- URL string
- SuccessMessage string
- WarningMessage string
- ErrorMessage string
- Users []User
- Invites []Invite
+ baseContext
+ User *User
+ Users []User
+ Invites []Invite
}
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{
- App: app,
- User: user,
- URL: c.Request().URL.RequestURI(),
- SuccessMessage: app.lastSuccessMessage(&c),
- WarningMessage: app.lastWarningMessage(&c),
- ErrorMessage: app.lastErrorMessage(&c),
- Users: users,
- Invites: invites,
+ baseContext: app.NewBaseContext(&c),
+ User: user,
+ Users: users,
+ Invites: invites,
})
})
}
@@ -925,12 +923,8 @@ func FrontNewInvite(app *App) func(c echo.Context) error {
// GET /web/user/:uuid
func FrontUser(app *App) func(c echo.Context) error {
type userContext struct {
- App *App
+ baseContext
User *User
- URL string
- SuccessMessage string
- WarningMessage string
- ErrorMessage string
TargetUser *User
TargetUserID string
SkinURL *string
@@ -1014,12 +1008,8 @@ func FrontUser(app *App) func(c echo.Context) error {
}
return c.Render(http.StatusOK, "user", userContext{
- App: app,
+ baseContext: app.NewBaseContext(&c),
User: user,
- URL: c.Request().URL.RequestURI(),
- SuccessMessage: app.lastSuccessMessage(&c),
- WarningMessage: app.lastWarningMessage(&c),
- ErrorMessage: app.lastErrorMessage(&c),
TargetUser: targetUser,
AdminView: adminView,
LinkedOIDCProviderNames: linkedOIDCProviderNames.ToSlice(),
@@ -1032,18 +1022,14 @@ func FrontUser(app *App) func(c echo.Context) error {
// GET /web/player/:uuid
func FrontPlayer(app *App) func(c echo.Context) error {
type playerContext struct {
- App *App
- User *User
- URL string
- SuccessMessage string
- WarningMessage string
- ErrorMessage string
- PlayerUser *User
- Player *Player
- PlayerID string
- SkinURL *string
- CapeURL *string
- AdminView bool
+ baseContext
+ User *User
+ PlayerUser *User
+ Player *Player
+ PlayerID string
+ SkinURL *string
+ CapeURL *string
+ AdminView bool
}
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{
- App: app,
- User: user,
- URL: c.Request().URL.RequestURI(),
- SuccessMessage: app.lastSuccessMessage(&c),
- WarningMessage: app.lastWarningMessage(&c),
- ErrorMessage: app.lastErrorMessage(&c),
- PlayerUser: &playerUser,
- Player: &player,
- PlayerID: id,
- SkinURL: skinURL,
- CapeURL: capeURL,
- AdminView: adminView,
+ baseContext: app.NewBaseContext(&c),
+ User: user,
+ PlayerUser: &playerUser,
+ Player: &player,
+ 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 {
type challengeContext struct {
- App *App
+ baseContext
User *User
- URL string
- SuccessMessage string
- WarningMessage string
- ErrorMessage string
PlayerName string
RegistrationProvider string
SkinBase64 string
@@ -1372,12 +1350,8 @@ func frontChallenge(app *App, action string) func(c echo.Context) error {
skinBase64 := base64.StdEncoding.EncodeToString(challengeSkinBytes)
return c.Render(http.StatusOK, "challenge", challengeContext{
- App: app,
+ baseContext: app.NewBaseContext(&c),
User: user,
- URL: c.Request().URL.RequestURI(),
- SuccessMessage: app.lastSuccessMessage(&c),
- WarningMessage: app.lastWarningMessage(&c),
- ErrorMessage: app.lastErrorMessage(&c),
PlayerName: playerName,
SkinBase64: skinBase64,
SkinFilename: playerName + "-challenge.png",
diff --git a/go.mod b/go.mod
index af46dc8..d4d42f6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
module unmojang.org/drasl
-go 1.23.0
+go 1.23.5
-toolchain go1.23.2
+toolchain go1.23.10
require (
github.com/BurntSushi/toml v1.3.2
@@ -38,6 +38,7 @@ require (
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/pretty v0.3.1 // 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-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.18 // indirect
diff --git a/go.sum b/go.sum
index c678b5c..9a8dad0 100644
--- a/go.sum
+++ b/go.sum
@@ -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/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
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/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
diff --git a/locales/en-US/default.po b/locales/en-US/default.po
new file mode 100644
index 0000000..e69de29
diff --git a/locales/es/default.po b/locales/es/default.po
new file mode 100644
index 0000000..d09b2d6
--- /dev/null
+++ b/locales/es/default.po
@@ -0,0 +1,7 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: es\n"
+
+msgid "Register"
+msgstr "Registrarse"
diff --git a/main.go b/main.go
index f2ca6a3..1b81fea 100644
--- a/main.go
+++ b/main.go
@@ -13,8 +13,10 @@ import (
"github.com/dgraph-io/ristretto"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
+ "github.com/leonelquinteros/gotext"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
+ "golang.org/x/text/language"
"golang.org/x/time/rate"
"gorm.io/gorm"
"image"
@@ -25,6 +27,7 @@ import (
"net/url"
"os"
"path"
+ "path/filepath"
"regexp"
"strings"
"time"
@@ -71,6 +74,8 @@ type App struct {
OIDCProvidersByName map[string]*OIDCProvider
OIDCProvidersByIssuer map[string]*OIDCProvider
FallbackAPIServers []FallbackAPIServer
+ Locales map[language.Tag]*gotext.Locale
+ LocaleTags []language.Tag
}
func LogInfo(args ...interface{}) {
@@ -195,31 +200,36 @@ func (app *App) MakeServer() *echo.Echo {
t := NewTemplate(app)
e.Renderer = t
frontUser := FrontUser(app)
- e.GET("/", FrontRoot(app))
- e.GET("/web/admin", FrontAdmin(app))
- e.GET("/web/complete-registration", FrontCompleteRegistration(app))
- e.GET("/web/create-player-challenge", FrontCreatePlayerChallenge(app))
- e.GET("/web/manifest.webmanifest", FrontWebManifest(app))
- e.GET("/web/oidc-callback/:providerName", FrontOIDCCallback(app))
- e.GET("/web/player/:uuid", FrontPlayer(app))
- e.GET("/web/register-challenge", FrontRegisterChallenge(app))
- e.GET("/web/registration", FrontRegistration(app))
- e.GET("/web/user", frontUser)
- e.GET("/web/user/:uuid", frontUser)
- e.POST("/web/admin/delete-invite", FrontDeleteInvite(app))
- e.POST("/web/admin/new-invite", FrontNewInvite(app))
- e.POST("/web/admin/update-users", FrontUpdateUsers(app))
- e.POST("/web/create-player", FrontCreatePlayer(app))
- e.POST("/web/delete-player", FrontDeletePlayer(app))
- e.POST("/web/delete-user", FrontDeleteUser(app))
- e.POST("/web/login", FrontLogin(app))
- e.POST("/web/logout", FrontLogout(app))
- e.POST("/web/oidc-migrate", app.FrontOIDCMigrate())
- e.POST("/web/oidc-unlink", app.FrontOIDCUnlink())
- e.POST("/web/register", FrontRegister(app))
- e.POST("/web/update-player", FrontUpdatePlayer(app))
- e.POST("/web/update-user", FrontUpdateUser(app))
- e.Static("/web/public", path.Join(app.Config.DataDirectory, "public"))
+
+ getLanguage := app.GetLanguageMiddleware()
+
+ e.GET("/", FrontRoot(app), getLanguage)
+ g := e.Group("/web")
+ g.Use(getLanguage)
+ g.GET("/admin", FrontAdmin(app))
+ g.GET("/complete-registration", FrontCompleteRegistration(app))
+ g.GET("/create-player-challenge", FrontCreatePlayerChallenge(app))
+ g.GET("/manifest.webmanifest", FrontWebManifest(app))
+ g.GET("/oidc-callback/:providerName", FrontOIDCCallback(app))
+ g.GET("/player/:uuid", FrontPlayer(app))
+ g.GET("/register-challenge", FrontRegisterChallenge(app))
+ g.GET("/registration", FrontRegistration(app))
+ g.GET("/user", frontUser)
+ g.GET("/user/:uuid", frontUser)
+ g.POST("/admin/delete-invite", FrontDeleteInvite(app))
+ g.POST("/admin/new-invite", FrontNewInvite(app))
+ g.POST("/admin/update-users", FrontUpdateUsers(app))
+ g.POST("/create-player", FrontCreatePlayer(app))
+ g.POST("/delete-player", FrontDeletePlayer(app))
+ g.POST("/delete-user", FrontDeleteUser(app))
+ g.POST("/login", FrontLogin(app))
+ g.POST("/logout", FrontLogout(app))
+ 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/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)
}
+ // 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
key := ReadOrCreateKey(config)
keyBytes := Unwrap(x509.MarshalPKCS8PrivateKey(key))
@@ -575,6 +602,8 @@ func setup(config *Config) *App {
OIDCProvidersByName: oidcProvidersByName,
OIDCProvidersByIssuer: oidcProvidersByIssuer,
FallbackAPIServers: fallbackAPIServers,
+ Locales: locales,
+ LocaleTags: localeTags,
}
// Post-setup
diff --git a/public/style.css b/public/style.css
index b018d5e..6769c86 100644
--- a/public/style.css
+++ b/public/style.css
@@ -92,7 +92,7 @@ a:visited {
}
.logo {
- font-family: Helvetica, Arial, sans-serif;
+ font-family: Arial, sans-serif;
color: white;
text-decoration: none;
font-size: 2rem;
diff --git a/view/header.tmpl b/view/header.tmpl
index ef7b065..d9d98a7 100644
--- a/view/header.tmpl
+++ b/view/header.tmpl
@@ -24,7 +24,7 @@
{{ else }}
- Register
+ {{ .L.Get "Register" }}
{{ end }}