You'll need to do a few things to set up jfa-go. Press continue to get started.
+
{{ .lang.StartPage.pressStart }}
- Make sure you're accessing this page via HTTPS or a Private Network.
- Continue
+ {{ .lang.StartPage.httpsNotice }}
+ {{ .lang.StartPage.start }}
- 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!
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.
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 }}
+
-
- Back
+ {{ .lang.Strings.back }}
- Continue
+ {{ .lang.Strings.next }}
- 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 }}
- Back
+ {{ .lang.Strings.back }}
- Continue
+ {{ .lang.Strings.next }}
- 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 }}
- Back
+ {{ .lang.Strings.back }}
- Continue
+ {{ .lang.Strings.next }}
- 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 }}
- Back
+ {{ .lang.Strings.back }}
- Continue
+ {{ .lang.Strings.next }}
- Help Messages
-
These messages will display in the account creation page and in emails.
+ {{ .lang.HelpMessages.title }}
+
{{ .lang.HelpMessages.description }}
+
- Back
+ {{ .lang.Strings.back }}
- Continue
+ {{ .lang.Strings.next }}
- 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)