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 { - - + + @@ -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], }) }
NameDate${window.lang.strings("name")}${window.lang.strings("date")}
Delete${window.lang.strings("delete")}
None
${window.lang.strings("inviteNoInvites")}