-
{{ .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()))
+ 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 {
- 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
+ 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 {
- Name |
- Date |
+ ${window.lang.strings("name")} |
+ ${window.lang.strings("date")} |
@@ -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")}