From 4ac62a107ce6148fa0dae6ebd8beee16e6d0b7eb Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 12 Jan 2021 00:11:40 +0000 Subject: [PATCH 01/11] Start adding translation support for admin --- config.go | 1 + html/admin.html | 166 ++++++++++++++++++++---------------------- lang/admin/README.md | 5 ++ lang/admin/en-us.json | 80 ++++++++++++++++++++ main.go | 1 + storage.go | 81 ++++++++++++--------- views.go | 26 ++++--- 7 files changed, 229 insertions(+), 131 deletions(-) create mode 100644 lang/admin/README.md create mode 100644 lang/admin/en-us.json diff --git a/config.go b/config.go index 2e72432..347bed4 100644 --- a/config.go +++ b/config.go @@ -84,6 +84,7 @@ func (app *appContext) loadConfig() error { substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("") + app.storage.lang.chosenFormLang = app.config.Section("ui").Key("language").MustString("en-us") app.storage.lang.chosenFormLang = app.config.Section("ui").Key("language").MustString("en-us") return nil diff --git a/html/admin.html b/html/admin.html index d86e90b..71f6b83 100644 --- a/html/admin.html +++ b/html/admin.html @@ -16,164 +16,152 @@ @@ -182,40 +170,40 @@
- Invites - Accounts - Settings + {{ .strings.invites }} + {{ .strings.accounts }} + {{ .strings.settings }}
- Logout - Theme + {{ .strings.logout }} + {{ .strings.theme }}
- Invites + {{ .strings.invites }}
- Create + {{ .strings.create }}
- +
- +
- +
-

Warning invites with infinite uses can be used abusively.

- +

{{ .strings.warning }} {{ .strings.inviteInfiniteUsesWarning }}

+
- +
- Create + {{ .strings.create }}
- Accounts + {{ .strings.accounts }}
- Add User - Modify Settings - Delete User + {{ .quantityStrings.addUser.singular }} + {{ .strings.modifySettings }} + {{ .quantityStrings.deleteUser.singular }}
- - - + + + @@ -276,15 +264,15 @@
- Settings + {{ .string.settings }}
- Save + {{ .strings.settingsSave }}
- - About - User profiles + + {{ .strings.aboutProgram }} + {{ .strings.userProfiles }}
diff --git a/lang/admin/README.md b/lang/admin/README.md new file mode 100644 index 0000000..5bb2bba --- /dev/null +++ b/lang/admin/README.md @@ -0,0 +1,5 @@ +##### admin page translation + +* [x] static page content +* [ ] Typescript: + * [x] accounts.ts diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json new file mode 100644 index 0000000..24862a1 --- /dev/null +++ b/lang/admin/en-us.json @@ -0,0 +1,80 @@ +{ + "meta": { + "name": "English (US)" + }, + "strings": { + "invites": "Invites", + "accounts": "Accounts", + "settings": "Settings", + "theme": "Theme", + "inviteDays": "Days", + "inviteHours": "Hours", + "inviteMinutes": "Minutes", + "inviteNumberOfUses": "Number of uses", + "warning": "Warning", + "inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively", + "inviteSendToEmail": "Send to", + "login": "Login", + "logout": "Logout", + "create": "Create", + "apply": "Apply", + "delete": "Delete", + "submit": "Submit", + "name": "Name", + "username": "Username", + "password": "Password", + "emailAddress": "Email Address", + "lastActiveTime": "Last Active", + "from": "From", + "user": "User", + "aboutProgram": "About", + "version": "Version", + "commitNoun": "Commit", + "newUser": "New User", + "profile": "Profile", + "modifySettings": "Modify Settings", + "modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.", + "applyHomescreenLayout": "Apply homescreen layout", + "sendDeleteNotificationEmail": "Send notification email", + "sendDeleteNotifiationExample": "Your account has been deleted.", + "settingsRestartRequired": "Restart needed", + "settingsRestartRequiredDescription": "A restart is necessary to apply some settings you changed. Restart now or later?", + "settingsApplyRestartLater": "Apply, restart later", + "settingsApplyRestartNow": "Apply & restart", + "settingsApplied": "Settings applied.", + "settingsRefreshPage": "Refresh the page in a few seconds", + "settingsRequiredOrRestartMessage": "Note: {*} indicates a required field, {R} indicates changes require a restart.", + "settingsSave": "Save", + "ombiUserDefaults": "Ombi user defaults", + "ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go", + "userProfiles": "User Profiles", + "userProfilesDescription": "Profiles are applied to users when they create an account. A profile include library access rights and homescreen layout.", + "userProfilesIsDefault": "Default", + "userProfilesLibraries": "Libraries", + "addProfile": "Add Profile", + "addProfileDescription": "Create a Jellyfin user and configure it, then select it below. When this profile is applied to an invite, new users will be created with the settings.", + "addProfileNameOf": "Profile Name", + "addProfileStoreHomescreenLayout": "Store homescreen layout" + }, + "variableStrings": { + "settingsRequiredOrRestartMessage": "Note: {*} indicates a required field, {R} indicates changes require a restart." + }, + "quantityStrings": { + "modifySettingsFor": { + "singular": "Modify Settings for {n} user", + "plural": "Modify Settings for {n} users" + }, + "deleteNUsers": { + "singular": "Delete {n} user", + "plural": "Delete {n} users" + }, + "addUser": { + "singular": "Add user", + "plural": "Add users" + }, + "deleteUser": { + "singular": "Delete User", + "plural": "Delete Users" + } + } +} diff --git a/main.go b/main.go index 5f5a8c7..06ace3d 100644 --- a/main.go +++ b/main.go @@ -514,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") 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 e5b9ea6..01182ef 100644 --- a/storage.go +++ b/storage.go @@ -21,9 +21,12 @@ type Storage struct { } type Lang struct { - chosenFormLang string - FormPath string - Form map[string]map[string]interface{} + chosenFormLang string + chosenAdminLang string + AdminPath string + Admin map[string]map[string]interface{} + FormPath string + Form map[string]map[string]interface{} } // timePattern: %Y-%m-%dT%H:%M:%S.%f @@ -60,46 +63,58 @@ func (st *Storage) storeInvites() error { } func (st *Storage) loadLang() error { - formFiles, err := ioutil.ReadDir(st.lang.FormPath) - st.lang.Form = map[string]map[string]interface{}{} + loadData := func(path string) (map[string]map[string]interface{}, error) { + files, err := ioutil.ReadDir(path) + out := map[string]map[string]interface{}{} + if err != nil { + return nil, err + } + for _, f := range files { + index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) + var data map[string]interface{} + if substituteStrings != "" { + var file []byte + var err error + file, err = ioutil.ReadFile(filepath.Join(path, f.Name())) + if err != nil { + file = []byte("{}") + } + // Replace Jellyfin with emby on form + file = []byte(strings.ReplaceAll(string(file), "Jellyfin", substituteStrings)) + err = json.Unmarshal(file, &data) + if err != nil { + log.Printf("ERROR: Failed to read \"%s\": %s", path, err) + return nil, err + } + } else { + err := loadJSON(filepath.Join(path, f.Name()), &data) + if err != nil { + return nil, err + } + } + out[index] = data + } + return out, nil + } + form, err := loadData(st.lang.FormPath) if err != nil { return err } - for _, f := range formFiles { - index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) - var data map[string]interface{} - if substituteStrings != "" { - var file []byte - var err error - file, err = ioutil.ReadFile(filepath.Join(st.lang.FormPath, f.Name())) - if err != nil { - file = []byte("{}") - } - // Replace Jellyfin with emby on form - file = []byte(strings.ReplaceAll(string(file), "Jellyfin", substituteStrings)) - err = json.Unmarshal(file, &data) - if err != nil { - log.Printf("ERROR: Failed to read \"%s\": %s", st.lang.FormPath, err) - return err - } - } else { - err := loadJSON(filepath.Join(st.lang.FormPath, f.Name()), &data) - if err != nil { - return err - } - } - - strings := data["strings"].(map[string]interface{}) + for index, lang := range form { + strings := lang["strings"].(map[string]interface{}) validationStrings := strings["validationStrings"].(map[string]interface{}) vS, err := json.Marshal(validationStrings) if err != nil { return err } strings["validationStrings"] = string(vS) - data["strings"] = strings - st.lang.Form[index] = data + lang["strings"] = strings + form[index] = lang } - return nil + st.lang.Form = form + admin, err := loadData(st.lang.AdminPath) + st.lang.Admin = admin + return err } func (st *Storage) loadEmails() error { diff --git a/views.go b/views.go index 50b038e..2a42d74 100644 --- a/views.go +++ b/views.go @@ -13,19 +13,27 @@ func gcHTML(gc *gin.Context, code int, file string, templ gin.H) { } func (app *appContext) AdminPage(gc *gin.Context) { + lang := gc.Query("lang") + if lang == "" { + lang = app.storage.lang.chosenFormLang + } else if _, ok := app.storage.lang.Form[lang]; !ok { + lang = app.storage.lang.chosenFormLang + } emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) gcHTML(gc, http.StatusOK, "admin.html", gin.H{ - "urlBase": app.URLBase, - "cssClass": app.cssClass, - "contactMessage": "", - "email_enabled": emailEnabled, - "notifications": notificationsEnabled, - "version": VERSION, - "commit": COMMIT, - "ombiEnabled": ombiEnabled, - "username": !app.config.Section("email").Key("no_username").MustBool(false), + "urlBase": app.URLBase, + "cssClass": app.cssClass, + "contactMessage": "", + "email_enabled": emailEnabled, + "notifications": notificationsEnabled, + "version": VERSION, + "commit": COMMIT, + "ombiEnabled": ombiEnabled, + "username": !app.config.Section("email").Key("no_username").MustBool(false), + "strings": app.storage.lang.Admin[lang]["strings"], + "quantityStrings": app.storage.lang.Admin[lang]["quantityStrings"], }) } From c72282613d53212199640d8131c23e2c9f722d93 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 12 Jan 2021 23:15:12 +0000 Subject: [PATCH 02/11] Use lang file in typescript --- html/admin.html | 3 ++- lang/admin/README.md | 5 ---- lang/admin/en-us.json | 58 +++++++++++++++++++++++++++++++++++++++--- storage.go | 49 ++++++++++++++++++++--------------- ts/admin.ts | 19 +++++++++++--- ts/modules/accounts.ts | 32 ++++++++++------------- ts/modules/common.ts | 12 +++++---- ts/modules/invites.ts | 36 +++++++++++++------------- ts/modules/lang.ts | 52 +++++++++++++++++++++++++++++++++++++ ts/modules/profiles.ts | 16 ++++++------ ts/modules/settings.ts | 31 ++++++++++++---------- ts/typings/d.ts | 12 +++++++++ views.go | 1 + 13 files changed, 229 insertions(+), 97 deletions(-) delete mode 100644 lang/admin/README.md create mode 100644 ts/modules/lang.ts diff --git a/html/admin.html b/html/admin.html index 71f6b83..2227b52 100644 --- a/html/admin.html +++ b/html/admin.html @@ -9,6 +9,7 @@ window.emailEnabled = {{ .email_enabled }}; window.ombiEnabled = {{ .ombiEnabled }}; window.usernamesEnabled = {{ .username }}; + window.langFile = JSON.parse({{ .language }}); {{ template "header.html" . }} Admin - jfa-go @@ -264,7 +265,7 @@
- {{ .string.settings }} + {{ .strings.settings }}
{{ .strings.settingsSave }}
diff --git a/lang/admin/README.md b/lang/admin/README.md deleted file mode 100644 index 5bb2bba..0000000 --- a/lang/admin/README.md +++ /dev/null @@ -1,5 +0,0 @@ -##### admin page translation - -* [x] static page content -* [ ] Typescript: - * [x] accounts.ts diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index 24862a1..f545fd0 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -21,6 +21,7 @@ "delete": "Delete", "submit": "Submit", "name": "Name", + "date": "Date", "username": "Username", "password": "Password", "emailAddress": "Email Address", @@ -32,6 +33,9 @@ "commitNoun": "Commit", "newUser": "New User", "profile": "Profile", + "success": "Success", + "error": "Error", + "unknown": "Unknown", "modifySettings": "Modify Settings", "modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.", "applyHomescreenLayout": "Apply homescreen layout", @@ -43,7 +47,7 @@ "settingsApplyRestartNow": "Apply & restart", "settingsApplied": "Settings applied.", "settingsRefreshPage": "Refresh the page in a few seconds", - "settingsRequiredOrRestartMessage": "Note: {*} indicates a required field, {R} indicates changes require a restart.", + "settingsRequiredOrRestartMessage": "Note: {n} indicates a required field, {n} indicates changes require a restart.", "settingsSave": "Save", "ombiUserDefaults": "Ombi user defaults", "ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go", @@ -54,11 +58,49 @@ "addProfile": "Add Profile", "addProfileDescription": "Create a Jellyfin user and configure it, then select it below. When this profile is applied to an invite, new users will be created with the settings.", "addProfileNameOf": "Profile Name", - "addProfileStoreHomescreenLayout": "Store homescreen layout" + "addProfileStoreHomescreenLayout": "Store homescreen layout", + + "inviteNoUsersCreated": "None yet!", + "inviteUsersCreated": "Created users", + "inviteNoProfile": "No Profile", + "copy": "Copy", + "inviteDateCreated": "Created", + "inviteRemainingUses": "Remaining uses", + "inviteNoInvites": "None", + "inviteExpiresInTime": "Expires in {n}", + + "notifyEvent": "Notify on:", + "notifyInviteExpiry": "On expiry", + "notifyUserCreation": "On user creation" }, - "variableStrings": { - "settingsRequiredOrRestartMessage": "Note: {*} indicates a required field, {R} indicates changes require a restart." + "notifications": { + "changedEmailAddress": "Changed email address of {n}.", + "userCreated": "User {n} created.", + "createProfile": "Created profile {n}.", + "saveSettings": "Settings were saved", + "setOmbiDefaults": "Stored ombi defaults.", + "errorConnection": "Couldn't connect to jfa-go.", + "error401Unauthorized": "Unauthorized. Try refreshing the page.", + "errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.", + "errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.", + "errorSettingsFailed": "Application failed.", + "errorLoginBlank": "The username and/or password were left blank.", + "errorUnknown": "Unknown error.", + "errorBlankFields": "Fields were left blank", + "errorDeleteProfile": "Failed to delete profile {n}", + "errorLoadProfiles": "Failed to load profiles.", + "errorCreateProfile": "Failed to create profile {n}", + "errorSetDefaultProfile": "Failed to set default profile.", + "errorLoadUsers": "Failed to load users.", + "errorSaveSettings": "Couldn't save settings.", + "errorLoadSettings": "Failed to load settings.", + "errorSetOmbiDefaults": "Failed to store ombi defaults.", + "errorLoadOmbiUsers": "Failed to load ombi users.", + "errorChangedEmailAddress": "Couldn't change email address of {n}.", + "errorFailureCheckLogs": "Failed (check console/logs)", + "errorPartialFailureCheckLogs": "Partial failure (check console/logs)" }, + "quantityStrings": { "modifySettingsFor": { "singular": "Modify Settings for {n} user", @@ -75,6 +117,14 @@ "deleteUser": { "singular": "Delete User", "plural": "Delete Users" + }, + "deletedUser": { + "singular": "Deleted {n} user.", + "plural": "Deleted {n} users." + }, + "appliedSettings": { + "singular": "Applied settings to {n} user.", + "plural": "Applied settings to {n} users." } } } diff --git a/storage.go b/storage.go index 01182ef..dc24851 100644 --- a/storage.go +++ b/storage.go @@ -25,6 +25,7 @@ type Lang struct { chosenAdminLang string AdminPath string Admin map[string]map[string]interface{} + AdminJSON map[string]string FormPath string Form map[string]map[string]interface{} } @@ -63,40 +64,45 @@ func (st *Storage) storeInvites() error { } func (st *Storage) loadLang() error { - loadData := func(path 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) + outString := map[string]string{} out := map[string]map[string]interface{}{} if err != nil { - return nil, err + return nil, nil, err } for _, f := range files { index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) var data map[string]interface{} + var file []byte + var err error + file, err = ioutil.ReadFile(filepath.Join(path, f.Name())) + if err != nil { + file = []byte("{}") + } + // Replace Jellyfin with emby on form if substituteStrings != "" { - var file []byte - var err error - file, err = ioutil.ReadFile(filepath.Join(path, f.Name())) - if err != nil { - file = []byte("{}") - } - // Replace Jellyfin with emby on form - file = []byte(strings.ReplaceAll(string(file), "Jellyfin", substituteStrings)) - err = json.Unmarshal(file, &data) - if err != nil { - log.Printf("ERROR: Failed to read \"%s\": %s", path, err) - return nil, err - } - } else { - err := loadJSON(filepath.Join(path, f.Name()), &data) + fileString := strings.ReplaceAll(string(file), "Jellyfin", substituteStrings) + file = []byte(fileString) + } + err = json.Unmarshal(file, &data) + if err != nil { + log.Printf("ERROR: Failed to read \"%s\": %s", path, err) + return nil, nil, err + } + if stringJson { + stringJSON, err := json.Marshal(data) if err != nil { - return nil, err + return nil, nil, err } + outString[index] = string(stringJSON) } out[index] = data + } - return out, nil + return outString, out, nil } - form, err := loadData(st.lang.FormPath) + _, form, err := loadData(st.lang.FormPath, false) if err != nil { return err } @@ -112,8 +118,9 @@ func (st *Storage) loadLang() error { form[index] = lang } st.lang.Form = form - admin, err := loadData(st.lang.AdminPath) + adminJSON, admin, err := loadData(st.lang.AdminPath, true) st.lang.Admin = admin + st.lang.AdminJSON = adminJSON return err } diff --git a/ts/admin.ts b/ts/admin.ts index f44a4a4..5811af3 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -1,15 +1,26 @@ import { toggleTheme, loadTheme } from "./modules/theme.js"; +import { lang, LangFile } from "./modules/lang.js"; import { Modal } from "./modules/modal.js"; import { Tabs } from "./modules/tabs.js"; import { inviteList, createInvite } from "./modules/invites.js"; import { accountsList } from "./modules/accounts.js"; import { settingsList } from "./modules/settings.js"; import { ProfileEditor } from "./modules/profiles.js"; -import { _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js"; +import { _get, _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js"; loadTheme(); (document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme; +var langLoaded = false; + +window.lang = new lang(window.langFile as LangFile); +// _get(`/lang/admin/${window.language}.json`, null, (req: XMLHttpRequest) => { +// if (req.readyState == 4 && req.status == 200) { +// langLoaded = true; +// window.lang = new lang(req.response as LangFile); +// } +// }); + window.animationEvent = whichAnimationEvent(); window.token = ""; @@ -110,12 +121,12 @@ function login(username: string, password: string, run?: (state?: number) => voi req.onreadystatechange = function (): void { if (this.readyState == 4) { if (this.status != 200) { - let errorMsg = "Connection error."; + let errorMsg = window.lang.notif("errorConnection"); if (this.response) { errorMsg = this.response["error"]; } if (!errorMsg) { - errorMsg = "Unknown error"; + errorMsg = window.lang.notif("errorUnknown"); } if (!refresh) { window.notifications.customError("loginError", errorMsg); @@ -153,7 +164,7 @@ function login(username: string, password: string, run?: (state?: number) => voi const username = (document.getElementById("login-user") as HTMLInputElement).value; const password = (document.getElementById("login-password") as HTMLInputElement).value; if (!username || !password) { - window.notifications.customError("loginError", "The username and/or password were left blank."); + window.notifications.customError("loginError", window.lang.notif("errorLoginBlank")); return; } toggleLoader(button); diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 80c1593..1b78129 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -100,10 +100,10 @@ class user implements User { _post("/users/emails", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 200) { - window.notifications.customPositive("emailChanged", "Success:", `Changed email address of "${this.name}".`); + window.notifications.customSuccess("emailChanged", window.lang.var("notifications", "changedEmailAddress", `"${this.name}"`)); } else { this.email = oldEmail; - window.notifications.customError("emailChanged", `Couldn't change email address of "${this.name}".`); + window.notifications.customError("emailChanged", window.lang.var("notifications", "errorChangedEmailAddress", `"${this.name}"`)); } } }); @@ -184,11 +184,9 @@ export class accountsList { } this._modifySettings.classList.remove("unfocused"); this._deleteUser.classList.remove("unfocused"); - (this._checkCount == 1) ? this._deleteUser.textContent = "Delete User" : this._deleteUser.textContent = "Delete Users"; + this._deleteUser.textContent = window.lang.quantity("deleteUser", this._checkCount); } } - - private _genCountString = (): string => { return `${this._checkCount} user${(this._checkCount > 1) ? "s" : ""}`; } private _collectUsers = (): string[] => { let list: string[] = []; @@ -208,7 +206,7 @@ export class accountsList { }; for (let field in send) { if (!send[field]) { - window.notifications.customError("addUserBlankField", "Fields were left blank."); + window.notifications.customError("addUserBlankField", window.lang.notif("errorBlankFields")); return; } } @@ -217,7 +215,7 @@ export class accountsList { if (req.readyState == 4) { toggleLoader(button); if (req.status == 200) { - window.notifications.customPositive("addUser", "Success:", `user "${send['username']}" created.`); + window.notifications.customSuccess("addUser", window.lang.var("notifications", "userCreated", `"${send['username']}"`)); } this.reload(); window.modals.addUser.close(); @@ -227,7 +225,7 @@ export class accountsList { deleteUsers = () => { const modalHeader = document.getElementById("header-delete-user"); - modalHeader.textContent = this._genCountString(); + modalHeader.textContent = window.lang.quantity("deleteNUsers", this._checkCount); let list = this._collectUsers(); const form = document.getElementById("form-delete-user") as HTMLFormElement; const button = form.querySelector("span.submit") as HTMLSpanElement; @@ -247,13 +245,13 @@ export class accountsList { toggleLoader(button); window.modals.deleteUser.close(); if (req.status != 200 && req.status != 204) { - let errorMsg = "Failed (check console/logs)."; + let errorMsg = window.lang.notif("errorFailureCheckLogs"); if (!("error" in req.response)) { - errorMsg = "Partial failure (check console/logs)."; + errorMsg = window.lang.notif("errorPartialFailureCheckLogs"); } window.notifications.customError("deleteUserError", errorMsg); } else { - window.notifications.customPositive("deleteUserSuccess", "Success:", `deleted ${this._genCountString()}.`); + window.notifications.customSuccess("deleteUserSuccess", window.lang.quantity("deletedUser", this._checkCount)); } this.reload(); } @@ -264,7 +262,7 @@ export class accountsList { modifyUsers = () => { const modalHeader = document.getElementById("header-modify-user"); - modalHeader.textContent = this._genCountString(); + modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._checkCount) let list = this._collectUsers(); (() => { let innerHTML = ""; @@ -310,18 +308,18 @@ export class accountsList { const homescreen = Object.keys(response["homescreen"]).length; const policy = Object.keys(response["policy"]).length; if (homescreen != 0 && policy == 0) { - errorMsg = "Settings were applied, but applying homescreen layout may have failed."; + errorMsg = window.lang.notif("errorSettingsAppliedNoHomescreenLayout"); } else if (policy != 0 && homescreen == 0) { - errorMsg = "Homescreen layout was applied, but applying settings may have failed."; + errorMsg = window.lang.notif("errorHomescreenAppliedNoSettings"); } else if (policy != 0 && homescreen != 0) { - errorMsg = "Application failed."; + errorMsg = window.lang.notif("errorSettingsFailed"); } } else if ("error" in response) { errorMsg = response["error"]; } window.notifications.customError("modifySettingsError", errorMsg); } else if (req.status == 200 || req.status == 204) { - window.notifications.customPositive("modifySettingsSuccess", "Success:", `applied settings to ${this._genCountString()}.`); + window.notifications.customSuccess("modifySettingsSuccess", window.lang.quantity("appliedSettings", this._checkCount)); } this.reload(); window.modals.modifyUser.close(); @@ -331,8 +329,6 @@ export class accountsList { window.modals.modifyUser.show(); } - - constructor() { this._users = {}; this._selectAll.checked = false; diff --git a/ts/modules/common.ts b/ts/modules/common.ts index 7dfe918..b3b4017 100644 --- a/ts/modules/common.ts +++ b/ts/modules/common.ts @@ -60,7 +60,7 @@ export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHtt window.notifications.connectionError(); return; } else if (req.status == 401) { - window.notifications.customError("401Error", "Unauthorized. Try logging back in."); + window.notifications.customError("401Error", window.lang.notif("error401Unauthorized")); } onreadystatechange(req); }; @@ -80,7 +80,7 @@ export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHt window.notifications.connectionError(); return; } else if (req.status == 401) { - window.notifications.customError("401Error", "Unauthorized. Try logging back in."); + window.notifications.customError("401Error", window.lang.notif("error401Unauthorized")); } onreadystatechange(req); }; @@ -97,7 +97,7 @@ export function _delete(url: string, data: Object, onreadystatechange: (req: XML window.notifications.connectionError(); return; } else if (req.status == 401) { - window.notifications.customError("401Error", "Unauthorized. Try logging back in."); + window.notifications.customError("401Error", window.lang.notif("error401Unauthorized")); } onreadystatechange(req); }; @@ -131,7 +131,7 @@ export class notificationBox implements NotificationBox { private _error = (message: string): HTMLElement => { const noti = document.createElement('aside'); noti.classList.add("aside", "~critical", "!normal", "mt-half", "notification-error"); - noti.innerHTML = `Error: ${message}`; + noti.innerHTML = `${window.lang.strings("error")}: ${message}`; const closeButton = document.createElement('span') as HTMLSpanElement; closeButton.classList.add("button", "~critical", "!low", "ml-1"); closeButton.innerHTML = ``; @@ -152,7 +152,7 @@ export class notificationBox implements NotificationBox { return noti; } - connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go."); } + connectionError = () => { this.customError("connectionError", window.lang.notif("errorConnection")); } customError = (type: string, message: string) => { this._errorTypes[type] = this._errorTypes[type] || false; @@ -179,6 +179,8 @@ export class notificationBox implements NotificationBox { this._positiveTypes[type] = true; setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._positiveTypes[type] = false; } }, this.timeout*1000); } + + customSuccess = (type: string, message: string) => this.customPositive(type, window.lang.strings("success") + ":", message) } export const whichAnimationEvent = () => { diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts index afbdff6..1d05475 100644 --- a/ts/modules/invites.ts +++ b/ts/modules/invites.ts @@ -92,7 +92,7 @@ export class DOMInvite implements Invite { this._usedBy = uB; if (uB.length == 0) { this._right.classList.add("empty"); - this._userTable.innerHTML = `

None yet!

`; + this._userTable.innerHTML = `

${window.lang.strings("inviteNoUsersCreated")}

`; return; } this._right.classList.remove("empty"); @@ -100,8 +100,8 @@ export class DOMInvite implements Invite {
UsernameEmail AddressLast Active{{ .strings.username }}{{ .strings.emailAddress }}{{ .strings.lastActiveTime }}
- - + + @@ -153,7 +153,7 @@ export class DOMInvite implements Invite { } else { selected = selected || select.value; } - let innerHTML = ``; + let innerHTML = ``; for (let profile of window.availableProfiles) { innerHTML += ``; } @@ -221,7 +221,7 @@ export class DOMInvite implements Invite { this._codeArea.classList.add("inv-codearea"); this._codeArea.innerHTML = ` - + `; const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement; copyButton.onclick = () => { @@ -248,7 +248,7 @@ export class DOMInvite implements Invite { - Delete + ${window.lang.strings("delete")} - + `; this._name = this._row.querySelector("b.profile-name"); this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement; @@ -71,7 +71,7 @@ class profile implements Profile { if (req.status == 200 || req.status == 204) { this.remove(); } else { - window.notifications.customError("profileDelete", `Failed to delete profile "${this.name}"`); + window.notifications.customError("profileDelete", window.lang.var("notifications", "errorDeleteProfile", `"${this.name}"`)); } } }) @@ -98,7 +98,7 @@ export class ProfileEditor { get empty(): boolean { return (Object.keys(this._table.children).length == 0) } set empty(state: boolean) { if (state) { - this._table.innerHTML = `` + this._table.innerHTML = `` } else if (this._table.querySelector("td.empty")) { this._table.textContent = ``; } @@ -133,7 +133,7 @@ export class ProfileEditor { this.default = resp.default_profile; window.modals.profiles.show(); } else { - window.notifications.customError("profileEditor", "Failed to load profiles."); + window.notifications.customError("profileEditor", window.lang.notif("errorLoadProfiles")); } } }) @@ -149,7 +149,7 @@ export class ProfileEditor { this.default = newDefault; } else { this.default = prevDefault; - window.notifications.customError("profileDefault", "Failed to set default profile."); + window.notifications.customError("profileDefault", window.lang.notif("errorSetDefaultProfile")); } } }); @@ -171,7 +171,7 @@ export class ProfileEditor { window.modals.profiles.close(); window.modals.addProfile.show(); } else { - window.notifications.customError("loadUsers", "Failed to load users."); + window.notifications.customError("loadUsers", window.lang.notif("errorLoadUsers")); } } }); @@ -191,9 +191,9 @@ export class ProfileEditor { window.modals.addProfile.close(); if (req.status == 200 || req.status == 204) { this.load(); - window.notifications.customPositive("createProfile", "Success:", `created profile "${send['name']}"`); + window.notifications.customSuccess("createProfile", window.lang.var("notifications", "createProfile", `"${send['name']}"`)); } else { - window.notifications.customError("createProfile", `Failed to create profile "${send['name']}"`); + window.notifications.customError("createProfile", window.lang.var("notifications", "errorCreateProfile", `"${send['name']}"`)); } window.modals.profiles.show(); } diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index 0467dcd..bf63642 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -345,6 +345,17 @@ class DOMSelect implements SSelect { if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); } }; this._select.onchange = onValueChange; + + const message = document.getElementById("settings-message") as HTMLElement; + message.innerHTML = window.lang.var("strings", + "settingsRequiredOrRestartMessage", + `*`, + `R` + ); + + + + this.update(setting); } update = (s: SSelect) => { @@ -501,9 +512,9 @@ export class settingsList { private _send = (config: Object, run?: () => void) => _post("/config", config, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 200 || req.status == 204) { - window.notifications.customPositive("settingsSaved", "Success:", "settings were saved."); + window.notifications.customSuccess("settingsSaved", window.lang.notif("saveSettings")); } else { - window.notifications.customError("settingsSaved", "Couldn't save settings."); + window.notifications.customError("settingsSaved", window.lang.notif("errorSaveSettings")); } this.reload(); if (run) { run(); } @@ -526,7 +537,7 @@ export class settingsList { reload = () => _get("/config", null, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status != 200) { - window.notifications.customError("settingsLoadError", "Failed to load settings."); + window.notifications.customError("settingsLoadError", window.lang.notif("errorLoadSettings")); return; } let settings = req.response as Settings; @@ -558,7 +569,7 @@ class ombiDefaults { constructor() { this._button = document.createElement("span") as HTMLSpanElement; this._button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half"); - this._button.innerHTML = `Ombi user defaults `; + this._button.innerHTML = `${window.lang.strings("ombiUserDefaults")} `; this._button.onclick = this.load; this._form = document.getElementById("form-ombi-defaults") as HTMLFormElement; this._form.onsubmit = this.send; @@ -575,9 +586,9 @@ class ombiDefaults { if (req.readyState == 4) { toggleLoader(button); if (req.status == 200 || req.status == 204) { - window.notifications.customPositive("ombiDefaults", "Success:", "stored ombi defaults."); + window.notifications.customSuccess("ombiDefaults", window.lang.notif("setOmbiDefaults")); } else { - window.notifications.customError("ombiDefaults", "Failed to store ombi defaults."); + window.notifications.customError("ombiDefaults", window.lang.notif("errorSetOmbiDefaults")); } window.modals.ombiDefaults.close(); } @@ -600,15 +611,9 @@ class ombiDefaults { window.modals.ombiDefaults.show(); } else { toggleLoader(this._button); - window.notifications.customError("ombiLoadError", "Failed to load ombi users.") + window.notifications.customError("ombiLoadError", window.lang.notif("errorLoadOmbiUsers")) } } }); } } - - - - - - diff --git a/ts/typings/d.ts b/ts/typings/d.ts index 7c2b8c3..71ed557 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -27,12 +27,24 @@ declare interface Window { tabs: Tabs; invites: inviteList; notifications: NotificationBox; + language: string; + lang: Lang; + langFile: {}; +} + +declare interface Lang { + get: (sect: string, key: string) => string; + strings: (key: string) => string; + notif: (key: string) => string; + var: (sect: string, key: string, ...subs: string[]) => string; + quantity: (key: string, number: number) => string; } declare interface NotificationBox { connectionError: () => void; customError: (type: string, message: string) => void; customPositive: (type: string, bold: string, message: string) => void; + customSuccess: (type: string, message: string) => void; } declare interface Tabs { diff --git a/views.go b/views.go index 2a42d74..f3aea03 100644 --- a/views.go +++ b/views.go @@ -34,6 +34,7 @@ func (app *appContext) AdminPage(gc *gin.Context) { "username": !app.config.Section("email").Key("no_username").MustBool(false), "strings": app.storage.lang.Admin[lang]["strings"], "quantityStrings": app.storage.lang.Admin[lang]["quantityStrings"], + "language": app.storage.lang.AdminJSON[lang], }) } From 2d2727f7e8bf07b241d3b87fc1462ea6374017ab Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 12 Jan 2021 23:37:22 +0000 Subject: [PATCH 03/11] separate options for form and admin language --- api.go | 46 +++++++++++++++++++++++++++-------------- config.go | 11 ++++++++-- config/config-base.json | 15 ++++++++++++-- views.go | 4 ++-- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/api.go b/api.go index ffcd5e9..ab7b7b7 100644 --- a/api.go +++ b/api.go @@ -1085,17 +1085,25 @@ func (app *appContext) GetConfig(gc *gin.Context) { app.info.Println("Config requested") resp := app.configBase // Load language options - langOptions := make([]string, len(app.storage.lang.Form)) - chosenLang := app.config.Section("ui").Key("language").MustString("en-us") - chosenLangName := app.storage.lang.Form[chosenLang]["meta"].(map[string]interface{})["name"].(string) - i := 0 - for _, lang := range app.storage.lang.Form { - langOptions[i] = lang["meta"].(map[string]interface{})["name"].(string) - i++ - } - l := resp.Sections["ui"].Settings["language"] - l.Options = langOptions - l.Value = chosenLangName + loadLangs := func(langs *map[string]map[string]interface{}, settingsKey string) (string, []string) { + 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.Options = formOptions + fl.Value = formChosen + adminChosen, adminOptions := loadLangs(&app.storage.lang.Admin, "admin") + al := resp.Sections["ui"].Settings["language-admin"] + al.Options = adminOptions + al.Value = adminChosen for sectName, section := range resp.Sections { for settingName, setting := range section.Settings { val := app.config.Section(sectName).Key(settingName) @@ -1111,11 +1119,12 @@ func (app *appContext) GetConfig(gc *gin.Context) { resp.Sections[sectName].Settings[settingName] = s } } - resp.Sections["ui"].Settings["language"] = l + resp.Sections["ui"].Settings["language-form"] = fl + resp.Sections["ui"].Settings["language-admin"] = al t := resp.Sections["jellyfin"].Settings["type"] opts := make([]string, len(serverTypes)) - i = 0 + i := 0 for _, v := range serverTypes { opts[i] = v i++ @@ -1146,10 +1155,17 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { tempConfig.NewSection(section) } for setting, value := range settings.(map[string]interface{}) { - if section == "ui" && setting == "language" { + if section == "ui" && setting == "language-form" { for key, lang := range app.storage.lang.Form { if lang["meta"].(map[string]interface{})["name"].(string) == value.(string) { - tempConfig.Section("ui").Key("language").SetValue(key) + tempConfig.Section("ui").Key("language-form").SetValue(key) + break + } + } + } else if section == "ui" && setting == "language-admin" { + for key, lang := range app.storage.lang.Admin { + if lang["meta"].(map[string]interface{})["name"].(string) == value.(string) { + tempConfig.Section("ui").Key("language-admin").SetValue(key) break } } diff --git a/config.go b/config.go index 347bed4..247180e 100644 --- a/config.go +++ b/config.go @@ -84,8 +84,15 @@ func (app *appContext) loadConfig() error { substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("") - app.storage.lang.chosenFormLang = app.config.Section("ui").Key("language").MustString("en-us") - app.storage.lang.chosenFormLang = app.config.Section("ui").Key("language").MustString("en-us") + oldFormLang := app.config.Section("ui").Key("language").MustString("") + if oldFormLang != "" { + app.storage.lang.chosenFormLang = oldFormLang + } + newFormLang := app.config.Section("ui").Key("language-form").MustString("") + if newFormLang != "" { + app.storage.lang.chosenFormLang = newFormLang + } + app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us") return nil } diff --git a/config/config-base.json b/config/config-base.json index d32796c..382ac8f 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -84,7 +84,7 @@ "description": "Settings related to the UI and program functionality." }, "settings": { - "language": { + "language-form": { "name": "Default Form Language", "required": false, "requires_restart": true, @@ -93,7 +93,18 @@ "en-us" ], "value": "en-US", - "description": "Default UI Language. Currently only implemented for account creation form. 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": { + "name": "Default Admin Language", + "required": false, + "requires_restart": true, + "type": "select", + "options": [ + "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." }, "theme": { "name": "Default Look", diff --git a/views.go b/views.go index f3aea03..d3939a8 100644 --- a/views.go +++ b/views.go @@ -15,9 +15,9 @@ func gcHTML(gc *gin.Context, code int, file string, templ gin.H) { func (app *appContext) AdminPage(gc *gin.Context) { lang := gc.Query("lang") if lang == "" { - lang = app.storage.lang.chosenFormLang + lang = app.storage.lang.chosenAdminLang } else if _, ok := app.storage.lang.Form[lang]; !ok { - lang = app.storage.lang.chosenFormLang + lang = app.storage.lang.chosenAdminLang } emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() From 5e8d7944bd1dcbc15f05472628f2272526917c20 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 12 Jan 2021 23:43:44 +0000 Subject: [PATCH 04/11] add language selector to admin --- api.go | 11 +++++++++-- html/admin.html | 10 ++++++++++ main.go | 2 +- ts/admin.ts | 5 ++--- ts/form.ts | 19 ++----------------- ts/modules/lang.ts | 17 ++++++++++++++++- 6 files changed, 40 insertions(+), 24 deletions(-) diff --git a/api.go b/api.go index ab7b7b7..1c9565a 100644 --- a/api.go +++ b/api.go @@ -1237,9 +1237,16 @@ func (app *appContext) Logout(gc *gin.Context) { // @Router /lang [get] // @tags Other func (app *appContext) GetLanguages(gc *gin.Context) { + page := gc.Param("page") resp := langDTO{} - for key, lang := range app.storage.lang.Form { - resp[key] = lang["meta"].(map[string]interface{})["name"].(string) + if page == "form" { + for key, lang := range app.storage.lang.Form { + resp[key] = lang["meta"].(map[string]interface{})["name"].(string) + } + } else if page == "admin" { + for key, lang := range app.storage.lang.Admin { + resp[key] = lang["meta"].(map[string]interface{})["name"].(string) + } } if len(resp) == 0 { respond(500, "Couldn't get languages", gc) diff --git a/html/admin.html b/html/admin.html index 2227b52..5f214da 100644 --- a/html/admin.html +++ b/html/admin.html @@ -167,6 +167,16 @@
+ + + + + + +
diff --git a/main.go b/main.go index 06ace3d..3428014 100644 --- a/main.go +++ b/main.go @@ -577,7 +577,7 @@ func start(asDaemon, firstCall bool) { router.GET("/accounts", app.AdminPage) router.GET("/settings", app.AdminPage) - router.GET("/lang", app.GetLanguages) + router.GET("/lang/:page", app.GetLanguages) router.GET("/token/login", app.getTokenLogin) router.GET("/token/refresh", app.getTokenRefresh) router.POST("/newUser", app.NewUser) diff --git a/ts/admin.ts b/ts/admin.ts index 5811af3..d47b6c1 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -1,5 +1,5 @@ import { toggleTheme, loadTheme } from "./modules/theme.js"; -import { lang, LangFile } from "./modules/lang.js"; +import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { Modal } from "./modules/modal.js"; import { Tabs } from "./modules/tabs.js"; import { inviteList, createInvite } from "./modules/invites.js"; @@ -11,9 +11,8 @@ import { _get, _post, notificationBox, whichAnimationEvent, toggleLoader } from loadTheme(); (document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme; -var langLoaded = false; - window.lang = new lang(window.langFile as LangFile); +loadLangSelector("admin"); // _get(`/lang/admin/${window.language}.json`, null, (req: XMLHttpRequest) => { // if (req.readyState == 4 && req.status == 200) { // langLoaded = true; diff --git a/ts/form.ts b/ts/form.ts index d5301bb..b2b2581 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -1,5 +1,6 @@ import { Modal } from "./modules/modal.js"; import { _get, _post, toggleLoader } from "./modules/common.js"; +import { loadLangSelector } from "./modules/lang.js"; interface formWindow extends Window { validationStrings: pwValStrings; @@ -22,23 +23,7 @@ interface pwValStrings { [ type: string ]: pwValString; } -_get("/lang", null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status != 200) { - document.getElementById("lang-dropdown").remove(); - return; - } - const list = document.getElementById("lang-list") as HTMLDivElement; - let innerHTML = ''; - for (let code in req.response) { - innerHTML += `${req.response[code]}`; - } - list.innerHTML = innerHTML; - } -}); - - - +loadLangSelector("form"); window.modal = new Modal(document.getElementById("modal-success")); declare var window: formWindow; diff --git a/ts/modules/lang.ts b/ts/modules/lang.ts index 288a5c6..5765921 100644 --- a/ts/modules/lang.ts +++ b/ts/modules/lang.ts @@ -1,3 +1,5 @@ +import { _get } from "../modules/common.js"; + interface Meta { name: string; } @@ -45,7 +47,20 @@ export class lang implements Lang { } } - +export const loadLangSelector = (page: string) => _get("/lang/" + page, null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200) { + document.getElementById("lang-dropdown").remove(); + return; + } + const list = document.getElementById("lang-list") as HTMLDivElement; + let innerHTML = ''; + for (let code in req.response) { + innerHTML += `${req.response[code]}`; + } + list.innerHTML = innerHTML; + } +}); From 1707d011a2c9065d7247dde2b1fbbd8da2fbdea4 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 14 Jan 2021 14:18:12 +0000 Subject: [PATCH 05/11] attempt to use http2 server push --- views.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/views.go b/views.go index d3939a8..01af549 100644 --- a/views.go +++ b/views.go @@ -22,6 +22,14 @@ func (app *appContext) AdminPage(gc *gin.Context) { emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) + if pusher := gc.Writer.Pusher(); pusher != nil { + toPush := []string{"/js/admin.js", "/js/theme.js", "/js/lang.js", "/js/modal.js", "/js/tabs.js", "/js/invites.js", "/js/accounts.js", "/js/settings.js", "/js/profiles.js", "/js/common.js"} + for _, f := range toPush { + if err := pusher.Push(f, nil); err != nil { + app.debug.Printf("Failed HTTP2 ServerPush of \"%s\": %+v", f, err) + } + } + } gcHTML(gc, http.StatusOK, "admin.html", gin.H{ "urlBase": app.URLBase, "cssClass": app.cssClass, From 0710e0547972d3ceb90733aa7af84a1829eaa087 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 14 Jan 2021 17:51:12 +0000 Subject: [PATCH 06/11] Add email translation, add part of french translations Admin translation from @Killianbe, Email translation from @Cornichon420. French is currently not functional, a few things are missing which i'm waiting on. --- api.go | 4 +-- config.go | 7 ++-- config/config-base.json | 12 +++++++ email.go | 66 ++++++++++++++++++++++------------ lang/admin/fr-fr.json | 78 +++++++++++++++++++++++++++++++++++++++++ lang/email/en-us.json | 41 ++++++++++++++++++++++ lang/email/fr-fr.json | 32 +++++++++++++++++ mail/created.mjml | 17 +++++---- mail/created.txt | 10 +++--- mail/deleted.mjml | 4 +-- mail/deleted.txt | 4 +-- mail/email.mjml | 12 +++---- mail/email.txt | 12 +++---- mail/expired.mjml | 6 ++-- mail/expired.txt | 6 ++-- mail/invite-email.mjml | 10 +++--- mail/invite-email.txt | 8 ++--- main.go | 2 +- storage.go | 27 +++++++++++++- 19 files changed, 283 insertions(+), 75 deletions(-) create mode 100644 lang/admin/fr-fr.json create mode 100644 lang/email/en-us.json create mode 100644 lang/email/fr-fr.json 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 }}

- - - + + + - - - + + + - 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 } From 5401593279d1118c9b6d02bc2e1193edfae6f8e0 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 14 Jan 2021 20:24:28 +0000 Subject: [PATCH 07/11] Fix email language selection, add finished french emails --- api.go | 27 +++++++++++++++++++++++---- lang/email/fr-fr.json | 10 ++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/api.go b/api.go index f65a0c9..d1c382b 100644 --- a/api.go +++ b/api.go @@ -1087,7 +1087,7 @@ func (app *appContext) GetConfig(gc *gin.Context) { // Load language options loadLangs := func(langs *map[string]map[string]interface{}, settingsKey string) (string, []string) { langOptions := make([]string, len(*langs)) - chosenLang := app.config.Section("ui").Key("language-" + settingsKey).MustString("en-us") + 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 { @@ -1096,14 +1096,25 @@ func (app *appContext) GetConfig(gc *gin.Context) { } return chosenLangName, langOptions } - formChosen, formOptions := loadLangs(&app.storage.lang.Form, "form") + formChosen, formOptions := loadLangs(&app.storage.lang.Form, "-form") fl := resp.Sections["ui"].Settings["language-form"] fl.Options = formOptions fl.Value = formChosen - adminChosen, adminOptions := loadLangs(&app.storage.lang.Admin, "admin") + adminChosen, adminOptions := loadLangs(&app.storage.lang.Admin, "-admin") al := resp.Sections["ui"].Settings["language-admin"] al.Options = adminOptions al.Value = adminChosen + emailOptions := make([]string, len(app.storage.lang.Email)) + 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.Options = emailOptions + el.Value = emailChosen for sectName, section := range resp.Sections { for settingName, setting := range section.Settings { val := app.config.Section(sectName).Key(settingName) @@ -1121,10 +1132,11 @@ func (app *appContext) GetConfig(gc *gin.Context) { } resp.Sections["ui"].Settings["language-form"] = fl resp.Sections["ui"].Settings["language-admin"] = al + resp.Sections["email"].Settings["language"] = el t := resp.Sections["jellyfin"].Settings["type"] opts := make([]string, len(serverTypes)) - i := 0 + i = 0 for _, v := range serverTypes { opts[i] = v i++ @@ -1169,6 +1181,13 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { break } } + } else if section == "email" && setting == "language" { + for key := range app.storage.lang.Email { + if app.storage.lang.Email.get(key, "meta", "name") == value.(string) { + tempConfig.Section("email").Key("language").SetValue(key) + break + } + } } else if section == "jellyfin" && setting == "type" { for k, v := range serverTypes { if v == value.(string) { diff --git a/lang/email/fr-fr.json b/lang/email/fr-fr.json index 40e7adf..54850bf 100644 --- a/lang/email/fr-fr.json +++ b/lang/email/fr-fr.json @@ -4,13 +4,21 @@ "author": "https://github.com/Cornichon420" }, "userCreated": { + "title": "Notification : Utilisateur créé", "aUserWasCreated": "Un utilisateur a été créé avec ce code {n}", "name": "Nom", "emailAddress": "Adresse", "time": "Date", "notificationNotice": "" }, + "inviteExpiry": { + "title": "Notification : Invitation expirée", + "inviteExpired": "Invitation expirée.", + "expiredAt": "Le code {n} a expiré à {n}.", + "notificationNotice": "" + }, "passwordReset": { + "title": "Réinitialisation de mot de passe demandée - Jellyfin", "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.", @@ -19,10 +27,12 @@ "pin": "PIN" }, "userDeleted": { + "title": "Ton compte a été désactivé - Jellyfin", "yourAccountWasDeleted": "Ton compte Jellyfin a été supprimé.", "reason": "Motif" }, "inviteEmail": { + "title": "Invitation - Jellyfin", "hello": "Salut", "youHaveBeenInvited": "Tu a été invité à rejoindre Jellyfin.", "toJoin": "Pour continuer, suis le lien en dessous.", From 3e53b742f48f575b8cdc693582f01a06ef6a572b Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 14 Jan 2021 21:39:06 +0000 Subject: [PATCH 08/11] fix spelling in french email --- html/admin.html | 1 + lang/email/fr-fr.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/html/admin.html b/html/admin.html index 5f214da..11d56ef 100644 --- a/html/admin.html +++ b/html/admin.html @@ -264,6 +264,7 @@
+ diff --git a/lang/email/fr-fr.json b/lang/email/fr-fr.json index 54850bf..83889c2 100644 --- a/lang/email/fr-fr.json +++ b/lang/email/fr-fr.json @@ -18,7 +18,7 @@ "notificationNotice": "" }, "passwordReset": { - "title": "Réinitialisation de mot de passe demandée - Jellyfin", + "title": "Réinitialisation de mot du passe demandée - Jellyfin", "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.", @@ -34,9 +34,9 @@ "inviteEmail": { "title": "Invitation - Jellyfin", "hello": "Salut", - "youHaveBeenInvited": "Tu a été invité à rejoindre Jellyfin.", + "youHaveBeenInvited": "Tu as é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 !", + "inviteExpiry": "L'invitation expirera le {n}, à {n}, soit dans {n}, alors fais vite !", "linkButton": "Lien" } } From 3c1599b6b7de0a5ae544605863435d0758e3f4a7 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 15 Jan 2021 13:30:29 +0000 Subject: [PATCH 09/11] add finished french for admin --- html/admin.html | 3 +-- lang/admin/fr-fr.json | 56 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/html/admin.html b/html/admin.html index 11d56ef..f5617a9 100644 --- a/html/admin.html +++ b/html/admin.html @@ -99,7 +99,7 @@ {{ .strings.settingsRestartRequired }} ×

{{ .strings.settingsRestartRequiredDescription }}

- {{ .strings.settingsApplyRestartLater }} + {{ .strings.settingsApplyRestartLater }} {{ .strings.settingsApplyRestartNow }}
@@ -264,7 +264,6 @@
- diff --git a/lang/admin/fr-fr.json b/lang/admin/fr-fr.json index 72974b7..1145332 100644 --- a/lang/admin/fr-fr.json +++ b/lang/admin/fr-fr.json @@ -22,17 +22,21 @@ "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", - "aboutProgram": "A propros", + "aboutProgram": "A propos", "version": "Version", "commitNoun": "Commettre", "newUser": "Nouvel utilisateur", "profile": "Profil", + "success": "Succès", + "error": "Erreur", + "unknown": "Inconnu", "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", @@ -55,7 +59,47 @@ "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" + "addProfileStoreHomescreenLayout": "Enregistrer la disposition de l'écran d'accueil", + + "inviteNoUsersCreated": "Aucun pour l'instant!", + "inviteUsersCreated": "Utilisateurs créer", + "inviteNoProfile": "Aucun profil", + "copy": "Copier", + "inviteDateCreated": "Créer", + "inviteRemainingUses": "Utilisations restantes", + "inviteNoInvites": "Aucune", + "inviteExpiresInTime": "Expires dans {n}", + + "notifyEvent": "Notifier sur:", + "notifyInviteExpiry": "À l'expiration", + "notifyUserCreation": "à la création de l'utilisateur" + }, + "notifications": { + "changedEmailAddress": "Adresse e-mail modifiée de {n}.", + "userCreated": "L'utilisateur {n} a été créé.", + "createProfile": "Profil créé {n}.", + "saveSettings": "Les paramètres ont été enregistrés", + "setOmbiDefaults": "Valeurs par défaut de Ombi.", + "errorConnection": "Impossible de se connecter à jfa-go.", + "error401Unauthorized": "Non autorisé. Essayez d'actualiser la page.", + "errorSettingsAppliedNoHomescreenLayout": "Les paramètres ont été appliqués, mais l'application de la disposition de l'écran d'accueil a peut-être échoué.", + "errorHomescreenAppliedNoSettings": "La disposition de l'écran d'accueil a été appliquée, mais l'application des paramètres a peut-être échoué.", + "errorSettingsFailed": "L'application a échoué.", + "errorLoginBlank": "Le nom d'utilisateur et / ou le mot de passe sont vides", + "errorUnknown": "Erreur inconnue.", + "errorBlankFields": "Les champs sont vides", + "errorDeleteProfile": "Échec de la suppression du profil {n}", + "errorLoadProfiles": "Échec du chargement des profils.", + "errorCreateProfile": "Échec de la création du profil {n}", + "errorSetDefaultProfile": "Échec de la définition du profil par défaut", + "errorLoadUsers": "Échec du chargement des utilisateurs.", + "errorSaveSettings": "Impossible d'enregistrer les paramètres.", + "errorLoadSettings": "Échec du chargement des paramètres.", + "errorSetOmbiDefaults": "Impossible de stocker les valeurs par défaut d'Ombi.", + "errorLoadOmbiUsers": "Échec du chargement des utilisateurs Ombi.", + "errorChangedEmailAddress": "Impossible de modifier l'adresse e-mail de {n}.", + "errorFailureCheckLogs": "Échec (vérifier la console / les journaux)", + "errorPartialFailureCheckLogs": "Panne partielle (vérifier la console / les journaux)" }, "quantityStrings": { "modifySettingsFor": { @@ -73,6 +117,14 @@ "deleteUser": { "singular": "Supprimer l'utilisateur", "plural": "Supprimer les utilisateurs" + }, + "deletedUser": { + "singular": "Supprimer {n} utilisateur.", + "plural": "Supprimer {n} utilisateurs." + }, + "appliedSettings": { + "singular": "Appliquer le paramètre {n} utilisteur.", + "plural": "Appliquer les paramètres {n} utilisteurs." } } } From b1becb9ef5f23686d8763b966f1b27ea82ff4bb3 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 15 Jan 2021 13:37:09 +0000 Subject: [PATCH 10/11] fix display of username box on add account modal --- html/admin.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/admin.html b/html/admin.html index f5617a9..2a94c48 100644 --- a/html/admin.html +++ b/html/admin.html @@ -8,7 +8,7 @@ window.notificationsEnabled = {{ .notifications }}; window.emailEnabled = {{ .email_enabled }}; window.ombiEnabled = {{ .ombiEnabled }}; - window.usernamesEnabled = {{ .username }}; + window.usernameEnabled = {{ .username }}; window.langFile = JSON.parse({{ .language }}); {{ template "header.html" . }} From 0f92ce21665e546b14c0c048dc703e1f04ba7231 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 15 Jan 2021 13:40:42 +0000 Subject: [PATCH 11/11] update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11805f2..ca09bc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,5 @@ #### Translation -Currently only the account creation form can be translated. Strings are defined in `lang/form/.json` (country code as in `en-us`, `fr-fr`, e.g). You can see the existing ones [here](https://github.com/hrfee/jfa-go/tree/main/lang/form). +Currently the admin page, account creation form and emails can be translated. Strings are defined in `lang//.json` (country code as in `en-us`, `fr-fr`, e.g). You can see the existing ones [here](https://github.com/hrfee/jfa-go/tree/main/lang). Make sure to define `name` in the `meta` section, and you can optionally add an `author` value there as well. If you can, make a pull request with your new file. If not, email me or create an issue. #### Code
NameDate${window.lang.strings("name")}${window.lang.strings("date")}
Delete${window.lang.strings("delete")}
None
${window.lang.strings("inviteNoInvites")}
NameAddressTime{{ .name }}{{ .address }}{{ .time }}
{{ .username }}{{ .address }}{{ .time }}{{ .nameVal }}{{ .addressVal }}{{ .timeVal }}
{{ .strings.username }}{{ .strings.language }} {{ .strings.emailAddress }} {{ .strings.lastActiveTime }}
{{ .strings.username }}{{ .strings.language }} {{ .strings.emailAddress }} {{ .strings.lastActiveTime }}