diff --git a/config/config-base.json b/config/config-base.json index a33dee3..c25d7b5 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -720,7 +720,7 @@ "required": false, "requires_restart": false, "type": "text", - "value": "Your account was deleted - Jellyfin", + "value": "", "description": "Subject of account deletion emails." }, "email_html": { diff --git a/html/setup2.html b/html/setup2.html index 42cb8f9..5fa18ff 100644 --- a/html/setup2.html +++ b/html/setup2.html @@ -1,9 +1,8 @@ - + - - {{ template "header.html" . }} - {{ .strings.pageTitle }} + + {{ .lang.Strings.pageTitle }}
@@ -20,328 +19,327 @@
- +
- Welcome! + {{ .lang.StartPage.welcome }}
-

You'll need to do a few things to set up jfa-go. Press continue to get started.

+

{{ .lang.StartPage.pressStart }}

- Language -

Community translations are available for most parts of jfa-go. You can choose the default languages below, but users can still change it for themselves. If you want to help out, sign up here to start contributing!

+ {{ .lang.Language.title }} +

- Login -

To access the admin page, you need to login with a method below:

+ {{ .lang.Login.title }} +

{{ .lang.Login.description }}

- Jellyfin/Emby -

jfa-go needs admin access because API tokens don't allow user creation. You should create a separate account and check "Allow this user to manage the server". You can disable everything else. Once done, enter the credentials here.

+ {{ .lang.JellyfinEmby.title }} +

{{ .lang.JellyfinEmby.description }}

- Email -

jfa-go can send password reset PINs and various notifications. You can connect to an SMTP server, or use the Mailgun API.

+ {{ .lang.Email.title }} +

-
- Notifications -

If enabled, you can choose (per invite) to receive an email when an invite expires, or a user is created. If you didn't choose Jellyfin Authentication, make sure you provided an email address.

+ {{ .lang.Notifications.title }} +

{{ .lang.Notifications.description }}

- Password Resets -

When a user tries to reset their password, Jellyfin creates a file named "passwordreset-*.json" which contains a PIN. jfa-go reads the file and sends the PIN to the user.

+ {{ .lang.PasswordResets.title }} +

{{ .lang.PasswordResets.description }}

- Invite Emails -

If enabled, you can send invites directly to a user's email address. Because you might be using a reverse proxy, you need to provide the URL invites are accessed from. Write your URL Base and append "/invite".

+ {{ .lang.InviteEmails.title }} +

{{ .lang.InviteEmails.description }}

- Password Validation -

If enabled, a set of password requirements will show on the create account page, such as minimum length, uppercase/lowercase characters, etc.

+ {{ .lang.PasswordValidation.title }} +

{{ .lang.PasswordValidation.description }}

- Help Messages -

These messages will display in the account creation page and in emails.

+ {{ .lang.HelpMessages.title }} +

{{ .lang.HelpMessages.description }}

+
- Finished! + {{ .lang.EndPage.finished }}
-

There are more settings you can configure on the admin page. Click below to restart, then refresh the page.

+

{{ .lang.EndPage.restartMessage }}

- Submit + {{ .lang.Strings.submit }}
- {{ template "form-base" . }} diff --git a/lang.go b/lang.go index 27f6fb7..ccac0f5 100644 --- a/lang.go +++ b/lang.go @@ -26,6 +26,13 @@ func (ls *adminLangs) getOptions(chosen string) (string, []string) { return chosenLang, opts } +type commonLangs map[string]commonLang + +type commonLang struct { + Meta langMeta `json:"meta"` + Strings langSection `json:"strings"` +} + type adminLang struct { Meta langMeta `json:"meta"` Strings langSection `json:"strings"` @@ -77,6 +84,35 @@ type emailLang struct { WelcomeEmail langSection `json:"welcomeEmail"` } +type setupLangs map[string]setupLang + +type setupLang struct { + Meta langMeta `json:"meta"` + Strings langSection `json:"strings"` + StartPage langSection `json:"startPage"` + EndPage langSection `json:"endPage"` + Language langSection `json:"language"` + Login langSection `json:"login"` + JellyfinEmby langSection `json:"jellyfinEmby"` + Email langSection `json:"email"` + Notifications langSection `json:"notifications"` + PasswordResets langSection `json:"passwordResets"` + InviteEmails langSection `json:"inviteEmails"` + PasswordValidation langSection `json:"passwordValidation"` + HelpMessages langSection `json:"helpMessages"` +} + +func (ls *setupLangs) 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 + i++ + } + return chosenLang, opts +} + type langSection map[string]string func (el langSection) format(field string, vals ...string) string { diff --git a/lang/admin/de-de.json b/lang/admin/de-de.json index 9456f99..da6b5ed 100644 --- a/lang/admin/de-de.json +++ b/lang/admin/de-de.json @@ -19,12 +19,8 @@ "create": "Erstellen", "apply": "Anwenden", "delete": "Löschen", - "submit": "Absenden", "name": "Name", "date": "Datum", - "username": "Benutzername", - "password": "Passwort", - "emailAddress": "E-Mail-Adresse", "lastActiveTime": "Zuletzt aktiv", "from": "Von", "user": "Benutzer", diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index 83b5dff..3a754f1 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -19,12 +19,8 @@ "create": "Create", "apply": "Apply", "delete": "Delete", - "submit": "Submit", "name": "Name", "date": "Date", - "username": "Username", - "password": "Password", - "emailAddress": "Email Address", "lastActiveTime": "Last Active", "from": "From", "user": "User", diff --git a/lang/admin/fr-fr.json b/lang/admin/fr-fr.json index 22d70fb..aee70b4 100644 --- a/lang/admin/fr-fr.json +++ b/lang/admin/fr-fr.json @@ -20,12 +20,8 @@ "create": "Créer", "apply": "Appliquer", "delete": "Effacer", - "submit": "Soumettre", "name": "Nom", "date": "Date", - "username": "Nom d'utilisateur", - "password": "Mot de passe", - "emailAddress": "Addresse Email", "lastActiveTime": "Dernière activité", "from": "De", "user": "Utilisateur", diff --git a/lang/admin/nl-nl.json b/lang/admin/nl-nl.json index 87ec5ec..a3b5c9e 100644 --- a/lang/admin/nl-nl.json +++ b/lang/admin/nl-nl.json @@ -19,12 +19,8 @@ "create": "Aanmaken", "apply": "Toepassen", "delete": "Verwijderen", - "submit": "Verstuur", "name": "Naam", "date": "Datum", - "username": "Gebruikersnaam", - "password": "Wachtwoord", - "emailAddress": "E-mailadres", "lastActiveTime": "Laatst actief", "from": "Van", "user": "Gebruiker", diff --git a/lang/common/de-de.json b/lang/common/de-de.json new file mode 100644 index 0000000..05ee422 --- /dev/null +++ b/lang/common/de-de.json @@ -0,0 +1,11 @@ +{ + "meta": { + "name": "Deutsch (DE)" + }, + "strings": { + "username": "Benutzername", + "password": "Passwort", + "emailAddress": "E-Mail-Adresse", + "submit": "Absenden" + } +} diff --git a/lang/common/en-us.json b/lang/common/en-us.json new file mode 100644 index 0000000..1dfcd57 --- /dev/null +++ b/lang/common/en-us.json @@ -0,0 +1,11 @@ +{ + "meta": { + "name": "English (US)" + }, + "strings": { + "username": "Username", + "password": "Password", + "emailAddress": "Email Address", + "submit": "Submit" + } +} diff --git a/lang/common/fr-fr.json b/lang/common/fr-fr.json new file mode 100644 index 0000000..0ab8d13 --- /dev/null +++ b/lang/common/fr-fr.json @@ -0,0 +1,12 @@ +{ + "meta": { + "name": "Francais (FR)", + "author": "https://github.com/Killianbe" + }, + "strings": { + "username": "Nom d'utilisateur", + "password": "Mot de passe", + "emailAddress": "Addresse Email", + "submit": "Soumettre" + } +} diff --git a/lang/common/nl-nl.json b/lang/common/nl-nl.json new file mode 100644 index 0000000..c099758 --- /dev/null +++ b/lang/common/nl-nl.json @@ -0,0 +1,11 @@ +{ + "meta": { + "name": "Nederlands (NL)" + }, + "strings": { + "username": "Gebruikersnaam", + "password": "Wachtwoord", + "emailAddress": "E-mailadres", + "submit": "Verstuur" + } +} diff --git a/lang/setup/en-us.json b/lang/setup/en-us.json new file mode 100644 index 0000000..ef3cbda --- /dev/null +++ b/lang/setup/en-us.json @@ -0,0 +1,101 @@ +{ + "meta": { + "name": "English (US)" + }, + "strings": { + "pageTitle": "Setup - jfa-go", + "next": "Next", + "back": "Back", + "optional": "Optional", + "serverType": "Server Type", + "disabled": "Disabled", + "enabled": "Enabled", + "port": "Port", + "message": "Message", + "serverAddress": "Server Address", + "emailSubject": "Email Subject", + "URL": "URL" + }, + "startPage": { + "welcome": "Welcome!", + "pressStart": "You'll need to do a few things to set up jfa-go. Press start to get continue.", + "httpsNotice": "Make sure you're accessing this page via HTTPS or on a private network.", + "start": "Start" + }, + "endPage": { + "finished": "Finished!", + "restartMessage": "There are more settings you can configure on the admin page. Click below to restart, then refresh the page." + }, + "language": { + "title": "Language", + "description": "Community translations are available for most parts of jfa-go. You can choose the default languages below, but users can still change it if they wish. If you want to help translate, sign up to {n} to start contributing!", + "defaultAdminLang": "Default admin language", + "defaultFormLang": "Default account creation language", + "defaultEmailLang": "Default email language" + }, + "login": { + "title": "Login", + "description": "To access the admin page, you need to login with a method below:", + "authorizeWithJellyfin": "Authorize with Jellyfin/Emby: Login details are shared with Jellyfin, which allows for multiple users.", + "authorizeManual": "Username and Password: Manually set the username and password.", + "adminOnly": "Admin users only (recommended)", + "emailNotice": "Your email address can be used to receive notifications." + }, + "jellyfinEmby": { + "title": "Jellyfin/Emby", + "description": "An admin account is needed because the API does not allow user creation using an API key. You should create a separate account and check 'Allow this user to manage the server'. You can disable everything else. Once done, enter the login details here.", + "embyNotice": "Emby support is limited and does not support password resets.", + "internal": "Internal", + "external": "External", + "addressExternalNotice": "Leave blank to use the same address.", + "testConnection": "Test Connection" + }, + "email": { + "title": "Email", + "description": "jfa-go can send password reset PINs and various notifications through email. You can connect to an SMTP server, or use the {n} API.", + "fromAddress": "From Address", + "senderName": "Sender Name", + "dateFormat": "Date Format", + "dateFormatNotice": "Date follows the strftime format. For more info, visit {n}.", + "time24h": "24h Time", + "time12h": "12h Time", + "encryption": "Encryption", + "mailgunApiURL": "API URL", + "mailgunApiKey": "API Key" + }, + "notifications": { + "title": "Notifications", + "description": "If enabled, you can choose (per invite) to receive an email when an invite expires, or a user is created. If you didn't choose the Jellyfin login method, make sure you provided your email address." + }, + "passwordResets": { + "title": "Password Resets", + "description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user.", + "pathToJellyfin": "Path to Jellyfin configuration directory", + "pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '/passwordreset-*.json' will appear." + }, + "inviteEmails": { + "title": "Invite Emails", + "description": "If enabled, you can send invites directly to a user's email address. Because you might be using a reverse proxy, you need to provide the URL invites are accessed from. Write your URL Base, and append '/invite'." + }, + "passwordValidation": { + "title": "Password Validation", + "description": "If enabled, a set of password requirements will show on the account creation page, such as minimum length, uppercase/lowercase characters, etc.", + "length": "Length", + "uppercase": "Uppercase characters", + "lowercase": "Lowercase characters", + "numbers": "Numbers", + "special": "Special characters (%, *, etc.)" + }, + "helpMessages": { + "title": "Help Messages", + "description": "These messages will display in the account creation page and in some emails.", + "contactMessage": "Contact Message", + "contactMessageNotice": "Displays at the bottom of all pages except admin.", + "helpMessage": "Help Message", + "helpMessageNotice": "Displays on the account creation page.", + "successMessage": "Success Message", + "successMessageNotice": "Displays when a user creates their account.", + "emailMessage": "Email Message", + "emailMessageNotice": "Displays at the bottom of emails." + } +} diff --git a/main.go b/main.go index 4525f44..870f624 100644 --- a/main.go +++ b/main.go @@ -516,6 +516,7 @@ func start(asDaemon, firstCall bool) { } } } + app.storage.lang.CommonPath = filepath.Join(app.localPath, "lang", "common") app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form") app.storage.lang.AdminPath = filepath.Join(app.localPath, "lang", "admin") app.storage.lang.EmailPath = filepath.Join(app.localPath, "lang", "email") @@ -559,6 +560,22 @@ func start(asDaemon, firstCall bool) { } else { debugMode = false address = "0.0.0.0:8056" + + app.storage.lang.CommonPath = filepath.Join(app.localPath, "lang", "common") + app.storage.lang.EmailPath = filepath.Join(app.localPath, "lang", "email") + app.storage.lang.SetupPath = filepath.Join(app.localPath, "lang", "setup") + err := app.storage.loadLangCommon() + if err != nil { + app.info.Fatalf("Failed to load language files: %+v\n", err) + } + err = app.storage.loadLangEmail() + if err != nil { + app.info.Fatalf("Failed to load language files: %+v\n", err) + } + err = app.storage.loadLangSetup() + if err != nil { + app.info.Fatalf("Failed to load language files: %+v\n", err) + } } app.info.Println("Loading routes") if debugMode { @@ -618,9 +635,7 @@ func start(asDaemon, firstCall bool) { } app.info.Printf("Starting router @ %s", address) } else { - router.GET("/", func(gc *gin.Context) { - gc.HTML(200, "setup.html", gin.H{}) - }) + router.GET("/", app.ServeSetup) router.POST("/jellyfin/test", app.TestJF) router.POST("/config", app.ModifyConfig) app.info.Printf("Loading setup @ %s", address) diff --git a/setup.go b/setup.go index 4bb1ab3..5da0930 100644 --- a/setup.go +++ b/setup.go @@ -1,11 +1,33 @@ package main import ( + "encoding/json" + "io/ioutil" + "path/filepath" + "strings" + "github.com/gin-gonic/gin" "github.com/hrfee/jfa-go/common" "github.com/hrfee/jfa-go/mediabrowser" ) +func (app *appContext) ServeSetup(gc *gin.Context) { + lang := gc.Query("lang") + if lang == "" { + lang = "en-us" + } else if _, ok := app.storage.lang.Admin[lang]; !ok { + lang = "en-us" + } + emailLang := lang + if _, ok := app.storage.lang.Email[lang]; !ok { + emailLang = "en-us" + } + gc.HTML(200, "setup2.html", gin.H{ + "lang": app.storage.lang.Setup[lang], + "emailLang": app.storage.lang.Email[emailLang], + }) +} + type testReq struct { Host string `json:"jfHost"` Username string `json:"jfUser"` @@ -24,3 +46,55 @@ func (app *appContext) TestJF(gc *gin.Context) { } gc.JSON(200, map[string]bool{"success": true}) } + +func (st *Storage) loadLangSetup() error { + st.lang.Setup = map[string]setupLang{} + var english setupLang + load := func(fname string) error { + index := strings.TrimSuffix(fname, filepath.Ext(fname)) + lang := setupLang{} + f, err := ioutil.ReadFile(filepath.Join(st.lang.SetupPath, fname)) + if err != nil { + return err + } + err = json.Unmarshal(f, &lang) + if err != nil { + return err + } + st.lang.Common.patchCommon(index, &lang.Strings) + if fname != "en-us.json" { + patchLang(&english.Strings, &lang.Strings) + patchLang(&english.StartPage, &lang.StartPage) + patchLang(&english.EndPage, &lang.EndPage) + patchLang(&english.Language, &lang.Language) + patchLang(&english.Login, &lang.Login) + patchLang(&english.JellyfinEmby, &lang.JellyfinEmby) + patchLang(&english.Email, &lang.Email) + patchLang(&english.Notifications, &lang.Notifications) + patchLang(&english.PasswordResets, &lang.PasswordResets) + patchLang(&english.InviteEmails, &lang.InviteEmails) + patchLang(&english.PasswordValidation, &lang.PasswordValidation) + patchLang(&english.HelpMessages, &lang.HelpMessages) + } + st.lang.Setup[index] = lang + return nil + } + err := load("en-us.json") + if err != nil { + return err + } + english = st.lang.Setup["en-us"] + files, err := ioutil.ReadDir(st.lang.SetupPath) + 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 +} diff --git a/storage.go b/storage.go index 4b586e9..9ef3575 100644 --- a/storage.go +++ b/storage.go @@ -55,9 +55,17 @@ type Lang struct { Form formLangs EmailPath string Email emailLangs + CommonPath string + Common commonLangs + SetupPath string + Setup setupLangs } func (st *Storage) loadLang() (err error) { + err = st.loadLangCommon() + if err != nil { + return + } err = st.loadLangAdmin() if err != nil { return @@ -70,6 +78,20 @@ func (st *Storage) loadLang() (err error) { return } +func (common *commonLangs) patchCommon(lang string, other *langSection) { + if *other == nil { + *other = langSection{} + } + if _, ok := (*common)[lang]; !ok { + lang = "en-us" + } + for n, ev := range (*common)[lang].Strings { + if v, ok := (*other)[n]; !ok || v == "" { + (*other)[n] = ev + } + } +} + // If a given language has missing values, fill it in with the english value. func patchLang(english, other *langSection) { if *other == nil { @@ -97,6 +119,49 @@ func patchQuantityStrings(english, other *map[string]quantityString) { } } +func (st *Storage) loadLangCommon() error { + st.lang.Common = map[string]commonLang{} + var english commonLang + load := func(fname string) error { + index := strings.TrimSuffix(fname, filepath.Ext(fname)) + lang := commonLang{} + f, err := ioutil.ReadFile(filepath.Join(st.lang.CommonPath, 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) + } + st.lang.Common[index] = lang + return nil + } + err := load("en-us.json") + if err != nil { + return err + } + english = st.lang.Common["en-us"] + files, err := ioutil.ReadDir(st.lang.CommonPath) + 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) loadLangAdmin() error { st.lang.Admin = map[string]adminLang{} var english adminLang @@ -114,6 +179,7 @@ func (st *Storage) loadLangAdmin() error { if err != nil { return err } + st.lang.Common.patchCommon(index, &lang.Strings) if fname != "en-us.json" { patchLang(&english.Strings, &lang.Strings) patchLang(&english.Notifications, &lang.Notifications) @@ -164,6 +230,7 @@ func (st *Storage) loadLangForm() error { if err != nil { return err } + st.lang.Common.patchCommon(index, &lang.Strings) if fname != "en-us.json" { patchLang(&english.Strings, &lang.Strings) patchQuantityStrings(&english.ValidationStrings, &lang.ValidationStrings)