diff --git a/package-lock.json b/package-lock.json
index b93102a..a773833 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3493,6 +3493,37 @@
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.2.3.tgz",
"integrity": "sha512-pXnVMfJKSIWU2Ml4JHP7pZEPIrgBO1Fd3WGx+fPBsS+KRGhE4vxooD8XBGWbQOIVSZsVK7pUDBBkCicNu80yzQ=="
},
+ "axios": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
+ "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
+ "requires": {
+ "follow-redirects": "1.5.10"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "follow-redirects": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+ "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+ "requires": {
+ "debug": "=3.1.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
+ }
+ },
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -4904,6 +4935,21 @@
"sha.js": "^2.4.8"
}
},
+ "cross-fetch": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
+ "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
+ "requires": {
+ "node-fetch": "2.6.1"
+ },
+ "dependencies": {
+ "node-fetch": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+ "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
+ }
+ }
+ },
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -7799,6 +7845,14 @@
"terser": "^4.6.3"
}
},
+ "html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "requires": {
+ "void-elements": "3.1.0"
+ }
+ },
"html-webpack-plugin": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz",
@@ -8058,6 +8112,30 @@
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
+ "i18next": {
+ "version": "19.9.2",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz",
+ "integrity": "sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg==",
+ "requires": {
+ "@babel/runtime": "^7.12.0"
+ }
+ },
+ "i18next-browser-languagedetector": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.3.1.tgz",
+ "integrity": "sha512-KIToAzf8zwWvacgnRwJp63ase26o24AuNUlfNVJ5YZAFmdGhsJpmFClxXPuk9rv1FMI4lnc8zLSqgZPEZMrW4g==",
+ "requires": {
+ "@babel/runtime": "^7.5.5"
+ }
+ },
+ "i18next-http-backend": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.2.6.tgz",
+ "integrity": "sha512-NeNNRofj+rR6Cw+/Elf8bCVaCiqWg2Y6F+CrmDvHiPzAW2Dtxxlk8O0na2et/rr1n3ST6rJr4nMXH/QOFuhaeA==",
+ "requires": {
+ "cross-fetch": "3.1.4"
+ }
+ },
"iconv-lite": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
@@ -10915,6 +10993,11 @@
"minimist": "^1.2.5"
}
},
+ "moment": {
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
+ "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
+ },
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -13321,6 +13404,14 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
},
+ "react-flags": {
+ "version": "0.1.18",
+ "resolved": "https://registry.npmjs.org/react-flags/-/react-flags-0.1.18.tgz",
+ "integrity": "sha512-fa8D6DIZS6DWRqLcmKGIHVT13r4viHAfIRth9cFO7cDyxEPfTBbZei6p0Xeao6of4C/K4XU/j35aMjPC15ePIg==",
+ "requires": {
+ "prop-types": "^15.5.10"
+ }
+ },
"react-google-maps": {
"version": "9.4.5",
"resolved": "https://registry.npmjs.org/react-google-maps/-/react-google-maps-9.4.5.tgz",
@@ -13339,6 +13430,25 @@
"warning": "^3.0.0"
}
},
+ "react-i18next": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.11.0.tgz",
+ "integrity": "sha512-p1jHmoyJgDFQmyubUEjrx6kCsr1izW/C8i9pOiJy+9lJqLYwNA8sElVplm0VAnop3kH68edT0/g3wB3UvAcRCQ==",
+ "requires": {
+ "@babel/runtime": "^7.14.5",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.14.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
+ "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
+ }
+ },
"react-is": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz",
@@ -16182,6 +16292,11 @@
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
},
+ "void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk="
+ },
"w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
diff --git a/package.json b/package.json
index 31edf44..0388e64 100644
--- a/package.json
+++ b/package.json
@@ -26,8 +26,15 @@
"react-scripts": "4.0.1",
"react-syntax-highlighter": "^15.4.3",
"react-toastify": "^7.0.3",
+ "react-i18next": "^11.4.0",
+ "react-flags": "^0.1.18",
"recharts": "^2.0.4",
- "tinycolor2": "^1.4.2"
+ "tinycolor2": "^1.4.2",
+ "axios": "^0.19.2",
+ "i18next": "^19.4.4",
+ "i18next-browser-languagedetector": "^4.1.1",
+ "i18next-http-backend": "^1.0.10",
+ "moment": "^2.25.3"
},
"scripts": {
"start": "react-scripts start",
diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json
new file mode 100644
index 0000000..cbebf2d
--- /dev/null
+++ b/public/locales/en/translations.json
@@ -0,0 +1,41 @@
+{
+ "DATE": "{{date,intlDate}}",
+ "LONG_DATE": "{{date,intlLongDate}}",
+ "DATE_FORMAT": "{{date, format}}",
+ "TIME_FROM_X": "{{date,intlTimeFromX}}",
+ "H_FROM_X": "{{date,intlHoursFromX}}",
+ "H_FROM_M": "{{number,intlHoursFromMinutes}}",
+ "NUMBER": "{{number,intlNumber}}",
+ "DECIMAL": "{{number,intlDecimal}}",
+ "DECIMAL2": "{{number,intlDecimal2}}",
+ "Language": {
+ "English": "English",
+ "Romanian": "Romanian"
+ },
+ "Menu": {
+ "Dashboard": "Dashboard",
+ "Resources": "Resources"
+ },
+ "Login": {
+ "Username": "Username",
+ "Password": "Password",
+ "Label": "Login",
+ "ChangeUser": "Change user",
+ "UserChanged": "User changed",
+ "Logout": "Logout",
+ "IncorrectCredentials": "Incorrect credentials.",
+ "Hello": "Hi, {{username}}",
+ "AuthenticationDate": "Authentication date"
+ },
+ "Machine": {
+ "FullName": "Full machine name",
+ "Name": "Machine name",
+ "IP": "IP",
+ "MAC": "MAC address",
+ "PoweredOn": "Powered on",
+ "Actions": {
+ "Wake": "Wake",
+ "Ping": "Ping"
+ }
+ }
+}
diff --git a/public/locales/ro/translations.json b/public/locales/ro/translations.json
new file mode 100644
index 0000000..17dae68
--- /dev/null
+++ b/public/locales/ro/translations.json
@@ -0,0 +1,32 @@
+{
+ "Language": {
+ "English": "Engleză",
+ "Romanian": "Română"
+ },
+ "Menu": {
+ "Dashboard": "Dashboard",
+ "Resources": "Resurse"
+ },
+ "Login": {
+ "Username": "Utilizator",
+ "Password": "Parolă",
+ "Label": "Autentificare",
+ "ChangeUser": "Schimbă utilizatorul",
+ "UserChanged": "Utilizator schimbat",
+ "Logout": "Deconectare",
+ "IncorrectCredentials": "Credențiale incorecte.",
+ "Hello": "Salut, {{username}}",
+ "AuthenticationDate": "Momentul autentificării"
+ },
+ "Machine": {
+ "FullName": "Nume intreg masina",
+ "Name": "Nume masina",
+ "IP": "IP",
+ "MAC": "Adresa MAC",
+ "PoweredOn": "Pornit",
+ "Actions": {
+ "Wake": "Pornește",
+ "Ping": "Ping"
+ }
+ }
+}
diff --git a/src/components/App.js b/src/components/App.js
index f7c3ba7..23f3683 100644
--- a/src/components/App.js
+++ b/src/components/App.js
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { Suspense } from "react";
import { HashRouter, Route, Switch, Redirect } from "react-router-dom";
// components
@@ -16,19 +16,25 @@ export default function App() {
var { isAuthenticated } = useUserState();
return (
-
-
- } />
- }
- />
-
-
-
-
-
+ Loading...}>
+
+
+ }
+ />
+ }
+ />
+
+
+
+
+
+
);
function PrivateRoute({ component, ...rest }) {
diff --git a/src/components/Sidebar/components/SidebarLink/SidebarLink.js b/src/components/Sidebar/components/SidebarLink/SidebarLink.js
index 802ad00..58ae34f 100644
--- a/src/components/Sidebar/components/SidebarLink/SidebarLink.js
+++ b/src/components/Sidebar/components/SidebarLink/SidebarLink.js
@@ -11,6 +11,7 @@ import {
import { Inbox as InboxIcon } from "@material-ui/icons";
import { Link } from "react-router-dom";
import classnames from "classnames";
+import { useTranslation } from "react-i18next";
// styles
import useStyles from "./styles";
@@ -29,7 +30,8 @@ export default function SidebarLink({
type
}) {
var classes = useStyles();
-
+ const { t } = useTranslation();
+ const _label = t(label);
// local
var [isOpen, setIsOpen] = useState(false);
var isLinkActive =
@@ -44,7 +46,7 @@ export default function SidebarLink({
[classes.linkTextHidden]: !isSidebarOpened
})}
>
- {label}
+ {_label}
);
@@ -76,7 +78,7 @@ export default function SidebarLink({
[classes.linkTextHidden]: !isSidebarOpened
})
}}
- primary={label}
+ primary={_label}
/>
@@ -112,7 +114,7 @@ export default function SidebarLink({
[classes.linkTextHidden]: !isSidebarOpened
})
}}
- primary={label}
+ primary={_label}
/>
);
@@ -141,7 +143,7 @@ export default function SidebarLink({
[classes.linkTextHidden]: !isSidebarOpened
})
}}
- primary={label}
+ primary={_label}
/>
{children && (
diff --git a/src/index.js b/src/index.js
index 9735518..20c057e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,12 +2,12 @@ import React from "react";
import ReactDOM from "react-dom";
import { ThemeProvider } from "@material-ui/styles";
import { CssBaseline } from "@material-ui/core";
-
import Themes from "./themes";
import App from "./components/App";
import * as serviceWorker from "./serviceWorker";
import { LayoutProvider } from "./context/LayoutContext";
import { UserProvider } from "./context/UserContext";
+import "./utils/i18n";
ReactDOM.render(
@@ -18,7 +18,7 @@ ReactDOM.render(
,
- document.getElementById("root"),
+ document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
diff --git a/src/utils/axios.js b/src/utils/axios.js
new file mode 100644
index 0000000..0f17e8d
--- /dev/null
+++ b/src/utils/axios.js
@@ -0,0 +1,75 @@
+import axios from "axios";
+import i18next from "i18next";
+import { getItem } from "./localStorage";
+import { storageKeys } from "./identity";
+
+function getHeaders() {
+ const token = getItem(storageKeys.TOKEN);
+ const language = i18next.language;
+
+ return {
+ "Content-Type": "application/json",
+ Authorization: `Basic ${token.raw}`,
+ "Accept-Language": `${language}`
+ };
+}
+
+async function internalRequest(url, options) {
+ try {
+ const res = await axios.request(url, options);
+ return res.data;
+ } catch (error) {
+ if (error.response && error.response.data) {
+ throw (
+ {
+ ...error.response.data,
+ message: error.response.data.detail || error.response.data.title
+ } || error
+ );
+ }
+ // The request was made but no response was received
+ // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
+ // http.ClientRequest in node.js
+ throw error;
+ }
+}
+
+export const request = (url, options) => internalRequest(url, options);
+
+export function post(url, data) {
+ const options = {
+ method: "post",
+ data: JSON.stringify(data),
+ headers: getHeaders()
+ };
+
+ return internalRequest(url, options);
+}
+
+export function put(url, data) {
+ const options = {
+ method: "put",
+ data: JSON.stringify(data),
+ headers: getHeaders()
+ };
+
+ return internalRequest(url, options);
+}
+
+export function del(url, data) {
+ const options = {
+ method: "delete",
+ data: JSON.stringify(data),
+ headers: getHeaders()
+ };
+
+ return internalRequest(url, options);
+}
+
+export function get(url) {
+ const options = {
+ method: "GET",
+ headers: getHeaders()
+ };
+ return internalRequest(url, options);
+}
diff --git a/src/utils/dataType.js b/src/utils/dataType.js
new file mode 100644
index 0000000..d7ac7e8
--- /dev/null
+++ b/src/utils/dataType.js
@@ -0,0 +1,21 @@
+const isArray = (parameter) => {
+ const _isArray = parameter.constructor === Array;
+ return _isArray;
+};
+
+const isObject = (parameter) => {
+ const _isObject = typeof parameter === "object" && parameter !== null;
+
+ return _isObject;
+};
+
+function isJson(str) {
+ try {
+ const data = JSON.parse(str);
+ return { data, success: true };
+ } catch (e) {
+ return { data: null, success: false };
+ }
+}
+
+export { isArray, isObject, isJson };
diff --git a/src/utils/i18n.js b/src/utils/i18n.js
new file mode 100644
index 0000000..24f5c29
--- /dev/null
+++ b/src/utils/i18n.js
@@ -0,0 +1,101 @@
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+import Backend from "i18next-http-backend";
+import LanguageDetector from "i18next-browser-languagedetector";
+import moment from "moment";
+import "moment/locale/ro.js";
+import "moment/locale/de.js";
+
+i18n
+ .use(Backend)
+ .use(LanguageDetector)
+ .use(initReactI18next) // passes i18n down to react-i18next
+ .init(
+ {
+ fallbackLng: "en",
+ debug: true,
+ ns: ["translations"],
+ defaultNS: "translations",
+ //whitelist: ["en", "ro"],
+ interpolation: {
+ escapeValue: false, // not needed for react as it escapes by default
+ format: function (value, format, lng) {
+ if (format === "uppercase") return value.toUpperCase();
+ if (format === "intlDate") {
+ if (value && moment(value).isValid()) {
+ return moment(value).format("L");
+ }
+ return "";
+ }
+
+ if (format === "intlLongDate") {
+ if (value && moment(value).isValid()) {
+ return moment(value).format("LLLL");
+ }
+ return "";
+ }
+
+ if (format === "intlTimeFromX") {
+ if (value && moment(value.start).isValid()) {
+ let startDate = moment(value.start);
+ let endDate = moment(value.end);
+ return moment(endDate).from(startDate, true);
+ }
+ return "";
+ }
+
+ if (format === "intlHoursFromX") {
+ if (value && moment(value.start).isValid()) {
+ let startDate = moment(value.start);
+ let endDate = moment(value.end);
+ let span = moment.duration(endDate - startDate);
+ return `${parseInt(span.asHours(), 10)}h ${parseInt(
+ span.asMinutes() % 60,
+ 10
+ )}m`;
+ }
+ return "";
+ }
+
+ if (format === "intlNumber")
+ return new Intl.NumberFormat(lng).format(value);
+ if (format === "intlDecimal")
+ return new Intl.NumberFormat(lng, {
+ minimumFractionDigits: 2
+ }).format(value);
+ if (format === "intlDecimal2")
+ return new Intl.NumberFormat(lng, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ }).format(value);
+
+ //dateformat
+ if (value && value.format) {
+ if (value.value && moment(value).isValid()) {
+ return moment(value.value).format(value.format);
+ }
+ return "";
+ }
+
+ return value;
+ }
+ },
+ backend: {
+ loadPath: `${process.env.PUBLIC_URL || ""}/locales/{{lng}}/{{ns}}.json`
+ }
+ },
+ () => {
+ const currentLang = i18n.language;
+ if (!currentLang || !currentLang.startsWith("ro")) {
+ i18n.changeLanguage("en");
+ } else {
+ i18n.changeLanguage("ro");
+ }
+ }
+ );
+
+i18n.on("languageChanged", function (lng) {
+ moment.locale(lng);
+});
+
+export default i18n;
diff --git a/src/utils/identity.js b/src/utils/identity.js
new file mode 100644
index 0000000..ba79528
--- /dev/null
+++ b/src/utils/identity.js
@@ -0,0 +1,35 @@
+import { request } from "./axios";
+import { setItem, getItem, removeItem } from "./localStorage";
+
+const storageKeys = {
+ TOKEN: "AUTHORIZATION_TOKEN",
+ USER: "USER_NAME"
+};
+
+const authenticate = async (userName, password) => {
+ const urlTemplate = process.env.REACT_APP_IDENTITY_AUTHENTICATION_URL;
+ const url = urlTemplate
+ .replace("{username}", userName)
+ .replace("{password}", password);
+ const options = {
+ method: "post"
+ };
+
+ const response = await request(url, options);
+ if (response.status === "SUCCESS") {
+ setItem(storageKeys.TOKEN, response.token);
+ setItem(storageKeys.USER, userName);
+ }
+
+ return response;
+};
+
+const invalidate = () => {
+ const token = getItem(storageKeys.TOKEN);
+ if (token) {
+ removeItem(storageKeys.TOKEN);
+ removeItem(storageKeys.USER);
+ }
+};
+
+export { storageKeys, authenticate, invalidate };
diff --git a/src/utils/localStorage.js b/src/utils/localStorage.js
new file mode 100644
index 0000000..22a7097
--- /dev/null
+++ b/src/utils/localStorage.js
@@ -0,0 +1,36 @@
+import { isArray, isObject, isJson } from "./dataType";
+
+const setItem = (key, value) => {
+ let valueToStore = value;
+ if (isArray(value) || isObject(value)) {
+ valueToStore = JSON.stringify(value);
+ }
+
+ window.localStorage.setItem(key, valueToStore);
+};
+
+const getItem = (key) => {
+ var value = window.localStorage.getItem(key);
+ var { data, success } = isJson(value);
+
+ if (success) {
+ return data;
+ } else {
+ return value;
+ }
+};
+
+const removeItem = (key) => {
+ window.localStorage.removeItem(key);
+};
+
+const clear = () => {
+ window.localStorage.clear();
+};
+
+const key = (index) => {
+ var keyName = window.localStorage.key(index);
+ return keyName;
+};
+
+export { setItem, getItem, removeItem, clear, key };