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 };