Login page internationalisation (#5765)

* Load locale strings in login page
* Generate and use login locale strings
* Add makefile target
* Update workflow
* Update build dockerfiles
* Add missing default string
This commit is contained in:
WithoutPants 2025-03-27 11:56:43 +11:00 committed by GitHub
parent c8d74f0bcf
commit d9b4e62420
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 203 additions and 23 deletions

3
.gitignore vendored
View file

@ -21,6 +21,9 @@ vendor
# GraphQL generated output
internal/api/generated_*.go
# Generated locale files
ui/login/locales/*
####
# Visual Studio
####

View file

@ -281,6 +281,10 @@ generate-ui:
generate-backend: touch-ui
go generate ./cmd/stash
.PHONY: generate-login-locale
generate-login-locale:
go generate ./ui
.PHONY: generate-dataloaders
generate-dataloaders:
go generate ./internal/api/loaders
@ -351,7 +355,10 @@ ifdef STASH_SOURCEMAPS
endif
.PHONY: ui
ui: ui-env
ui: ui-only generate-login-locale
.PHONY: ui-only
ui-only: ui-env
cd ui/v2.5 && yarn build
.PHONY: zip-ui

View file

@ -1,7 +1,7 @@
# This dockerfile should be built with `make docker-build` from the stash root.
# Build Frontend
FROM node:alpine as frontend
FROM node:20-alpine AS frontend
RUN apk add --no-cache make git
## cache node_modules separately
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
@ -13,19 +13,22 @@ RUN make pre-ui
RUN make generate-ui
ARG GITHASH
ARG STASH_VERSION
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
# Build Backend
FROM golang:1.22-alpine as backend
FROM golang:1.22.8-alpine AS backend
RUN apk add --no-cache make alpine-sdk
WORKDIR /stash
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
COPY ./graphql /stash/graphql/
COPY ./scripts /stash/scripts/
COPY ./pkg /stash/pkg/
COPY ./cmd /stash/cmd
COPY ./internal /stash/internal
COPY ./cmd /stash/cmd/
COPY ./internal /stash/internal/
# needed for generate-login-locale
COPY ./ui /stash/ui/
RUN make generate-backend generate-login-locale
COPY --from=frontend /stash /stash/
RUN make generate-backend
ARG GITHASH
ARG STASH_VERSION
RUN make flags-release flags-pie stash

View file

@ -1,7 +1,7 @@
# This dockerfile should be built with `make docker-cuda-build` from the stash root.
# Build Frontend
FROM node:alpine as frontend
FROM node:20-alpine AS frontend
RUN apk add --no-cache make git
## cache node_modules separately
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
@ -13,19 +13,22 @@ RUN make pre-ui
RUN make generate-ui
ARG GITHASH
ARG STASH_VERSION
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
# Build Backend
FROM golang:1.22-bullseye as backend
FROM golang:1.22.8-bullseye AS backend
RUN apt update && apt install -y build-essential golang
WORKDIR /stash
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
COPY ./graphql /stash/graphql/
COPY ./scripts /stash/scripts/
COPY ./pkg /stash/pkg/
COPY ./cmd /stash/cmd
COPY ./internal /stash/internal
# needed for generate-login-locale
COPY ./ui /stash/ui/
RUN make generate-backend generate-login-locale
COPY --from=frontend /stash /stash/
RUN make generate-backend
ARG GITHASH
ARG STASH_VERSION
RUN make flags-release flags-pie stash

View file

@ -4,7 +4,7 @@ This dockerfile is used to build a stash docker container using the current sour
# Building the docker container
From the top-level directory (should contain `main.go` file):
From the top-level directory (should contain `tools.go` file):
```
make docker-build

View file

@ -42,6 +42,7 @@ import (
const (
loginEndpoint = "/login"
loginLocaleEndpoint = loginEndpoint + "/locale"
logoutEndpoint = "/logout"
gqlEndpoint = "/graphql"
playgroundEndpoint = "/playground"
@ -228,6 +229,7 @@ func Initialize() (*Server, error) {
r.Get(loginEndpoint, handleLogin())
r.Post(loginEndpoint, handleLoginPost())
r.Get(logoutEndpoint, handleLogout())
r.Get(loginLocaleEndpoint, handleLoginLocale(cfg))
r.HandleFunc(loginEndpoint+"/*", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.TrimPrefix(r.URL.Path, loginEndpoint)
w.Header().Set("Cache-Control", "no-cache")

View file

@ -17,7 +17,11 @@ import (
"github.com/stashapp/stash/ui"
)
const returnURLParam = "returnURL"
const (
returnURLParam = "returnURL"
defaultLocale = "en-GB"
)
func getLoginPage() []byte {
data, err := fs.ReadFile(ui.LoginUIBox, "login.html")
@ -58,6 +62,47 @@ func serveLoginPage(w http.ResponseWriter, r *http.Request, returnURL string, lo
utils.ServeStaticContent(w, r, buffer.Bytes())
}
func handleLoginLocale(cfg *config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// get the locale from the config
lang := cfg.GetLanguage()
if lang == "" {
lang = defaultLocale
}
data, err := getLoginLocale(lang)
if err != nil {
logger.Debugf("Failed to load login locale file for language %s: %v", lang, err)
// try again with the default language
if lang != defaultLocale {
data, err = getLoginLocale(defaultLocale)
if err != nil {
logger.Errorf("Failed to load login locale file for default language %s: %v", defaultLocale, err)
}
}
// if there's still an error, response with an internal server error
if err != nil {
http.Error(w, "Failed to load login locale file", http.StatusInternalServerError)
return
}
}
// write a script to set the locale string map as a global variable
localeScript := fmt.Sprintf("var localeStrings = %s;", data)
w.Header().Set("Content-Type", "application/javascript")
_, _ = w.Write([]byte(localeScript))
}
}
func getLoginLocale(lang string) ([]byte, error) {
data, err := fs.ReadFile(ui.LoginUIBox, "locales/"+lang+".json")
if err != nil {
return nil, err
}
return data, nil
}
func handleLogin() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
returnURL := r.URL.Query().Get(returnURLParam)

View file

@ -0,0 +1,90 @@
//go:build ignore
// +build ignore
package main
import (
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/utils"
)
func main() {
verbose := len(os.Args) > 1 && os.Args[1] == "-v"
fmt.Printf("Generating login locales\n")
// read all json files in the locales directory
// and extract only the login part
// assume running from ui directory
dirFS := os.DirFS(filepath.Join("v2.5", "src", "locales"))
// ensure the login/locales directory exists
if err := fsutil.EnsureDir(filepath.Join("login", "locales")); err != nil {
panic(err)
}
fs.WalkDir(dirFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
panic(err)
}
if d.IsDir() {
return nil
}
if filepath.Ext(path) != ".json" {
return nil
}
// extract the login part
// from the json file
src, err := dirFS.Open(path)
if err != nil {
panic(err)
}
defer src.Close()
data, err := io.ReadAll(src)
if err != nil {
panic(err)
}
m := make(utils.NestedMap)
if err := json.Unmarshal(data, &m); err != nil {
panic(err)
}
l, found := m.Get("login")
if !found {
// nothing to do
return nil
}
// create new json file
// with only the login part
if verbose {
fmt.Printf("Writing %s\n", d.Name())
}
f, err := os.Create(filepath.Join("login", "locales", d.Name()))
if err != nil {
panic(err)
}
defer f.Close()
e := json.NewEncoder(f)
if err := e.Encode(l); err != nil {
panic(err)
}
return nil
})
}

View file

@ -9,6 +9,18 @@
<link rel="shortcut icon" href="data:,">
<link rel="stylesheet" href="login/login.css">
<link rel="stylesheet" href="css">
<!-- load locale -->
<script>
var localeStrings = {
username: "Username",
password: "Password",
login: "Login",
invalid_credentials: "Invalid credentials",
internal_error: "Unexpected internal error. See logs for more details"
};
</script>
<script src="login/locale"></script>
</head>
<script>
@ -25,28 +37,27 @@
if (xhr.status == 200) {
window.location.replace(returnURL);
} else {
document.getElementsByClassName("login-error")[0].innerHTML = xhr.responseText;
document.getElementsByClassName("login-error")[0].innerHTML = localeStrings.invalid_credentials;
}
}
};
xhr.onerror = function() {
document.getElementsByClassName("login-error")[0].innerHTML = "An error occurred while trying to login.";
document.getElementsByClassName("login-error")[0].innerHTML = localeStrings.internal_error;
};
xhr.send("username=" + username + "&password=" + password + "&returnURL=" + returnURL);
}
</script>
<body class="login">
<div class="dialog">
<div class="card">
<form action="login" method="POST" onsubmit="event.preventDefault(); login();">
<div class="form-group">
<label for="username"><h6>Username</h6></label>
<label for="username"><h6 id="username-heading">Username</h6></label>
<input class="text-input form-control" id="username" name="username" type="text" placeholder="Username" />
</div>
<div class="form-group">
<label for="password"><h6>Password</h6></label>
<label for="password"><h6 id="password-heading">Password</h6></label>
<input class="text-input form-control" id="password" name="password" type="password" placeholder="Password" />
</div>
<div class="login-error">
@ -56,11 +67,19 @@
<input type="hidden" id="returnURL" name="returnURL" value="{{.URL}}" />
<div>
<input class="btn btn-primary" type="submit" value="Login">
<input id="login-button" class="btn btn-primary" type="submit" value="Login">
</div>
</form>
</div>
</div>
</body>
<script>
document.getElementById("username-heading").innerText = localeStrings.username;
document.getElementById("password-heading").innerText = localeStrings.password;
document.getElementById("username").placeholder = localeStrings.username;
document.getElementById("password").placeholder = localeStrings.password;
document.getElementById("login-button").value = localeStrings.login;
</script>
</html>

View file

@ -1,3 +1,4 @@
//go:generate go run -tags=dev ../scripts/generateLoginLocales.go
package ui
import (

View file

@ -1142,6 +1142,13 @@
"generic": "Loading…",
"plugins": "Loading plugins…"
},
"login": {
"login": "Login",
"username": "Username",
"password": "Password",
"invalid_credentials": "Invalid username or password",
"internal_error": "Unexpected internal error. See logs for more details"
},
"marker_count": "Marker Count",
"markers": "Markers",
"measurements": "Measurements",