mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
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:
parent
c8d74f0bcf
commit
d9b4e62420
11 changed files with 203 additions and 23 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -21,6 +21,9 @@ vendor
|
|||
# GraphQL generated output
|
||||
internal/api/generated_*.go
|
||||
|
||||
# Generated locale files
|
||||
ui/login/locales/*
|
||||
|
||||
####
|
||||
# Visual Studio
|
||||
####
|
||||
|
|
|
|||
9
Makefile
9
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -41,10 +41,11 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
loginEndpoint = "/login"
|
||||
logoutEndpoint = "/logout"
|
||||
gqlEndpoint = "/graphql"
|
||||
playgroundEndpoint = "/playground"
|
||||
loginEndpoint = "/login"
|
||||
loginLocaleEndpoint = loginEndpoint + "/locale"
|
||||
logoutEndpoint = "/logout"
|
||||
gqlEndpoint = "/graphql"
|
||||
playgroundEndpoint = "/playground"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
90
scripts/generateLoginLocales.go
Normal file
90
scripts/generateLoginLocales.go
Normal 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
|
||||
})
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
1
ui/ui.go
1
ui/ui.go
|
|
@ -1,3 +1,4 @@
|
|||
//go:generate go run -tags=dev ../scripts/generateLoginLocales.go
|
||||
package ui
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue