1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2025-04-19 17:42:53 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
e834445b0b
Restructure language loading to support incomplete translations
On startup, files are scanned and any missing values are replaced with
the english version.
2021-01-19 00:29:29 +00:00
1aadd12006
move validationStrings out of strings in lang/form 2021-01-18 22:06:50 +00:00
26a1f30d32
Fix initial language setting value
For some reason it was set as en-US, not en-us.
2021-01-18 00:14:12 +00:00
14 changed files with 512 additions and 267 deletions

51
api.go
View File

@ -1085,33 +1085,15 @@ 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
loadLangs := func(langs *map[string]map[string]interface{}, settingsKey string) (string, []string) { formChosen, formOptions := app.storage.lang.Form.getOptions(app.config.Section("ui").Key("language-form").MustString("en-us"))
langOptions := make([]string, len(*langs))
chosenLang := app.config.Section("ui").Key("language" + settingsKey).MustString("en-us")
chosenLangName := (*langs)[chosenLang]["meta"].(map[string]interface{})["name"].(string)
i := 0
for _, lang := range *langs {
langOptions[i] = lang["meta"].(map[string]interface{})["name"].(string)
i++
}
return chosenLangName, langOptions
}
formChosen, formOptions := loadLangs(&app.storage.lang.Form, "-form")
fl := resp.Sections["ui"].Settings["language-form"] fl := resp.Sections["ui"].Settings["language-form"]
fl.Options = formOptions fl.Options = formOptions
fl.Value = formChosen fl.Value = formChosen
adminChosen, adminOptions := loadLangs(&app.storage.lang.Admin, "-admin") adminChosen, adminOptions := app.storage.lang.Admin.getOptions(app.config.Section("ui").Key("language-admin").MustString("en-us"))
al := resp.Sections["ui"].Settings["language-admin"] al := resp.Sections["ui"].Settings["language-admin"]
al.Options = adminOptions al.Options = adminOptions
al.Value = adminChosen al.Value = adminChosen
emailOptions := make([]string, len(app.storage.lang.Email)) emailChosen, emailOptions := app.storage.lang.Email.getOptions(app.config.Section("email").Key("language").MustString("en-us"))
chosenLang := app.config.Section("email").Key("language").MustString("en-us")
emailChosen := app.storage.lang.Email.get(chosenLang, "meta", "name")
i := 0
for langName := range app.storage.lang.Email {
emailOptions[i] = app.storage.lang.Email.get(langName, "meta", "name")
i++
}
el := resp.Sections["email"].Settings["language"] el := resp.Sections["email"].Settings["language"]
el.Options = emailOptions el.Options = emailOptions
el.Value = emailChosen el.Value = emailChosen
@ -1136,7 +1118,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
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++
@ -1169,21 +1151,21 @@ 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-form" { if section == "ui" && setting == "language-form" {
for key, lang := range app.storage.lang.Form { for key, lang := range app.storage.lang.Form {
if lang["meta"].(map[string]interface{})["name"].(string) == value.(string) { if lang.Meta.Name == value.(string) {
tempConfig.Section("ui").Key("language-form").SetValue(key) tempConfig.Section("ui").Key("language-form").SetValue(key)
break break
} }
} }
} else if section == "ui" && setting == "language-admin" { } else if section == "ui" && setting == "language-admin" {
for key, lang := range app.storage.lang.Admin { for key, lang := range app.storage.lang.Admin {
if lang["meta"].(map[string]interface{})["name"].(string) == value.(string) { if lang.Meta.Name == value.(string) {
tempConfig.Section("ui").Key("language-admin").SetValue(key) tempConfig.Section("ui").Key("language-admin").SetValue(key)
break break
} }
} }
} else if section == "email" && setting == "language" { } else if section == "email" && setting == "language" {
for key := range app.storage.lang.Email { for key, lang := range app.storage.lang.Email {
if app.storage.lang.Email.get(key, "meta", "name") == value.(string) { if lang.Meta.Name == value.(string) {
tempConfig.Section("email").Key("language").SetValue(key) tempConfig.Section("email").Key("language").SetValue(key)
break break
} }
@ -1260,11 +1242,11 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
resp := langDTO{} resp := langDTO{}
if page == "form" { if page == "form" {
for key, lang := range app.storage.lang.Form { for key, lang := range app.storage.lang.Form {
resp[key] = lang["meta"].(map[string]interface{})["name"].(string) resp[key] = lang.Meta.Name
} }
} else if page == "admin" { } else if page == "admin" {
for key, lang := range app.storage.lang.Admin { for key, lang := range app.storage.lang.Admin {
resp[key] = lang["meta"].(map[string]interface{})["name"].(string) resp[key] = lang.Meta.Name
} }
} }
if len(resp) == 0 { if len(resp) == 0 {
@ -1274,6 +1256,19 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
gc.JSON(200, resp) gc.JSON(200, resp)
} }
func (app *appContext) ServeLang(gc *gin.Context) {
page := gc.Param("page")
lang := strings.Replace(gc.Param("file"), ".json", "", 1)
if page == "admin" {
gc.JSON(200, app.storage.lang.Admin[lang])
return
} else if page == "form" {
gc.JSON(200, app.storage.lang.Form[lang])
return
}
respondBool(400, false, gc)
}
// func Restart() error { // func Restart() error {
// defer func() { // defer func() {
// if r := recover(); r != nil { // if r := recover(); r != nil {

View File

@ -92,7 +92,7 @@
"options": [ "options": [
"en-us" "en-us"
], ],
"value": "en-US", "value": "en-us",
"description": "Default Account Form Language. Submit a PR on github if you'd like to translate." "description": "Default Account Form Language. Submit a PR on github if you'd like to translate."
}, },
"language-admin": { "language-admin": {
@ -103,7 +103,7 @@
"options": [ "options": [
"en-us" "en-us"
], ],
"value": "en-US", "value": "en-us",
"description": "Default Admin page Language. Settings has not been translated. Submit a PR on github if you'd like to translate." "description": "Default Admin page Language. Settings has not been translated. Submit a PR on github if you'd like to translate."
}, },
"theme": { "theme": {

View File

@ -73,8 +73,7 @@ func (sm *SMTP) send(address, fromName, fromAddr string, email *Email) error {
// Emailer contains the email sender, email content, and methods to construct message content. // Emailer contains the email sender, email content, and methods to construct message content.
type Emailer struct { type Emailer struct {
fromAddr, fromName string fromAddr, fromName string
lang *EmailLang lang emailLang
cLang string
sender emailClient sender emailClient
} }
@ -110,8 +109,7 @@ func NewEmailer(app *appContext) *Emailer {
emailer := &Emailer{ emailer := &Emailer{
fromAddr: app.config.Section("email").Key("address").String(), fromAddr: app.config.Section("email").Key("address").String(),
fromName: app.config.Section("email").Key("from").String(), fromName: app.config.Section("email").Key("from").String(),
lang: &(app.storage.lang.Email), lang: app.storage.lang.Email[app.storage.lang.chosenEmailLang],
cLang: app.storage.lang.chosenEmailLang,
} }
method := app.config.Section("email").Key("method").String() method := app.config.Section("email").Key("method").String()
if method == "smtp" { if method == "smtp" {
@ -137,7 +135,7 @@ func (emailer *Emailer) NewMailgun(url, key string) {
sender := &Mailgun{ sender := &Mailgun{
client: mailgun.NewMailgun(strings.Split(emailer.fromAddr, "@")[1], key), client: mailgun.NewMailgun(strings.Split(emailer.fromAddr, "@")[1], key),
} }
// Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages' // Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages')
if strings.Contains(url, "messages") { if strings.Contains(url, "messages") {
url = url[0:strings.LastIndex(url, "/")] url = url[0:strings.LastIndex(url, "/")]
url = url[0:strings.LastIndex(url, "/")] url = url[0:strings.LastIndex(url, "/")]
@ -157,9 +155,8 @@ func (emailer *Emailer) NewSMTP(server string, port int, username, password stri
} }
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext) (*Email, error) { func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext) (*Email, error) {
lang := emailer.cLang
email := &Email{ email := &Email{
subject: app.config.Section("invite_emails").Key("subject").MustString(emailer.lang.get(lang, "inviteEmail", "title")), subject: app.config.Section("invite_emails").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
} }
expiry := invite.ValidTill expiry := invite.ValidTill
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern) d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
@ -175,11 +172,11 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
} }
var tplData bytes.Buffer var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{ err = tpl.Execute(&tplData, map[string]string{
"hello": emailer.lang.get(lang, "inviteEmail", "hello"), "hello": emailer.lang.InviteEmail.get("hello"),
"youHaveBeenInvited": emailer.lang.get(lang, "inviteEmail", "youHaveBeenInvited"), "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
"toJoin": emailer.lang.get(lang, "inviteEmail", "toJoin"), "toJoin": emailer.lang.InviteEmail.get("toJoin"),
"inviteExpiry": emailer.lang.format(lang, "inviteEmail", "inviteExpiry", d, t, expiresIn), "inviteExpiry": emailer.lang.InviteEmail.format("inviteExpiry", d, t, expiresIn),
"linkButton": emailer.lang.get(lang, "inviteEmail", "linkButton"), "linkButton": emailer.lang.InviteEmail.get("linkButton"),
"invite_link": inviteLink, "invite_link": inviteLink,
"message": message, "message": message,
}) })
@ -196,9 +193,8 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
} }
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext) (*Email, error) { func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext) (*Email, error) {
lang := emailer.cLang
email := &Email{ email := &Email{
subject: emailer.lang.get(lang, "inviteExpiry", "title"), subject: emailer.lang.InviteExpiry.get("title"),
} }
expiry := app.formatDatetime(invite.ValidTill) expiry := app.formatDatetime(invite.ValidTill)
for _, key := range []string{"html", "text"} { for _, key := range []string{"html", "text"} {
@ -209,9 +205,9 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
} }
var tplData bytes.Buffer var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{ err = tpl.Execute(&tplData, map[string]string{
"inviteExpired": emailer.lang.get(lang, "inviteExpiry", "inviteExpired"), "inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
"expiredAt": emailer.lang.format(lang, "inviteExpiry", "expiredAt", "\""+code+"\"", expiry), "expiredAt": emailer.lang.InviteExpiry.format("expiredAt", "\""+code+"\"", expiry),
"notificationNotice": emailer.lang.get(lang, "inviteExpiry", "notificationNotice"), "notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"),
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -226,9 +222,8 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
} }
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) (*Email, error) { func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) (*Email, error) {
lang := emailer.cLang
email := &Email{ email := &Email{
subject: emailer.lang.get(lang, "userCreated", "title"), subject: emailer.lang.UserCreated.get("title"),
} }
created := app.formatDatetime(invite.Created) created := app.formatDatetime(invite.Created)
var tplAddress string var tplAddress string
@ -245,14 +240,14 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
} }
var tplData bytes.Buffer var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{ err = tpl.Execute(&tplData, map[string]string{
"aUserWasCreated": emailer.lang.format(lang, "userCreated", "aUserWasCreated", "\""+code+"\""), "aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""),
"name": emailer.lang.get(lang, "userCreated", "name"), "name": emailer.lang.UserCreated.get("name"),
"address": emailer.lang.get(lang, "userCreated", "emailAddress"), "address": emailer.lang.UserCreated.get("emailAddress"),
"time": emailer.lang.get(lang, "userCreated", "time"), "time": emailer.lang.UserCreated.get("time"),
"nameVal": username, "nameVal": username,
"addressVal": tplAddress, "addressVal": tplAddress,
"timeVal": created, "timeVal": created,
"notificationNotice": emailer.lang.get(lang, "userCreated", "notificationNotice"), "notificationNotice": emailer.lang.UserCreated.get("notificationNotice"),
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -267,9 +262,8 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
} }
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Email, error) { func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Email, error) {
lang := emailer.cLang
email := &Email{ email := &Email{
subject: emailer.lang.get(lang, "passwordReset", "title"), subject: emailer.lang.PasswordReset.get("title"),
} }
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern) d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
message := app.config.Section("email").Key("message").String() message := app.config.Section("email").Key("message").String()
@ -281,12 +275,12 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema
} }
var tplData bytes.Buffer var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{ err = tpl.Execute(&tplData, map[string]string{
"helloUser": emailer.lang.format(lang, "passwordReset", "helloUser", pwr.Username), "helloUser": emailer.lang.PasswordReset.format("helloUser", pwr.Username),
"someoneHasRequestedReset": emailer.lang.get(lang, "passwordReset", "someoneHasRequestedReset"), "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
"ifItWasYou": emailer.lang.get(lang, "passwordReset", "ifItWasYou"), "ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"),
"codeExpiry": emailer.lang.format(lang, "passwordReset", "codeExpiry", d, t, expiresIn), "codeExpiry": emailer.lang.PasswordReset.format("codeExpiry", d, t, expiresIn),
"ifItWasNotYou": emailer.lang.get(lang, "passwordReset", "ifItWasNotYou"), "ifItWasNotYou": emailer.lang.PasswordReset.get("ifItWasNotYou"),
"pin": emailer.lang.get(lang, "passwordReset", "pin"), "pin": emailer.lang.PasswordReset.get("pin"),
"pinVal": pwr.Pin, "pinVal": pwr.Pin,
"message": message, "message": message,
}) })
@ -303,9 +297,8 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema
} }
func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email, error) { func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email, error) {
lang := emailer.cLang
email := &Email{ email := &Email{
subject: emailer.lang.get(lang, "userDeleted", "title"), subject: emailer.lang.UserDeleted.get("title"),
} }
for _, key := range []string{"html", "text"} { for _, key := range []string{"html", "text"} {
fpath := app.config.Section("deletion").Key("email_" + key).String() fpath := app.config.Section("deletion").Key("email_" + key).String()
@ -315,8 +308,8 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email
} }
var tplData bytes.Buffer var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{ err = tpl.Execute(&tplData, map[string]string{
"yourAccountWasDeleted": emailer.lang.get(lang, "userDeleted", "yourAccountWasDeleted"), "yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reason": emailer.lang.get(lang, "userDeleted", "reason"), "reason": emailer.lang.UserDeleted.get("reason"),
"reasonVal": reason, "reasonVal": reason,
}) })
if err != nil { if err != nil {

View File

@ -253,9 +253,9 @@
<div class="card ~neutral !low accounts mb-1"> <div class="card ~neutral !low accounts mb-1">
<span class="heading">{{ .strings.accounts }}</span> <span class="heading">{{ .strings.accounts }}</span>
<div class="fr"> <div class="fr">
<span class="button ~neutral !normal" id="accounts-add-user">{{ .quantityStrings.addUser.singular }}</span> <span class="button ~neutral !normal" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
<span class="button ~urge !normal" id="accounts-modify-user">{{ .strings.modifySettings }}</span> <span class="button ~urge !normal" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
<span class="button ~critical !normal" id="accounts-delete-user">{{ .quantityStrings.deleteUser.singular }}</span> <span class="button ~critical !normal" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
</div> </div>
<div class="card ~neutral !normal accounts-header table-responsive mt-half"> <div class="card ~neutral !normal accounts-header table-responsive mt-half">
<table class="table"> <table class="table">

View File

@ -1,8 +1,8 @@
{{ define "form-base" }} {{ define "form-base" }}
<script> <script>
window.usernameEnabled = {{ .username }}; window.usernameEnabled = {{ .username }};
window.validationStrings = JSON.parse({{ .lang.validationStrings }}); window.validationStrings = JSON.parse({{ .validationStrings }});
window.invalidPassword = "{{ .lang.reEnterPasswordInvalid }}"; window.invalidPassword = "{{ .strings.reEnterPasswordInvalid }}";
window.URLBase = "{{ .urlBase }}"; window.URLBase = "{{ .urlBase }}";
window.code = "{{ .code }}"; window.code = "{{ .code }}";
</script> </script>

View File

@ -3,14 +3,14 @@
<head> <head>
<link rel="stylesheet" type="text/css" href="css/base.css"> <link rel="stylesheet" type="text/css" href="css/base.css">
{{ template "header.html" . }} {{ template "header.html" . }}
<title>{{ .lang.pageTitle }}</title> <title>{{ .strings.pageTitle }}</title>
</head> </head>
<body class="max-w-full overflow-x-hidden section"> <body class="max-w-full overflow-x-hidden section">
<div id="modal-success" class="modal"> <div id="modal-success" class="modal">
<div class="modal-content card"> <div class="modal-content card">
<span class="heading mb-1">{{ .lang.successHeader }}</span> <span class="heading mb-1">{{ .strings.successHeader }}</span>
<p class="content mb-1">{{ .successMessage }}</p> <p class="content mb-1">{{ .successMessage }}</p>
<a class="button ~urge !normal full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .lang.successContinueButton }}</a> <a class="button ~urge !normal full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .strings.successContinueButton }}</a>
</div> </div>
</div> </div>
<div id="notification-box"></div> <div id="notification-box"></div>
@ -27,34 +27,34 @@
<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">
<span class="col heading">{{ .lang.createAccountHeader }}</span> <span class="col heading">{{ .strings.createAccountHeader }}</span>
<span class="col subheading"> {{ .helpMessage }}</span> <span class="col subheading"> {{ .helpMessage }}</span>
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<form class="card ~neutral !normal" id="form-create" href=""> <form class="card ~neutral !normal" id="form-create" href="">
<label class="label supra"> <label class="label supra">
{{ .lang.username }} {{ .strings.username }}
<input type="text" class="input ~neutral !high mt-half mb-1" placeholder="{{ .lang.username }}" id="create-username" aria-label="{{ .lang.username }}"> <input type="text" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.username }}" id="create-username" aria-label="{{ .strings.username }}">
</label> </label>
<label class="label supra" for="create-email">{{ .lang.emailAddress }}</label> <label class="label supra" for="create-email">{{ .strings.emailAddress }}</label>
<input type="email" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .lang.emailAddress }}" id="create-email" aria-label="{{ .lang.emailAddress }}" value="{{ .email }}"> <input type="email" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .strings.emailAddress }}" id="create-email" aria-label="{{ .strings.emailAddress }}" value="{{ .email }}">
<label class="label supra" for="create-password">{{ .lang.password }}</label> <label class="label supra" for="create-password">{{ .strings.password }}</label>
<input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .lang.password }}" id="create-password" aria-label="{{ .lang.password }}"> <input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-password" aria-label="{{ .strings.password }}">
<label class="label supra" for="create-reenter-password">{{ .lang.reEnterPassword }}</label> <label class="label supra" for="create-reenter-password">{{ .strings.reEnterPassword }}</label>
<input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .lang.password }}" id="create-reenter-password" aria-label="{{ .lang.reEnterPassword }}"> <input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-reenter-password" aria-label="{{ .strings.reEnterPassword }}">
<label> <label>
<input type="submit" class="unfocused"> <input type="submit" class="unfocused">
<span class="button ~urge !normal full-width center supra submit">{{ .lang.createAccountButton }}</span> <span class="button ~urge !normal full-width center supra submit">{{ .strings.createAccountButton }}</span>
</label> </label>
</form> </form>
</div> </div>
<div class="col"> <div class="col">
<div class="card ~neutral !normal"> <div class="card ~neutral !normal">
<span class="label supra" for="inv-uses">{{ .lang.passwordRequirementsHeader }}</span> <span class="label supra" for="inv-uses">{{ .strings.passwordRequirementsHeader }}</span>
<ul> <ul>
{{ range $key, $value := .requirements }} {{ range $key, $value := .requirements }}
<li class="" id="requirement-{{ $key }}" min="{{ $value }}"> <li class="" id="requirement-{{ $key }}" min="{{ $value }}">
@ -70,9 +70,6 @@
</div> </div>
</div> </div>
</div> </div>
<script>
window.validationStrings = {{ .lang.validationStrings }};
</script>
{{ template "form-base" . }} {{ template "form-base" . }}
</body> </body>
</html> </html>

90
lang.go Normal file
View File

@ -0,0 +1,90 @@
package main
import "strings"
type langMeta struct {
Name string `json:"name"`
}
type quantityString struct {
Singular string `json:"singular"`
Plural string `json:"plural"`
}
type adminLangs map[string]adminLang
func (ls *adminLangs) getOptions(chosen string) (string, []string) {
opts := make([]string, len(*ls))
chosenLang := (*ls)[chosen].Meta.Name
i := 0
for _, lang := range *ls {
opts[i] = lang.Meta.Name
}
return chosenLang, opts
}
type adminLang struct {
Meta langMeta `json:"meta"`
Strings langSection `json:"strings"`
Notifications langSection `json:"notifications"`
QuantityStrings map[string]quantityString `json:"quantityStrings"`
JSON string
}
type formLangs map[string]formLang
func (ls *formLangs) getOptions(chosen string) (string, []string) {
opts := make([]string, len(*ls))
chosenLang := (*ls)[chosen].Meta.Name
i := 0
for _, lang := range *ls {
opts[i] = lang.Meta.Name
}
return chosenLang, opts
}
type formLang struct {
Meta langMeta `json:"meta"`
Strings langSection `json:"strings"`
ValidationStrings map[string]quantityString `json:"validationStrings"`
validationStringsJSON string
}
type emailLangs map[string]emailLang
func (ls *emailLangs) getOptions(chosen string) (string, []string) {
opts := make([]string, len(*ls))
chosenLang := (*ls)[chosen].Meta.Name
i := 0
for _, lang := range *ls {
opts[i] = lang.Meta.Name
}
return chosenLang, opts
}
type emailLang struct {
Meta langMeta `json:"meta"`
UserCreated langSection `json:"userCreated"`
InviteExpiry langSection `json:"inviteExpiry"`
PasswordReset langSection `json:"passwordReset"`
UserDeleted langSection `json:"userDeleted"`
InviteEmail langSection `json:"inviteEmail"`
}
type langSection map[string]string
func (el *langSection) format(field string, vals ...string) string {
text := el.get(field)
for _, val := range vals {
text = strings.Replace(text, "{n}", val, 1)
}
return text
}
func (el *langSection) get(field string) string {
t, ok := (*el)[field]
if !ok {
return ""
}
return t
}

View File

@ -14,28 +14,28 @@
"createAccountButton": "Create Account", "createAccountButton": "Create Account",
"passwordRequirementsHeader": "Password Requirements", "passwordRequirementsHeader": "Password Requirements",
"successHeader": "Success!", "successHeader": "Success!",
"successContinueButton": "Continue", "successContinueButton": "Continue"
"validationStrings": { },
"length": { "validationStrings": {
"singular": "Must have at least {n} character", "length": {
"plural": "Must have at least {n} characters" "singular": "Must have at least {n} character",
}, "plural": "Must have at least {n} characters"
"uppercase": { },
"singular": "Must have at least {n} uppercase character", "uppercase": {
"plural": "Must have at least {n} uppercase characters" "singular": "Must have at least {n} uppercase character",
}, "plural": "Must have at least {n} uppercase characters"
"lowercase": { },
"singular": "Must have at least {n} lowercase character", "lowercase": {
"plural": "Must have at least {n} lowercase characters" "singular": "Must have at least {n} lowercase character",
}, "plural": "Must have at least {n} lowercase characters"
"number": { },
"singular": "Must have at least {n} number", "number": {
"plural": "Must have at least {n} numbers" "singular": "Must have at least {n} number",
}, "plural": "Must have at least {n} numbers"
"special": { },
"singular": "Must have at least {n} special character", "special": {
"plural": "Must have at least {n} special characters" "singular": "Must have at least {n} special character",
} "plural": "Must have at least {n} special characters"
} }
} }
} }

View File

@ -15,28 +15,28 @@
"createAccountButton": "Créer le compte", "createAccountButton": "Créer le compte",
"passwordRequirementsHeader": "Mot de passe requis", "passwordRequirementsHeader": "Mot de passe requis",
"successHeader": "Succes!", "successHeader": "Succes!",
"successContinueButton": "Continuer", "successContinueButton": "Continuer"
"validationStrings": { },
"length": { "validationStrings": {
"singular": "Doit avoir au moins {n} caractère", "length": {
"plural": "Doit avoir au moins {n} caractères" "singular": "Doit avoir au moins {n} caractère",
}, "plural": "Doit avoir au moins {n} caractères"
"uppercase": { },
"singular": "Doit avoir au moins {n} caractère majuscule", "uppercase": {
"plural": "Must have at least {n} caractères majuscules" "singular": "Doit avoir au moins {n} caractère majuscule",
}, "plural": "Must have at least {n} caractères majuscules"
"lowercase": { },
"singular": "Doit avoir au moins {n} caractère minuscule", "lowercase": {
"plural": "Doit avoir au moins {n} caractères minuscules" "singular": "Doit avoir au moins {n} caractère minuscule",
}, "plural": "Doit avoir au moins {n} caractères minuscules"
"number": { },
"singular": "Doit avoir au moins {n} nombre", "number": {
"plural": "Doit avoir au moins {n} nombres" "singular": "Doit avoir au moins {n} nombre",
}, "plural": "Doit avoir au moins {n} nombres"
"special": { },
"singular": "Doit avoir au moins {n} caractère spécial", "special": {
"plural": "Doit avoir au moins {n} caractères spéciaux" "singular": "Doit avoir au moins {n} caractère spécial",
} "plural": "Doit avoir au moins {n} caractères spéciaux"
} }
} }
} }

View File

@ -14,28 +14,28 @@
"createAccountButton": "Maak account aan", "createAccountButton": "Maak account aan",
"passwordRequirementsHeader": "Wachtwoordvereisten", "passwordRequirementsHeader": "Wachtwoordvereisten",
"successHeader": "Succes!", "successHeader": "Succes!",
"successContinueButton": "Doorgaan", "successContinueButton": "Doorgaan"
"validationStrings": { },
"length": { "validationStrings": {
"singular": "Moet ten minste {n} teken bevatten", "length": {
"plural": "Moet ten minste {n} tekens bevatten" "singular": "Moet ten minste {n} teken bevatten",
}, "plural": "Moet ten minste {n} tekens bevatten"
"uppercase": { },
"singular": "Moet ten minste {n} hoofdletter bevatten", "uppercase": {
"plural": "Moet ten minste {n} hoofdletters bevatten" "singular": "Moet ten minste {n} hoofdletter bevatten",
}, "plural": "Moet ten minste {n} hoofdletters bevatten"
"lowercase": { },
"singular": "Moet ten minste {n} kleine letter bevatten", "lowercase": {
"plural": "Moet ten minste {n} kleine letters bevatten" "singular": "Moet ten minste {n} kleine letter bevatten",
}, "plural": "Moet ten minste {n} kleine letters bevatten"
"number": { },
"singular": "Moet ten minste {n} cijfer bevatten", "number": {
"plural": "Moet ten minste {n} cijfers bevatten" "singular": "Moet ten minste {n} cijfer bevatten",
}, "plural": "Moet ten minste {n} cijfers bevatten"
"special": { },
"singular": "Moet ten minste {n} bijzonder teken bevatten", "special": {
"plural": "Moet ten minste {n} bijzondere tekens bevatten" "singular": "Moet ten minste {n} bijzonder teken bevatten",
} "plural": "Moet ten minste {n} bijzondere tekens bevatten"
} }
} }
} }

View File

@ -569,7 +569,6 @@ 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 {
@ -580,7 +579,7 @@ func start(asDaemon, firstCall bool) {
router.GET("/", app.AdminPage) router.GET("/", app.AdminPage)
router.GET("/accounts", app.AdminPage) router.GET("/accounts", app.AdminPage)
router.GET("/settings", app.AdminPage) router.GET("/settings", app.AdminPage)
router.GET("/lang/:page/:file", app.ServeLang)
router.GET("/lang/:page", app.GetLanguages) router.GET("/lang/:page", 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)

View File

@ -147,7 +147,7 @@ type setting struct {
} }
type section struct { type section struct {
Meta meta `json:"meta"` Meta langMeta `json:"meta"`
Order []string `json:"order"` Order []string `json:"order"`
Settings map[string]setting `json:"settings"` Settings map[string]setting `json:"settings"`
} }

View File

@ -20,30 +20,6 @@ type Storage struct {
lang Lang lang Lang
} }
type EmailLang map[string]map[string]map[string]interface{} // Map of lang codes to email name to fields
func (el *EmailLang) format(lang, email, field string, vals ...string) string {
text := (*el)[lang][email][field].(string)
for _, val := range vals {
text = strings.Replace(text, "{n}", val, 1)
}
return text
}
func (el *EmailLang) get(lang, email, field string) string { return (*el)[lang][email][field].(string) }
type Lang struct {
chosenFormLang string
chosenAdminLang string
chosenEmailLang string
AdminPath string
Admin map[string]map[string]interface{}
AdminJSON map[string]string
FormPath string
Form map[string]map[string]interface{}
EmailPath string
Email EmailLang
}
// timePattern: %Y-%m-%dT%H:%M:%S.%f // timePattern: %Y-%m-%dT%H:%M:%S.%f
type Profile struct { type Profile struct {
@ -67,6 +43,202 @@ type Invite struct {
Profile string `json:"profile"` Profile string `json:"profile"`
} }
type Lang struct {
chosenFormLang string
chosenAdminLang string
chosenEmailLang string
AdminPath string
Admin adminLangs
AdminJSON map[string]string
FormPath string
Form formLangs
EmailPath string
Email emailLangs
}
func (st *Storage) loadLang() (err error) {
err = st.loadLangAdmin()
if err != nil {
return
}
err = st.loadLangForm()
if err != nil {
return
}
err = st.loadLangEmail()
return
}
// If a given language has missing values, fill it in with the english value.
func patchLang(english, other *langSection) {
for n, ev := range *english {
if v, ok := (*other)[n]; !ok || v == "" {
(*other)[n] = ev
}
}
}
func patchQuantityStrings(english, other *map[string]quantityString) {
for n, ev := range *english {
qs, ok := (*other)[n]
if !ok {
(*other)[n] = ev
return
} else if qs.Singular == "" {
qs.Singular = ev.Singular
} else if (*other)[n].Plural == "" {
qs.Plural = ev.Plural
}
(*other)[n] = qs
}
}
func (st *Storage) loadLangAdmin() error {
st.lang.Admin = map[string]adminLang{}
var english adminLang
load := func(fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := adminLang{}
f, err := ioutil.ReadFile(filepath.Join(st.lang.AdminPath, fname))
if err != nil {
return err
}
if substituteStrings != "" {
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
}
err = json.Unmarshal(f, &lang)
if err != nil {
return err
}
if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings)
patchLang(&english.Notifications, &lang.Notifications)
patchQuantityStrings(&english.QuantityStrings, &lang.QuantityStrings)
}
stringAdmin, err := json.Marshal(lang)
if err != nil {
return err
}
lang.JSON = string(stringAdmin)
st.lang.Admin[index] = lang
return nil
}
err := load("en-us.json")
if err != nil {
return err
}
english = st.lang.Admin["en-us"]
files, err := ioutil.ReadDir(st.lang.AdminPath)
if err != nil {
return err
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(f.Name())
if err != nil {
return err
}
}
}
return nil
}
func (st *Storage) loadLangForm() error {
st.lang.Form = map[string]formLang{}
var english formLang
load := func(fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := formLang{}
f, err := ioutil.ReadFile(filepath.Join(st.lang.FormPath, fname))
if err != nil {
return err
}
if substituteStrings != "" {
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
}
err = json.Unmarshal(f, &lang)
if err != nil {
return err
}
if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings)
patchQuantityStrings(&english.ValidationStrings, &lang.ValidationStrings)
}
validationStrings, err := json.Marshal(lang.ValidationStrings)
if err != nil {
return err
}
lang.validationStringsJSON = string(validationStrings)
st.lang.Form[index] = lang
return nil
}
err := load("en-us.json")
if err != nil {
return err
}
english = st.lang.Form["en-us"]
files, err := ioutil.ReadDir(st.lang.FormPath)
if err != nil {
return err
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(f.Name())
if err != nil {
return err
}
}
}
return nil
}
func (st *Storage) loadLangEmail() error {
st.lang.Email = map[string]emailLang{}
var english emailLang
load := func(fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := emailLang{}
f, err := ioutil.ReadFile(filepath.Join(st.lang.EmailPath, fname))
if err != nil {
return err
}
if substituteStrings != "" {
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
}
err = json.Unmarshal(f, &lang)
if err != nil {
return err
}
if fname != "en-us.json" {
patchLang(&english.UserCreated, &lang.UserCreated)
patchLang(&english.InviteExpiry, &lang.InviteExpiry)
patchLang(&english.PasswordReset, &lang.PasswordReset)
patchLang(&english.UserDeleted, &lang.UserDeleted)
patchLang(&english.InviteEmail, &lang.InviteEmail)
}
st.lang.Email[index] = lang
return nil
}
err := load("en-us.json")
if err != nil {
return err
}
english = st.lang.Email["en-us"]
files, err := ioutil.ReadDir(st.lang.EmailPath)
if err != nil {
return err
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(f.Name())
if err != nil {
return err
}
}
}
return nil
}
type Invites map[string]Invite type Invites map[string]Invite
func (st *Storage) loadInvites() error { func (st *Storage) loadInvites() error {
@ -77,77 +249,75 @@ func (st *Storage) storeInvites() error {
return storeJSON(st.invite_path, st.invites) return storeJSON(st.invite_path, st.invites)
} }
func (st *Storage) loadLang() error { // func (st *Storage) loadLang() error {
loadData := func(path string, stringJson bool) (map[string]string, map[string]map[string]interface{}, error) { // loadData := func(path string, stringJson bool) (map[string]string, map[string]map[string]interface{}, error) {
files, err := ioutil.ReadDir(path) // files, err := ioutil.ReadDir(path)
outString := map[string]string{} // outString := map[string]string{}
out := map[string]map[string]interface{}{} // out := map[string]map[string]interface{}{}
if err != nil { // if err != nil {
return nil, nil, err // return nil, nil, err
} // }
for _, f := range files { // for _, f := range files {
index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) // index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
var data map[string]interface{} // var data map[string]interface{}
var file []byte // var file []byte
var err error // var err error
file, err = ioutil.ReadFile(filepath.Join(path, f.Name())) // file, err = ioutil.ReadFile(filepath.Join(path, f.Name()))
if err != nil { // if err != nil {
file = []byte("{}") // file = []byte("{}")
} // }
// Replace Jellyfin with something if necessary // // Replace Jellyfin with something if necessary
if substituteStrings != "" { // if substituteStrings != "" {
fileString := strings.ReplaceAll(string(file), "Jellyfin", substituteStrings) // fileString := strings.ReplaceAll(string(file), "Jellyfin", substituteStrings)
file = []byte(fileString) // file = []byte(fileString)
} // }
err = json.Unmarshal(file, &data) // err = json.Unmarshal(file, &data)
if err != nil { // if err != nil {
log.Printf("ERROR: Failed to read \"%s\": %s", path, err) // log.Printf("ERROR: Failed to read \"%s\": %s", path, err)
return nil, nil, err // return nil, nil, err
} // }
if stringJson { // if stringJson {
stringJSON, err := json.Marshal(data) // stringJSON, err := json.Marshal(data)
if err != nil { // if err != nil {
return nil, nil, err // return nil, nil, err
} // }
outString[index] = string(stringJSON) // outString[index] = string(stringJSON)
} // }
out[index] = data // out[index] = data
//
} // }
return outString, out, nil // return outString, out, nil
} // }
_, form, err := loadData(st.lang.FormPath, false) // _, form, err := loadData(st.lang.FormPath, false)
if err != nil { // if err != nil {
return err // return err
} // }
for index, lang := range form { // for index, lang := range form {
strings := lang["strings"].(map[string]interface{}) // validationStrings := lang["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 // }
} // lang["validationStrings"] = string(vS)
strings["validationStrings"] = string(vS) // form[index] = lang
lang["strings"] = strings // }
form[index] = lang // st.lang.Form = form
} // adminJSON, admin, err := loadData(st.lang.AdminPath, true)
st.lang.Form = form // st.lang.Admin = admin
adminJSON, admin, err := loadData(st.lang.AdminPath, true) // st.lang.AdminJSON = adminJSON
st.lang.Admin = admin //
st.lang.AdminJSON = adminJSON // _, emails, err := loadData(st.lang.EmailPath, false)
// fixedEmails := map[string]map[string]map[string]interface{}{}
_, emails, err := loadData(st.lang.EmailPath, false) // for lang, e := range emails {
fixedEmails := map[string]map[string]map[string]interface{}{} // f := map[string]map[string]interface{}{}
for lang, e := range emails { // for field, vals := range e {
f := map[string]map[string]interface{}{} // f[field] = vals.(map[string]interface{})
for field, vals := range e { // }
f[field] = vals.(map[string]interface{}) // fixedEmails[lang] = f
} // }
fixedEmails[lang] = f // st.lang.Email = fixedEmails
} // return err
st.lang.Email = fixedEmails // }
return err
}
func (st *Storage) loadEmails() error { func (st *Storage) loadEmails() error {
return loadJSON(st.emails_path, &st.emails) return loadJSON(st.emails_path, &st.emails)

View File

@ -45,7 +45,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
lang := gc.Query("lang") lang := gc.Query("lang")
if lang == "" { if lang == "" {
lang = app.storage.lang.chosenAdminLang lang = app.storage.lang.chosenAdminLang
} else if _, ok := app.storage.lang.Form[lang]; !ok { } else if _, ok := app.storage.lang.Admin[lang]; !ok {
lang = app.storage.lang.chosenAdminLang lang = app.storage.lang.chosenAdminLang
} }
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
@ -61,9 +61,9 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"commit": COMMIT, "commit": COMMIT,
"ombiEnabled": ombiEnabled, "ombiEnabled": ombiEnabled,
"username": !app.config.Section("email").Key("no_username").MustBool(false), "username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.Admin[lang]["strings"], "strings": app.storage.lang.Admin[lang].Strings,
"quantityStrings": app.storage.lang.Admin[lang]["quantityStrings"], "quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
"language": app.storage.lang.AdminJSON[lang], "language": app.storage.lang.Admin[lang].JSON,
}) })
} }
@ -84,18 +84,19 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
email = "" email = ""
} }
gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{ gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{
"urlBase": app.URLBase, "urlBase": app.URLBase,
"cssClass": app.cssClass, "cssClass": app.cssClass,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
"helpMessage": app.config.Section("ui").Key("help_message").String(), "helpMessage": app.config.Section("ui").Key("help_message").String(),
"successMessage": app.config.Section("ui").Key("success_message").String(), "successMessage": app.config.Section("ui").Key("success_message").String(),
"jfLink": app.config.Section("jellyfin").Key("public_server").String(), "jfLink": app.config.Section("jellyfin").Key("public_server").String(),
"validate": app.config.Section("password_validation").Key("enabled").MustBool(false), "validate": app.config.Section("password_validation").Key("enabled").MustBool(false),
"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[lang]["strings"], "strings": app.storage.lang.Form[lang].Strings,
"code": code, "validationStrings": app.storage.lang.Form[lang].validationStringsJSON,
"code": code,
}) })
} else { } else {
gcHTML(gc, 404, "invalidCode.html", gin.H{ gcHTML(gc, 404, "invalidCode.html", gin.H{