diff --git a/api.go b/api.go index 1c9565a..f65a0c9 100644 --- a/api.go +++ b/api.go @@ -455,7 +455,7 @@ func (app *appContext) DeleteUser(gc *gin.Context) { app.err.Printf("%s: Failed to send to %s", userID, address) app.debug.Printf("%s: Error: %s", userID, err) } else { - app.info.Printf("%s: Sent invite email to %s", userID, address) + app.info.Printf("%s: Sent deletion email to %s", userID, address) } }(userID, req.Reason, addr.(string)) } @@ -1176,7 +1176,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { break } } - } else { + } else if value.(string) != app.config.Section(section).Key(setting).MustString("") { tempConfig.Section(section).Key(setting).SetValue(value.(string)) } } diff --git a/config.go b/config.go index 247180e..5b11b76 100644 --- a/config.go +++ b/config.go @@ -52,7 +52,7 @@ func (app *appContext) loadConfig() error { key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) } } - for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template"} { + for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template"} { // if app.config.Section("files").Key(key).MustString("") == "" { // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) // } @@ -80,8 +80,6 @@ func (app *appContext) loadConfig() error { app.config.Section("jellyfin").Key("device").SetValue("jfa-go") app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", VERSION, COMMIT)) - app.email = NewEmailer(app) - substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("") oldFormLang := app.config.Section("ui").Key("language").MustString("") @@ -93,6 +91,9 @@ func (app *appContext) loadConfig() error { app.storage.lang.chosenFormLang = newFormLang } app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us") + app.storage.lang.chosenEmailLang = app.config.Section("email").Key("language").MustString("en-us") + + app.email = NewEmailer(app) return nil } diff --git a/config/config-base.json b/config/config-base.json index 382ac8f..1c9c28c 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -277,6 +277,18 @@ "description": "General email settings. Ignore if not using email features." }, "settings": { + "language": { + "name": "Email Language", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "select", + "options": [ + "en-us" + ], + "value": "en-us", + "description": "Default email language. Submit a PR on github if you'd like to translate." + }, "no_username": { "name": "Use email addresses as username", "required": false, diff --git a/email.go b/email.go index 10bd6c0..27b411e 100644 --- a/email.go +++ b/email.go @@ -73,6 +73,8 @@ func (sm *SMTP) send(address, fromName, fromAddr string, email *Email) error { // Emailer contains the email sender, email content, and methods to construct message content. type Emailer struct { fromAddr, fromName string + lang *EmailLang + cLang string sender emailClient } @@ -108,6 +110,8 @@ func NewEmailer(app *appContext) *Emailer { emailer := &Emailer{ fromAddr: app.config.Section("email").Key("address").String(), fromName: app.config.Section("email").Key("from").String(), + lang: &(app.storage.lang.Email), + cLang: app.storage.lang.chosenEmailLang, } method := app.config.Section("email").Key("method").String() if method == "smtp" { @@ -153,8 +157,9 @@ func (emailer *Emailer) NewSMTP(server string, port int, username, password stri } func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext) (*Email, error) { + lang := emailer.cLang email := &Email{ - subject: app.config.Section("invite_emails").Key("subject").String(), + subject: app.config.Section("invite_emails").Key("subject").MustString(emailer.lang.get(lang, "inviteEmail", "title")), } expiry := invite.ValidTill d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern) @@ -170,11 +175,13 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont } var tplData bytes.Buffer err = tpl.Execute(&tplData, map[string]string{ - "expiry_date": d, - "expiry_time": t, - "expires_in": expiresIn, - "invite_link": inviteLink, - "message": message, + "hello": emailer.lang.get(lang, "inviteEmail", "hello"), + "youHaveBeenInvited": emailer.lang.get(lang, "inviteEmail", "youHaveBeenInvited"), + "toJoin": emailer.lang.get(lang, "inviteEmail", "toJoin"), + "inviteExpiry": emailer.lang.format(lang, "inviteEmail", "inviteExpiry", d, t, expiresIn), + "linkButton": emailer.lang.get(lang, "inviteEmail", "linkButton"), + "invite_link": inviteLink, + "message": message, }) if err != nil { return nil, err @@ -189,8 +196,9 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont } func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext) (*Email, error) { + lang := emailer.cLang email := &Email{ - subject: "Notice: Invite expired", + subject: emailer.lang.get(lang, "inviteExpiry", "title"), } expiry := app.formatDatetime(invite.ValidTill) for _, key := range []string{"html", "text"} { @@ -201,8 +209,9 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont } var tplData bytes.Buffer err = tpl.Execute(&tplData, map[string]string{ - "code": code, - "expiry": expiry, + "inviteExpired": emailer.lang.get(lang, "inviteExpiry", "inviteExpired"), + "expiredAt": emailer.lang.format(lang, "inviteExpiry", "expiredAt", "\""+code+"\"", expiry), + "notificationNotice": emailer.lang.get(lang, "inviteExpiry", "notificationNotice"), }) if err != nil { return nil, err @@ -217,8 +226,9 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont } func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) (*Email, error) { + lang := emailer.cLang email := &Email{ - subject: "Notice: User created", + subject: emailer.lang.get(lang, "userCreated", "title"), } created := app.formatDatetime(invite.Created) var tplAddress string @@ -235,10 +245,14 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite } var tplData bytes.Buffer err = tpl.Execute(&tplData, map[string]string{ - "code": code, - "username": username, - "address": tplAddress, - "time": created, + "aUserWasCreated": emailer.lang.format(lang, "userCreated", "aUserWasCreated", "\""+code+"\""), + "name": emailer.lang.get(lang, "userCreated", "name"), + "address": emailer.lang.get(lang, "userCreated", "emailAddress"), + "time": emailer.lang.get(lang, "userCreated", "time"), + "nameVal": username, + "addressVal": tplAddress, + "timeVal": created, + "notificationNotice": emailer.lang.get(lang, "userCreated", "notificationNotice"), }) if err != nil { return nil, err @@ -253,8 +267,9 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite } func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Email, error) { + lang := emailer.cLang email := &Email{ - subject: app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin"), + subject: emailer.lang.get(lang, "passwordReset", "title"), } d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern) message := app.config.Section("email").Key("message").String() @@ -266,12 +281,14 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema } var tplData bytes.Buffer err = tpl.Execute(&tplData, map[string]string{ - "username": pwr.Username, - "expiry_date": d, - "expiry_time": t, - "expires_in": expiresIn, - "pin": pwr.Pin, - "message": message, + "helloUser": emailer.lang.format(lang, "passwordReset", "helloUser", pwr.Username), + "someoneHasRequestedReset": emailer.lang.get(lang, "passwordReset", "someoneHasRequestedReset"), + "ifItWasYou": emailer.lang.get(lang, "passwordReset", "ifItWasYou"), + "codeExpiry": emailer.lang.format(lang, "passwordReset", "codeExpiry", d, t, expiresIn), + "ifItWasNotYou": emailer.lang.get(lang, "passwordReset", "ifItWasNotYou"), + "pin": emailer.lang.get(lang, "passwordReset", "pin"), + "pinVal": pwr.Pin, + "message": message, }) if err != nil { return nil, err @@ -286,8 +303,9 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema } func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email, error) { + lang := emailer.cLang email := &Email{ - subject: app.config.Section("deletion").Key("subject").MustString("Your account was deleted - Jellyfin"), + subject: emailer.lang.get(lang, "userDeleted", "title"), } for _, key := range []string{"html", "text"} { fpath := app.config.Section("deletion").Key("email_" + key).String() @@ -297,7 +315,9 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email } var tplData bytes.Buffer err = tpl.Execute(&tplData, map[string]string{ - "reason": reason, + "yourAccountWasDeleted": emailer.lang.get(lang, "userDeleted", "yourAccountWasDeleted"), + "reason": emailer.lang.get(lang, "userDeleted", "reason"), + "reasonVal": reason, }) if err != nil { return nil, err diff --git a/lang/admin/fr-fr.json b/lang/admin/fr-fr.json new file mode 100644 index 0000000..72974b7 --- /dev/null +++ b/lang/admin/fr-fr.json @@ -0,0 +1,78 @@ +{ + "meta": { + "name": "Francais (FR)", + "author": "https://github.com/Killianbe" + }, + "strings": { + "invites": "Invite", + "accounts": "Comptes", + "settings": "Reglages", + "theme": "Thème", + "inviteDays": "Jours", + "inviteHours": "Heures", + "inviteMinutes": "Minutes", + "inviteNumberOfUses": "Nombre d'utilisateur", + "warning": "Attention", + "inviteInfiniteUsesWarning": "les invitations infinies peuvent être utilisées abusivement", + "inviteSendToEmail": "Envoyer à", + "login": "S'identifier", + "logout": "Se déconecter", + "create": "Créer", + "apply": "Appliquer", + "delete": "Effacer", + "submit": "Soumettre", + "name": "Nom", + "username": "Nom d'utilisateur", + "password": "Mot de passe", + "emailAddress": "Addresse Email", + "lastActiveTime": "Dernière activité", + "from": "De", + "user": "Utilisateur", + "aboutProgram": "A propros", + "version": "Version", + "commitNoun": "Commettre", + "newUser": "Nouvel utilisateur", + "profile": "Profil", + "modifySettings": "Modifier les paramètres", + "modifySettingsDescription": "Appliquez les paramètres à partir d'un profil existant ou obtenez-les directement auprès d'un utilisateur.", + "applyHomescreenLayout": "Appliquer la disposition de l'écran d'accueil", + "sendDeleteNotificationEmail": "Envoyer un e-mail de notification ", + "sendDeleteNotifiationExample": "Votre compte a été supprimé. ", + "settingsRestartRequired": "Redémarrage nécessaire ", + "settingsRestartRequiredDescription": "Un redémarrage est nécessaire pour appliquer certains paramètres que vous avez modifiés. Redémarrer maintenant ou plus tard?", + "settingsApplyRestartLater": "Appliquer, redémarrer plus tard ", + "settingsApplyRestartNow": "Appliquer et redémarrer ", + "settingsApplied": "Paramètres appliqués.", + "settingsRefreshPage": "Actualisez la page dans quelques secondes ", + "settingsRequiredOrRestartMessage": "Remarque: {n} indique un champ obligatoire, {n} indique que les modifications nécessitent un redémarrage. ", + "settingsSave": "Sauver", + "ombiUserDefaults": "Paramètres par défaut de l'utilisateur Ombi", + "ombiUserDefaultsDescription": "Créez un utilisateur Ombi et configurez-le, puis sélectionnez-le ci-dessous. Ses paramètres / autorisations seront stockés et appliqués aux nouveaux utilisateurs Ombi créés par jfa-go ", + "userProfiles": "Profils d'utilisateurs", + "userProfilesDescription": "Les profils sont appliqués aux utilisateurs lorsqu'ils créent un compte. Un profil inclut les droits d'accès à la bibliothèque et la disposition de l'écran d'accueil. ", + "userProfilesIsDefault": "Défaut", + "userProfilesLibraries": "Bibliothèques", + "addProfile": "Ajouter un profil", + "addProfileDescription": "Créez un utilisateur Jellyfin et configurez-le, puis sélectionnez-le ci-dessous. Lorsque ce profil est appliqué à une invitation, de nouveaux utilisateurs seront créés avec les paramètres. ", + "addProfileNameOf": "Nom de profil", + "addProfileStoreHomescreenLayout": "Enregistrer la disposition de l'écran d'accueil" + }, + "quantityStrings": { + "modifySettingsFor": { + "singular": "Modifier les paramètres pour {n} utilisateur", + "plural": "Modifier les paramètres pour {n} utilisateurs" + }, + "deleteNUsers": { + "singular": "Supprimer {n} utilisateur", + "plural": "Supprimer {n} utilisateurs" + }, + "addUser": { + "singular": "Ajouter un utilisateur", + "plural": "Ajouter des utilisateurs" + }, + "deleteUser": { + "singular": "Supprimer l'utilisateur", + "plural": "Supprimer les utilisateurs" + } + } +} diff --git a/lang/email/en-us.json b/lang/email/en-us.json new file mode 100644 index 0000000..5645229 --- /dev/null +++ b/lang/email/en-us.json @@ -0,0 +1,41 @@ +{ + "meta": { + "name": "English (US)" + }, + "userCreated": { + "title": "Notice: User created", + "aUserWasCreated": "A user was created using code {n}.", + "name": "Name", + "emailAddress": "Address", + "time": "Time", + "notificationNotice": "Note: Notification emails can be toggled on the admin dashboard." + }, + "inviteExpiry": { + "title": "Notice: Invite expired", + "inviteExpired": "Invite expired.", + "expiredAt": "Code {n} expired at {n}.", + "notificationNotice": "Note: Notification emails can be toggled on the admin dashboard." + }, + "passwordReset": { + "title": "Password reset requested - Jellyfin", + "helloUser": "Hi {n},", + "someoneHasRequestedReset": "Someone has recently requested a password reset on Jellyfin.", + "ifItWasYou": "If this was you, enter the pin below into the prompt.", + "codeExpiry": "The code will expire on {n}, at {n} UTC, which is in {n}.", + "ifItWasNotYou": "If this wasn't you, please ignore this email.", + "pin": "PIN" + }, + "userDeleted": { + "title": "Your account was deleted - Jellyfin", + "yourAccountWasDeleted": "Your Jellyfin account was deleted.", + "reason": "Reason" + }, + "inviteEmail": { + "title": "Invite - Jellyfin", + "hello": "Hi", + "youHaveBeenInvited": "You've been invited to Jellyfin.", + "toJoin": "To join, follow the below link.", + "inviteExpiry": "This invite will expire on {n}, at {n}, which is in {n}, so act quick.", + "linkButton": "Setup your account" + } +} diff --git a/lang/email/fr-fr.json b/lang/email/fr-fr.json new file mode 100644 index 0000000..40e7adf --- /dev/null +++ b/lang/email/fr-fr.json @@ -0,0 +1,32 @@ +{ + "meta": { + "name": "Francais (FR)", + "author": "https://github.com/Cornichon420" + }, + "userCreated": { + "aUserWasCreated": "Un utilisateur a été créé avec ce code {n}", + "name": "Nom", + "emailAddress": "Adresse", + "time": "Date", + "notificationNotice": "" + }, + "passwordReset": { + "helloUser": "Salut {n},", + "someoneHasRequestedReset": "Quelqu'un vient de demander une réinitialisation du mot de passe via Jellyfin.", + "ifItWasYou": "Si c'était bien toi, renseigne le code PIN en dessous.", + "codeExpiry": "Ce code expirera le {n}, à {n} UTC, soit dans {n}.", + "ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ce mail.", + "pin": "PIN" + }, + "userDeleted": { + "yourAccountWasDeleted": "Ton compte Jellyfin a été supprimé.", + "reason": "Motif" + }, + "inviteEmail": { + "hello": "Salut", + "youHaveBeenInvited": "Tu a été invité à rejoindre Jellyfin.", + "toJoin": "Pour continuer, suis le lien en dessous.", + "inviteExpiry": "L'invitation expirera le {n}, à {n}, sout dans {n}, alors fais vite !", + "linkButton": "Lien" + } +} diff --git a/mail/created.mjml b/mail/created.mjml index fe72fe6..ff012ce 100644 --- a/mail/created.mjml +++ b/mail/created.mjml @@ -20,26 +20,25 @@ -

User Created

-

A user was created using code {{ .code }}.

+

{{ .aUserWasCreated }}

- Name - Address - Time + {{ .name }} + {{ .address }} + {{ .time }} - {{ .username }} - {{ .address }} - {{ .time }} + {{ .nameVal }} + {{ .addressVal }} + {{ .timeVal }}
- Notification emails can be toggled on the admin dashboard. + {{ .notificationNotice }} diff --git a/mail/created.txt b/mail/created.txt index 7e3d3df..f48e6cf 100644 --- a/mail/created.txt +++ b/mail/created.txt @@ -1,7 +1,7 @@ -A user was created using code {{ .code }}. +{{ .aUserWasCreated }} -Name: {{ .username }} -Address: {{ .address }} -Time: {{ .time }} +{{ .name }}: {{ .nameVal }} +{{ .address }}: {{ .addressVal }} +{{ .time }}: {{ .timeVal }} -Note: Notification emails can be toggled on the admin dashboard. +{{ .notificationNotice }} diff --git a/mail/deleted.mjml b/mail/deleted.mjml index 78179e2..b4d6784 100644 --- a/mail/deleted.mjml +++ b/mail/deleted.mjml @@ -20,8 +20,8 @@ -

Your account was deleted.

-

Reason: {{ .reason }}

+

{{ .yourAccountWasDeleted }}

+

{{ .reason }}: {{ .reasonVal }}

diff --git a/mail/deleted.txt b/mail/deleted.txt index 19c07fb..ce6eb10 100644 --- a/mail/deleted.txt +++ b/mail/deleted.txt @@ -1,4 +1,4 @@ -Your Jellyfin account was deleted. -Reason: {{ .reason }} +{{ .yourAccountWasDeleted }} +{{ .reason }}: {{ .reasonVal }} {{ .message }} diff --git a/mail/email.mjml b/mail/email.mjml index fd1ea1e..71ae9e6 100644 --- a/mail/email.mjml +++ b/mail/email.mjml @@ -20,13 +20,13 @@ -

Hi {{ .username }},

-

Someone has recently requested a password reset on Jellyfin.

-

If this was you, enter the below pin into the prompt.

-

The code will expire on {{ .expiry_date }}, at {{ .expiry_time }} UTC, which is in {{ .expires_in }}.

-

If this wasn't you, please ignore this email.

+

{{ .helloUser }}

+

{{ .someoneHasRequestedReset }}

+

{{ .ifItWasYou }}

+

{{ .codeExpiry }}

+

{{ .ifItWasNotYou }}

- {{ .pin }} + {{ .pinVal }}
diff --git a/mail/email.txt b/mail/email.txt index 6bd59f8..fae1348 100644 --- a/mail/email.txt +++ b/mail/email.txt @@ -1,10 +1,10 @@ -Hi {{ .username }}, +{{ .helloUser }} -Someone has recently requests a password reset on Jellyfin. -If this was you, enter the below pin into the prompt. -This code will expire on {{ .expiry_date }}, at {{ .expiry_time }} UTC, which is in {{ .expires_in }}. -If this wasn't you, please ignore this email. +{{ .someoneHasRequestedReset }} +{{ .ifItWasYou }} +{{ .codeExpiry }} +{{ .ifItWasNotYou }} -PIN: {{ .pin }} +{{ .pin }}: {{ .pinVal }} {{ .message }} diff --git a/mail/expired.mjml b/mail/expired.mjml index 9f9c917..928c1ca 100644 --- a/mail/expired.mjml +++ b/mail/expired.mjml @@ -20,15 +20,15 @@ -

Invite Expired.

-

Code {{ .code }} expired at {{ .expiry }}.

+

{{ .inviteExpired }}

+

{{ .expiredAt }}

- Notification emails can be toggled on the admin dashboard. + {{ .notificationNotice }} diff --git a/mail/expired.txt b/mail/expired.txt index b834152..892892c 100644 --- a/mail/expired.txt +++ b/mail/expired.txt @@ -1,5 +1,5 @@ -Invite expired. +{{ .inviteExpired }} -Code {{ .code }} expired at {{ .expiry }}. +{{ .expiredAt }} -Note: Notification emails can be toggled on the admin dashboard. +{{ .notificationNotice }} diff --git a/mail/invite-email.mjml b/mail/invite-email.mjml index f5d688d..6b6f902 100644 --- a/mail/invite-email.mjml +++ b/mail/invite-email.mjml @@ -20,12 +20,12 @@ -

Hi,

-

You've been invited to Jellyfin.

-

To join, click the button below.

-

This invite will expire on {{ .expiry_date }}, at {{ .expiry_time }}, which is in {{ .expires_in }}, so act quick.

+

{{ .hello }},

+

{{ .youHaveBeenInvited }}

+

{{ .toJoin }}

+

{{ .inviteExpiry }}

- Setup your account + {{ .linkButton }}
diff --git a/mail/invite-email.txt b/mail/invite-email.txt index 10855f1..46cdd84 100644 --- a/mail/invite-email.txt +++ b/mail/invite-email.txt @@ -1,7 +1,7 @@ -Hi, -You've been invited to Jellyfin. -To join, follow the below link. -This invite will expire on {{ .expiry_date }}, at {{ .expiry_time }}, which is in {{ .expires_in }}, so act quick. +{{ .hello }}, +{{ .youHaveBeenInvited }} +{{ .toJoin }} +{{ .inviteExpiry }} {{ .invite_link }} diff --git a/main.go b/main.go index 3428014..9c0dad2 100644 --- a/main.go +++ b/main.go @@ -370,7 +370,6 @@ func start(asDaemon, firstCall bool) { app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String() app.storage.loadProfiles() - if !(len(app.storage.policy) == 0 && len(app.storage.configuration) == 0 && len(app.storage.displayprefs) == 0) { app.info.Println("Migrating user template files to new profile format") app.storage.migrateToProfile() @@ -515,6 +514,7 @@ func start(asDaemon, firstCall bool) { } 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") err = app.storage.loadLang() if err != nil { app.info.Fatalf("Failed to load language files: %+v\n", err) diff --git a/storage.go b/storage.go index dc24851..f61c50c 100644 --- a/storage.go +++ b/storage.go @@ -20,14 +20,28 @@ type Storage struct { 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 @@ -80,7 +94,7 @@ func (st *Storage) loadLang() error { if err != nil { file = []byte("{}") } - // Replace Jellyfin with emby on form + // Replace Jellyfin with something if necessary if substituteStrings != "" { fileString := strings.ReplaceAll(string(file), "Jellyfin", substituteStrings) file = []byte(fileString) @@ -121,6 +135,17 @@ func (st *Storage) loadLang() error { adminJSON, admin, err := loadData(st.lang.AdminPath, true) st.lang.Admin = admin st.lang.AdminJSON = adminJSON + + _, emails, err := loadData(st.lang.EmailPath, false) + fixedEmails := map[string]map[string]map[string]interface{}{} + for lang, e := range emails { + f := map[string]map[string]interface{}{} + for field, vals := range e { + f[field] = vals.(map[string]interface{}) + } + fixedEmails[lang] = f + } + st.lang.Email = fixedEmails return err }