1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 17:10:10 +00:00

add language selector to form

This commit is contained in:
Harvey Tindall 2021-01-11 19:17:43 +00:00
parent 461efa7f60
commit 3fbbc7f620
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
10 changed files with 126 additions and 68 deletions

59
api.go
View File

@ -3,8 +3,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -1088,23 +1086,17 @@ func (app *appContext) GetConfig(gc *gin.Context) {
app.info.Println("Config requested") app.info.Println("Config requested")
resp := app.configBase resp := app.configBase
// Load language options // Load language options
langPath := filepath.Join(app.localPath, "lang", "form") langOptions := make([]string, len(app.storage.lang.Form))
app.lang.langFiles, _ = ioutil.ReadDir(langPath) chosenLang := app.config.Section("ui").Key("language").MustString("en-us")
app.lang.langOptions = make([]string, len(app.lang.langFiles)) chosenLangName := app.storage.lang.Form[chosenLang]["meta"].(map[string]interface{})["name"].(string)
chosenLang := app.config.Section("ui").Key("language").MustString("en-us") + ".json" i := 0
for i, f := range app.lang.langFiles { for _, lang := range app.storage.lang.Form {
if f.Name() == chosenLang { langOptions[i] = lang["meta"].(map[string]interface{})["name"].(string)
app.lang.chosenIndex = i i++
} }
var langFile map[string]interface{} l := resp.Sections["ui"].Settings["language"]
file, _ := ioutil.ReadFile(filepath.Join(langPath, f.Name())) l.Options = langOptions
json.Unmarshal(file, &langFile) l.Value = chosenLangName
if meta, ok := langFile["meta"]; ok {
app.lang.langOptions[i] = meta.(map[string]interface{})["name"].(string)
}
}
s := resp.Sections["ui"].Settings["language"]
for sectName, section := range resp.Sections { for sectName, section := range resp.Sections {
for settingName, setting := range section.Settings { for settingName, setting := range section.Settings {
val := app.config.Section(sectName).Key(settingName) val := app.config.Section(sectName).Key(settingName)
@ -1120,13 +1112,11 @@ func (app *appContext) GetConfig(gc *gin.Context) {
resp.Sections[sectName].Settings[settingName] = s resp.Sections[sectName].Settings[settingName] = s
} }
} }
s.Options = app.lang.langOptions resp.Sections["ui"].Settings["language"] = l
s.Value = app.lang.langOptions[app.lang.chosenIndex]
resp.Sections["ui"].Settings["language"] = s
t := resp.Sections["jellyfin"].Settings["type"] t := resp.Sections["jellyfin"].Settings["type"]
opts := make([]string, len(serverTypes)) opts := make([]string, len(serverTypes))
i := 0 i = 0
for _, v := range serverTypes { for _, v := range serverTypes {
opts[i] = v opts[i] = v
i++ i++
@ -1158,9 +1148,9 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
} }
for setting, value := range settings.(map[string]interface{}) { for setting, value := range settings.(map[string]interface{}) {
if section == "ui" && setting == "language" { if section == "ui" && setting == "language" {
for i, lang := range app.lang.langOptions { for key, lang := range app.storage.lang.Form {
if value.(string) == lang { if lang["meta"].(map[string]interface{})["name"].(string) == value.(string) {
tempConfig.Section(section).Key(setting).SetValue(strings.Replace(app.lang.langFiles[i].Name(), ".json", "", 1)) tempConfig.Section("ui").Key("language").SetValue(key)
break break
} }
} }
@ -1225,6 +1215,25 @@ func (app *appContext) Logout(gc *gin.Context) {
respondBool(200, true, gc) respondBool(200, true, gc)
} }
// @Summary Returns a map of available language codes to their full names, usable in the lang query parameter.
// @Produce json
// @Success 200 {object} langDTO
// @Failure 500 {object} stringResponse
// @Router /lang [get]
// @tags Other
func (app *appContext) GetLanguages(gc *gin.Context) {
resp := langDTO{}
for key, lang := range app.storage.lang.Form {
fmt.Printf("%+v\n", lang["meta"])
resp[key] = lang["meta"].(map[string]interface{})["name"].(string)
}
if len(resp) == 0 {
respond(500, "Couldn't get languages", gc)
return
}
gc.JSON(200, resp)
}
// func Restart() error { // func Restart() error {
// defer func() { // defer func() {
// if r := recover(); r != nil { // if r := recover(); r != nil {

View File

@ -84,5 +84,7 @@ func (app *appContext) loadConfig() error {
substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("") substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
app.storage.lang.chosenFormLang = app.config.Section("ui").Key("language").MustString("en-us")
return nil return nil
} }

View File

@ -85,7 +85,7 @@
}, },
"settings": { "settings": {
"language": { "language": {
"name": "Language", "name": "Default Form Language",
"required": false, "required": false,
"requires_restart": true, "requires_restart": true,
"type": "select", "type": "select",
@ -93,7 +93,7 @@
"en-us" "en-us"
], ],
"value": "en-US", "value": "en-US",
"description": "UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate." "description": "Default UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate."
}, },
"theme": { "theme": {
"name": "Default Look", "name": "Default Look",

View File

@ -370,3 +370,8 @@ p.top {
bottom: 1rem; bottom: 1rem;
z-index: 16; z-index: 16;
} }
.dropdown {
padding-bottom: 0.5rem;
margin-bottom: -0.5rem;
}

View File

@ -14,6 +14,16 @@
</div> </div>
</div> </div>
<div id="notification-box"></div> <div id="notification-box"></div>
<span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
<span class="ml-1 chev"></span>
</span>
<div class="dropdown-display">
<div class="card ~neutral !low" id="lang-list">
</div>
</div>
</span>
<div class="page-container"> <div class="page-container">
<div class="card ~neutral !low"> <div class="card ~neutral !low">
<div class="row baseline"> <div class="row baseline">

20
main.go
View File

@ -76,17 +76,9 @@ type appContext struct {
port int port int
version string version string
quit chan os.Signal quit chan os.Signal
lang Languages
URLBase string URLBase string
} }
// Languages stores the names and filenames of language files, and the index of that which is currently selected.
type Languages struct {
langFiles []os.FileInfo // Language filenames
langOptions []string // Language names
chosenIndex int
}
func (app *appContext) loadHTML(router *gin.Engine) { func (app *appContext) loadHTML(router *gin.Engine) {
customPath := app.config.Section("files").Key("html_templates").MustString("") customPath := app.config.Section("files").Key("html_templates").MustString("")
templatePath := filepath.Join(app.localPath, "html") templatePath := filepath.Join(app.localPath, "html")
@ -521,13 +513,11 @@ func start(asDaemon, firstCall bool) {
} }
} }
} }
app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form")
lang := app.config.Section("ui").Key("language").MustString("en-us") err = app.storage.loadLang()
app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form", lang+".json") if err != nil {
if _, err := os.Stat(app.storage.lang.FormPath); os.IsNotExist(err) { app.info.Fatalf("Failed to load language files: %+v\n", err)
app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form", "en-us.json")
} }
app.storage.loadLang()
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout) app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
@ -574,6 +564,7 @@ func start(asDaemon, firstCall bool) {
router.Use(gin.Recovery()) router.Use(gin.Recovery())
router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.localPath, "web"), false))) router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.localPath, "web"), false)))
router.Use(static.Serve("/lang/", static.LocalFile(filepath.Join(app.localPath, "lang"), false)))
app.loadHTML(router) app.loadHTML(router)
router.NoRoute(app.NoRouteHandler) router.NoRoute(app.NoRouteHandler)
if debugMode { if debugMode {
@ -585,6 +576,7 @@ func start(asDaemon, firstCall bool) {
router.GET("/accounts", app.AdminPage) router.GET("/accounts", app.AdminPage)
router.GET("/settings", app.AdminPage) router.GET("/settings", app.AdminPage)
router.GET("/lang", app.GetLanguages)
router.GET("/token/login", app.getTokenLogin) router.GET("/token/login", app.getTokenLogin)
router.GET("/token/refresh", app.getTokenRefresh) router.GET("/token/refresh", app.getTokenRefresh)
router.POST("/newUser", app.NewUser) router.POST("/newUser", app.NewUser)

View File

@ -156,3 +156,5 @@ type settings struct {
Order []string `json:"order"` Order []string `json:"order"`
Sections map[string]section `json:"sections"` Sections map[string]section `json:"sections"`
} }
type langDTO map[string]string

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log" "log"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -20,8 +21,9 @@ type Storage struct {
} }
type Lang struct { type Lang struct {
chosenFormLang string
FormPath string FormPath string
Form map[string]interface{} Form map[string]map[string]interface{}
} }
// timePattern: %Y-%m-%dT%H:%M:%S.%f // timePattern: %Y-%m-%dT%H:%M:%S.%f
@ -58,33 +60,45 @@ func (st *Storage) storeInvites() error {
} }
func (st *Storage) loadLang() error { func (st *Storage) loadLang() error {
formFiles, err := ioutil.ReadDir(st.lang.FormPath)
st.lang.Form = map[string]map[string]interface{}{}
if err != nil {
return err
}
for _, f := range formFiles {
index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
var data map[string]interface{}
if substituteStrings != "" { if substituteStrings != "" {
var file []byte var file []byte
var err error var err error
file, err = ioutil.ReadFile(st.lang.FormPath) file, err = ioutil.ReadFile(filepath.Join(st.lang.FormPath, f.Name()))
if err != nil { if err != nil {
file = []byte("{}") file = []byte("{}")
} }
// Replace Jellyfin with emby on form // Replace Jellyfin with emby on form
file = []byte(strings.ReplaceAll(string(file), "Jellyfin", substituteStrings)) file = []byte(strings.ReplaceAll(string(file), "Jellyfin", substituteStrings))
err = json.Unmarshal(file, &st.lang.Form) err = json.Unmarshal(file, &data)
if err != nil { if err != nil {
log.Printf("ERROR: Failed to read \"%s\": %s", st.lang.FormPath, err) log.Printf("ERROR: Failed to read \"%s\": %s", st.lang.FormPath, err)
}
return err return err
} }
err := loadJSON(st.lang.FormPath, &st.lang.Form) } else {
err := loadJSON(filepath.Join(st.lang.FormPath, f.Name()), &data)
if err != nil { if err != nil {
return err return err
} }
strings := st.lang.Form["strings"].(map[string]interface{}) }
strings := data["strings"].(map[string]interface{})
validationStrings := strings["validationStrings"].(map[string]interface{}) validationStrings := strings["validationStrings"].(map[string]interface{})
vS, err := json.Marshal(validationStrings) vS, err := json.Marshal(validationStrings)
if err != nil { if err != nil {
return err return err
} }
strings["validationStrings"] = string(vS) strings["validationStrings"] = string(vS)
st.lang.Form["strings"] = strings data["strings"] = strings
st.lang.Form[index] = data
}
return nil return nil
} }

View File

@ -1,5 +1,5 @@
import { Modal } from "./modules/modal.js"; import { Modal } from "./modules/modal.js";
import { _post, toggleLoader } from "./modules/common.js"; import { _get, _post, toggleLoader } from "./modules/common.js";
interface formWindow extends Window { interface formWindow extends Window {
validationStrings: pwValStrings; validationStrings: pwValStrings;
@ -21,6 +21,24 @@ interface pwValStrings {
[ type: string ]: pwValString; [ type: string ]: pwValString;
} }
_get("/lang", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
document.getElementById("lang-dropdown").remove();
return;
}
const list = document.getElementById("lang-list") as HTMLDivElement;
let innerHTML = '';
for (let code in req.response) {
innerHTML += `<a href="?lang=${code}" class="button input ~neutral field mb-half">${req.response[code]}</a>`;
}
list.innerHTML = innerHTML;
}
});
window.modal = new Modal(document.getElementById("modal-success")); window.modal = new Modal(document.getElementById("modal-success"));
declare var window: formWindow; declare var window: formWindow;

View File

@ -31,6 +31,12 @@ func (app *appContext) AdminPage(gc *gin.Context) {
func (app *appContext) InviteProxy(gc *gin.Context) { func (app *appContext) InviteProxy(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
lang := gc.Query("lang")
if lang == "" {
lang = app.storage.lang.chosenFormLang
} else if _, ok := app.storage.lang.Form[lang]; !ok {
lang = app.storage.lang.chosenFormLang
}
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */ /* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
// if app.checkInvite(code, false, "") { // if app.checkInvite(code, false, "") {
if _, ok := app.storage.invites[code]; ok { if _, ok := app.storage.invites[code]; ok {
@ -49,7 +55,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"requirements": app.validator.getCriteria(), "requirements": app.validator.getCriteria(),
"email": email, "email": email,
"username": !app.config.Section("email").Key("no_username").MustBool(false), "username": !app.config.Section("email").Key("no_username").MustBool(false),
"lang": app.storage.lang.Form["strings"], "lang": app.storage.lang.Form[lang]["strings"],
}) })
} else { } else {
gcHTML(gc, 404, "invalidCode.html", gin.H{ gcHTML(gc, 404, "invalidCode.html", gin.H{