diff --git a/i18n-unused.config.js b/i18n-unused.config.js new file mode 100644 index 0000000..a35e409 --- /dev/null +++ b/i18n-unused.config.js @@ -0,0 +1,8 @@ +module.exports = { + srcPaths: ["src/**/*.{ts,tsx}", "!src/components/common/ckeditor/**"], + localesPath: "public/locales", + defaultNamespace: "common", + + // Match ANY t("...") call, anywhere in TS/TSX/JSX + translationKeyMatcher: "t\\([\"']([^\"']+)[\"']\\)", +}; diff --git a/i18next-parser.config.js b/i18next-parser.config.js new file mode 100644 index 0000000..29fbc40 --- /dev/null +++ b/i18next-parser.config.js @@ -0,0 +1,15 @@ +module.exports = { + locales: ["en"], + namespaceSeparator: false, + keySeparator: false, + defaultNamespace: "common", + output: "public/locales/$LOCALE/$NAMESPACE.json", + createOldCatalogs: false, + keepRemoved: false, + lexers: { + ts: ["JsxLexer"], + tsx: ["JsxLexer"], + js: ["JsxLexer"], + jsx: ["JsxLexer"], + }, +}; diff --git a/package.json b/package.json index 8f0f1e6..4f12978 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "cross-env": "^7.0.3", "date-fns": "^2.30.0", "html-react-parser": "^3.0.16", + "i18next": "^22.5.1", + "i18next-http-backend": "^3.0.2", "joi": "^17.9.1", "js-cookie": "^3.0.5", "jwt-decode": "^3.1.2", @@ -40,13 +42,13 @@ "react-bootstrap": "^2.7.4", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", + "react-i18next": "^12.3.1", "react-router-dom": "^6.10.0", "react-scripts": "^5.0.1", "react-toastify": "^9.1.2", "react-toggle": "^4.1.3", "runtime-env-cra": "^0.2.4", "sass": "^1.62.0", - "typescript": "^4.7.4", "web-vitals": "^3.3.1" }, "scripts": { @@ -56,7 +58,11 @@ "start": "concurrently \"npm run start-react\" \"npm run watch-css\" ", "build": "react-app-rewired build", "test": "react-app-rewired test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "i18n:extract": "i18next \"src/**/*.{ts,tsx,js,jsx}\" \"!src/components/common/ckeditor/**\" --config i18next-parser.config.js", + "i18n:unused": "i18n-unused display-unused", + "i18n:missed": "i18n-unused display-missed", + "i18n:check": "npm run i18n:extract && npm run i18n:unused" }, "eslintConfig": { "extends": [ @@ -79,6 +85,9 @@ "devDependencies": { "@types/node": "^20.2.3", "@types/react-toggle": "^4.0.3", - "react-app-rewired": "^2.2.1" + "i18n-unused": "^0.19.0", + "i18next-parser": "^9.3.0", + "react-app-rewired": "^2.2.1", + "typescript": "^4.9.5" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..3176a53 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,28 @@ +{ + "Admin": "Admin", + "AuditLog": "Audit Logs", + "AuditLogs": "Audit Logs", + "BlockedIPAddresses": "Blocked IP addresses", + "BlockedIPs": "Blocked IPs", + "ClientDomainManager": "Client Domain Manager", + "ClientDomains": "Client Domains", + "CustomFieldManager": "Custom Field Manager", + "CustomFields": "Custom Fields", + "e-print": "e-print", + "e-suite": "e-suite", + "ErrorLogs": "Error Logs", + "ExceptionLogs": "Exception Logs", + "Forms": "Forms", + "FormTemplateManager": "Form Template Manager", + "Glossaries": "Glossaries", + "GlossaryManager": "Glossary Manager", + "Home": "Home", + "Sequence": "Sequence", + "SequenceManager": "Sequence Manager", + "SiteManager": "Site Manager", + "SpecificationManager": "Specification Manager", + "SsoManager": "Sso Manager", + "Support": "Support", + "UserManager": "User Manager", + "Users": "Users" +} diff --git a/src/App.tsx b/src/App.tsx index aa85189..2ea2dcc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from "react"; import { Routes, Route, Navigate, useNavigate } from "react-router-dom"; import { Helmet, HelmetProvider, HtmlProps } from "react-helmet-async"; import { ToastContainer } from "react-toastify"; +import { useTranslation } from "react-i18next"; import config from "./config.json"; import authentication from "./modules/frame/services/authenticationService"; import ForgotPassword from "./modules/frame/components/ForgotPassword"; @@ -42,129 +43,459 @@ import BlockedIPs from "./modules/blockedIPs/blockedIPs"; import ErrorLogs from "./modules/errorLogs/errorLogs"; import SsoManager from "./modules/manager/ssoManager/ssoManager"; import SsoProviderDetails from "./modules/manager/ssoManager/SsoProviderDetails"; +import { Namespaces } from "./i18n"; function GetSecureRoutes() { - const profileRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? }/> - : }/>; + const { t } = useTranslation(); + const profileRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? ( + } /> + ) : ( + + + + } + /> + ); - return ( - <> - }/> - } /> - } /> - } /> + return ( + <> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - }/> - }/> - }/> + + + + } + /> + + + + } + /> + + + + } + /> - }/> - }/> - }/> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> - }/> - }/> - }/> + + + + } + /> + + + + } + /> + + + + } + /> - } /> - }/> - }/> - }/> - }/> - }/> + } /> + + + + } + /> + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - }/> - }/> - }/> + + + + } + /> + + + + } + /> + + + + } + /> - }/> - }/> - }/> + + + + } + /> + + + + } + /> + + + + } + /> - }/> - }/> - }/> + + + + } + /> + + + + } + /> + + + + } + /> - }/> - }/> - }/> - }/> - }/> - }/> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - }/> - }/> - }/> + + + + } + /> + + + + } + /> + + + + } + /> - }/> - }/> - }/> - - - {profileRoute} - }/> - }/> - - ); + + + + } + /> + + + + } + /> + + + + } + /> + + {profileRoute} + + + + } + /> + + + + } + /> + + ); } function App() { - let navigate = useNavigate(); + let navigate = useNavigate(); - useEffect(() => { - const timer = setInterval(async () => { - try { - if (authentication.hasToken()) { - await authentication.refreshToken(); + useEffect(() => { + const timer = setInterval(async () => { + try { + if (authentication.hasToken()) { + await authentication.refreshToken(); - if (authentication.tokenExpired()) { - navigate("/login"); - authentication.logout() - } - } - } - catch (e: any) { - console.log(e); - } - }, 10 * 1000); - return () => clearInterval(timer); + if (authentication.tokenExpired()) { + navigate("/login"); + authentication.logout(); + } + } + } catch (e: any) { + console.log(e); + } + }, 10 * 1000); + return () => clearInterval(timer); + }); + + const isSignedIn = authentication.getCurrentUser() != null; + + const secureRoutes = isSignedIn ? ( + GetSecureRoutes() + ) : ( + } /> + ); + + var htmlAttributes: HtmlProps = { + "data-bs-theme": theme.getPreferredTheme(), + }; + + window + .matchMedia("(prefers-color-scheme: dark)") + .addEventListener("change", () => { + window.location.reload(); }); - const isSignedIn = authentication.getCurrentUser() != null; + const loginRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? ( + } /> + ) : ( + + + + } + /> + ); - const secureRoutes = isSignedIn ? GetSecureRoutes() : } />; - - var htmlAttributes : HtmlProps = { - 'data-bs-theme' : theme.getPreferredTheme() - } - - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { - window.location.reload(); - }) - - const loginRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? }/> - : } />; - - return ( - - - {config.applicationName} - -
- - } /> - {loginRoute} - } /> - } /> - } /> - {secureRoutes} - } /> - - -
-
- ); + return ( + + + {config.applicationName} + +
+ + } /> + {loginRoute} + + + + } + /> + + + + } + /> + + + + } + /> + {secureRoutes} + } /> + + +
+
+ ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx index d6998fc..7ff0b3d 100644 --- a/src/components/common/AutoComplete.tsx +++ b/src/components/common/AutoComplete.tsx @@ -2,117 +2,125 @@ import * as React from "react"; import Option from "./option"; interface AutocompleteProps { - options?: Option[]; - selectedOptions?: Option[]; - placeholder?: string; - onSelect: (item: Option) => void; + options?: Option[]; + selectedOptions?: Option[]; + placeholder?: string; + onSelect: (item: Option) => void; } interface AutocompleteState { - filteredOptions: Option[]; + filteredOptions: Option[]; } -export default class Autocomplete extends React.PureComponent { - private inputRef; - constructor(props: AutocompleteProps) { - super(props); - this.state = { filteredOptions: [] } - this.inputRef = React.createRef(); +export default class Autocomplete extends React.PureComponent< + AutocompleteProps, + AutocompleteState +> { + private inputRef; + constructor(props: AutocompleteProps) { + super(props); + this.state = { filteredOptions: [] }; + this.inputRef = React.createRef(); + } + + private filterOptions(filterTerm: string) { + if (filterTerm !== "") { + let filtered = + this.props.options?.filter((x) => + x.name.toLowerCase().includes(filterTerm.toLowerCase()), + ) ?? []; + filtered = filtered.filter( + (x) => !this.props.selectedOptions?.some((y) => x._id === y._id), + ); + this.setState({ + filteredOptions: filtered, + }); + } else { + this.setState({ + filteredOptions: [], + }); } + } + private showOptions() { + let filtered = this.props.options?.filter((x) => x.name) ?? []; + filtered = filtered.filter( + (x) => !this.props.selectedOptions?.some((y) => x._id === y._id), + ); + this.setState({ + filteredOptions: filtered, + }); + } - private filterOptions(filterTerm: string) { - if (filterTerm !== "") { - let filtered = this.props.options?.filter(x => x.name.toLowerCase().includes(filterTerm.toLowerCase())) ?? []; - filtered = filtered.filter(x => !this.props.selectedOptions?.some(y => x._id === y._id)); - this.setState({ - filteredOptions: filtered - }); - } - else { - this.setState({ - filteredOptions: [] - }); - } - } + private hideAutocomplete = (event: any) => { + if (event.target.classList.contains("autocomplete-text-input")) return; - private showOptions() { - let filtered = this.props.options?.filter(x => x.name) ?? []; - filtered = filtered.filter(x => !this.props.selectedOptions?.some(y => x._id === y._id)); - this.setState({ - filteredOptions: filtered - }); - } + this.setState({ + filteredOptions: [], + }); + }; - private hideAutocomplete = (event: any) => { - if (event.target.classList.contains('autocomplete-text-input')) - return; + private handleBlur = (event: React.FocusEvent) => { + setTimeout(() => { + if ( + this.inputRef.current && + this.inputRef.current.contains(document.activeElement) + ) { + return; + } - this.setState({ - filteredOptions: [] - }); - } + this.setState({ filteredOptions: [] }); + }, 0); + }; - private handleBlur = (event: React.FocusEvent) => { - setTimeout(() => { - if ( - this.inputRef.current && - this.inputRef.current.contains(document.activeElement) - ) { - return; - } + componentDidMount() { + document.addEventListener("click", this.hideAutocomplete); + } - this.setState({ filteredOptions: [] }); - }, 0); - }; + componentWillUnmount() { + document.removeEventListener("click", this.hideAutocomplete); + } - componentDidMount() { - document.addEventListener('click', this.hideAutocomplete); - } + render() { + const { placeholder, onSelect } = this.props; + const { filteredOptions } = this.state; - componentWillUnmount() { - document.removeEventListener('click', this.hideAutocomplete); - } - - render() { - const { placeholder, onSelect } = this.props; - const { filteredOptions } = this.state; - - return ( -
- { this.filterOptions(e.target.value) }} - onFocus={(e) => { this.showOptions() }} - placeholder={placeholder} - /> - {filteredOptions.length > 0 && ( -
    - {filteredOptions.map((x, i) => -
  • - -
  • - )} -
- )} -
- ) - } + return ( +
+ { + this.filterOptions(e.target.value); + }} + onFocus={(e) => { + this.showOptions(); + }} + placeholder={placeholder} + /> + {filteredOptions.length > 0 && ( +
    + {filteredOptions.map((x, i) => ( +
  • + +
  • + ))} +
+ )} +
+ ); + } } diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index e44077d..f9c7698 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -1,93 +1,110 @@ -import react, { SyntheticEvent } from 'react'; -import { Link } from 'react-router-dom'; +import React, { SyntheticEvent } from "react"; +import { Link } from "react-router-dom"; -export enum ButtonType{ - none, - primary, - secondary, - success, - danger, - warning, - info, - light, - dark, - link +export enum ButtonType { + none, + primary, + secondary, + success, + danger, + warning, + info, + light, + dark, + link, } -export interface ButtonProps{ - testid?: string; - className?: string; - id?:string; - name?:string; - keyValue?: T; - children: React.ReactNode; - buttonType : ButtonType; - disabled?: boolean; - to?: string; - onClick?: ( keyValue : T | undefined ) => void; +export interface ButtonProps { + testid?: string; + className?: string; + id?: string; + name?: string; + keyValue?: T; + children: React.ReactNode; + buttonType: ButtonType; + disabled?: boolean; + to?: string; + onClick?: (keyValue: T | undefined) => void; } -class Button extends react.Component> { - Click = (e : SyntheticEvent) => { - const {keyValue, onClick} = this.props; - - if (onClick) - { - e.preventDefault(); - onClick(keyValue); - } +function Button({ + testid, + className, + id, + name, + keyValue, + children, + buttonType, + disabled, + to, + onClick, +}: ButtonProps) { + const handleClick = (e: SyntheticEvent) => { + if (onClick) { + e.preventDefault(); + onClick(keyValue); } - - render() { - const { id, className, children, buttonType, disabled, name, to, testid } = this.props; + }; - let classNames = ""; + let classNames = ""; - switch (buttonType) - { - case ButtonType.primary: - classNames = "btn btn-primary"; - break; - case ButtonType.secondary: - classNames = "btn btn-secondary"; - break; - case ButtonType.success: - classNames = "btn btn-success"; - break; - case ButtonType.danger: - classNames = "btn btn-danger"; - break; - case ButtonType.warning: - classNames = "btn btn-warning"; - break; - case ButtonType.info: - classNames = "btn btn-info"; - break; - case ButtonType.light: - classNames = "btn btn-light"; - break; - case ButtonType.dark: - classNames = "btn btn-dark"; - break; - case ButtonType.link: - classNames = "btn btn-link"; - break; - case ButtonType.none: - classNames = "btn btn-default" - break; - } + switch (buttonType) { + case ButtonType.primary: + classNames = "btn btn-primary"; + break; + case ButtonType.secondary: + classNames = "btn btn-secondary"; + break; + case ButtonType.success: + classNames = "btn btn-success"; + break; + case ButtonType.danger: + classNames = "btn btn-danger"; + break; + case ButtonType.warning: + classNames = "btn btn-warning"; + break; + case ButtonType.info: + classNames = "btn btn-info"; + break; + case ButtonType.light: + classNames = "btn btn-light"; + break; + case ButtonType.dark: + classNames = "btn btn-dark"; + break; + case ButtonType.link: + classNames = "btn btn-link"; + break; + case ButtonType.none: + classNames = "btn btn-default"; + break; + } - if (className !== undefined) - { - classNames += " " + className; - } - - if (to !== undefined){ - return {children} - } + if (className) { + classNames += " " + className; + } - return ; - } + if (to) { + return ( + + {children} + + ); + } + + return ( + + ); } -export default Button; \ No newline at end of file +export default Button; diff --git a/src/components/common/ConfirmButton.tsx b/src/components/common/ConfirmButton.tsx index d5c52ef..001ca90 100644 --- a/src/components/common/ConfirmButton.tsx +++ b/src/components/common/ConfirmButton.tsx @@ -1,57 +1,57 @@ -import react from 'react'; -import Button, { ButtonType } from './Button'; +import React, { useState, useCallback } from "react"; +import Button, { ButtonType } from "./Button"; +import { useTranslation } from "react-i18next"; +import { Namespaces } from "../../i18n"; -export interface ConfirmButtonProps{ - delayMS? : number; - buttonType : ButtonType; - keyValue: T; - children: React.ReactNode; - confirmMessage?: React.ReactNode; - onClick?: ( keyValue? : T ) => void; +export interface ConfirmButtonProps { + delayMS?: number; + buttonType: ButtonType; + keyValue: T; + children: React.ReactNode; + confirmMessage?: React.ReactNode; + onClick?: (keyValue?: T) => void; } -export interface ConfirmButtonState{ - firstClick : boolean +function ConfirmButton({ + delayMS = 5000, + buttonType, + keyValue, + children, + confirmMessage, + onClick, +}: ConfirmButtonProps) { + const [firstClick, setFirstClick] = useState(false); + const t = useTranslation(); + + const handleFirstClick = useCallback(() => { + setFirstClick(true); + + setTimeout(() => { + setFirstClick(false); + }, delayMS); + }, [delayMS]); + + const handleSecondClick = useCallback(() => { + if (onClick) { + onClick(keyValue); + } + }, [onClick, keyValue]); + + return ( + <> + {!firstClick && ( + + )} + + {firstClick && ( + + )} + + ); } -class ConfirmButton extends react.Component, ConfirmButtonState > { - state : ConfirmButtonState = { - firstClick : false - } - - FirstClick = () => { - const firstClick = true; - this.setState({firstClick}); - - let { delayMS } = this.props; - if (delayMS === undefined) - delayMS = 5000; - - setTimeout(() => { - console.log(`updating state`) - const firstClick = false; - this.setState({firstClick}); - }, delayMS); - } - - SecondClick = () => { - const {keyValue, onClick} = this.props; - - if (onClick) - onClick(keyValue); - } - - render() { - const { buttonType, children, confirmMessage } = this.props; - const { firstClick } = this.state; - - return ( - <> - {!firstClick && } - {firstClick && } - - ); - } -} - -export default ConfirmButton; \ No newline at end of file +export default ConfirmButton; diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..1f782c1 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,29 @@ +// /src/i18n.ts +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import HttpBackend from "i18next-http-backend"; +import { determineInitialLanguage } from "./modules/frame/services/lanugageService"; + +export const Namespaces = { + Common: "common", + Frame: "frame", +} as const; + +export type Namespace = (typeof Namespaces)[keyof typeof Namespaces]; + +i18n + .use(HttpBackend) // load translations from /public/locales + .use(initReactI18next) + .init({ + lng: determineInitialLanguage(), + fallbackLng: "en", + defaultNS: "common", + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: "/locales/{{lng}}/{{ns}}.json", + }, + }); + +export default i18n; diff --git a/src/index.tsx b/src/index.tsx index a1070c9..1e4d479 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,17 +1,19 @@ +import "./i18n"; import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; import { BrowserRouter } from "react-router-dom"; - -const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); +const root = ReactDOM.createRoot( + document.getElementById("root") as HTMLElement, +); root.render( - - - - - + + + + + , ); // If you want to start measuring performance in your app, pass a function diff --git a/src/modules/frame/components/LeftMenu.tsx b/src/modules/frame/components/LeftMenu.tsx index c7c70c9..478edc2 100644 --- a/src/modules/frame/components/LeftMenu.tsx +++ b/src/modules/frame/components/LeftMenu.tsx @@ -1,92 +1,120 @@ -import * as React from "react"; +import React, { useEffect, useState, useCallback } from "react"; import authentication from "../services/authenticationService"; -import '../../../Sass/_leftMenu.scss'; -import { faCog, faCogs, faHome, faPrint } from "@fortawesome/pro-thin-svg-icons"; +import "../../../Sass/_leftMenu.scss"; +import { + faCog, + faCogs, + faHome, + faPrint, +} from "@fortawesome/pro-thin-svg-icons"; import LeftMenuItem from "./LeftMenuItem"; import LeftMenuSubMenu, { LOCLeftMenuSubMenu } from "./LeftMenuSubMenu"; +import { useTranslation } from "react-i18next"; +import { Namespaces } from "../../../i18n"; -interface LeftMenuProps { - -} - -interface LeftMenuState { - openMenuItem? : LOCLeftMenuSubMenu; -} - -class LeftMenu extends React.Component { - state : LeftMenuState = { - openMenuItem : undefined - } +const LeftMenu: React.FC = () => { + const { t } = useTranslation(); - componentDidMount(): void { - document.body.addEventListener('click', () => { - this.setState( { openMenuItem : undefined }) - }, true); - } + const [openMenuItem, setOpenMenuItem] = useState(); - componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { - if (prevState === this.state) - { - this.setState( { openMenuItem : undefined }) - } - } + // Close menus when clicking outside + useEffect(() => { + const handleClick = () => setOpenMenuItem(undefined); + document.body.addEventListener("click", handleClick, true); + return () => document.body.removeEventListener("click", handleClick, true); + }, []); - handleClick = (menuItem: LOCLeftMenuSubMenu) => { - const { openMenuItem } = this.state; + const handleClick = useCallback((menuItem: LOCLeftMenuSubMenu) => { + setOpenMenuItem((current) => (current === menuItem ? undefined : menuItem)); + }, []); - const newMenuItem = openMenuItem === menuItem ? undefined : menuItem; + // Access checks + const viewOrganisation = authentication.hasAccess("ViewOrganisation"); + const viewUser = authentication.hasAccess("ViewUser"); + const viewDomain = authentication.hasAccess("ViewDomain"); + const viewGlossary = authentication.hasAccess("ViewGlossary"); + const viewFormTemplate = authentication.hasAccess("ViewFormTemplate"); + const viewField = authentication.hasAccess("ViewField"); + const viewSequence = authentication.hasAccess("ViewSequence"); + const viewSsoManager = authentication.hasAccess("ViewSsoProviders"); - this.setState( { openMenuItem : newMenuItem }) - }; + const viewAdmin = + viewUser || + viewDomain || + viewGlossary || + viewFormTemplate || + viewField || + viewSequence; - render() { - const viewOrganisation = authentication.hasAccess("ViewOrganisation"); - - const viewUser = authentication.hasAccess("ViewUser" ); - const viewDomain = authentication.hasAccess("ViewDomain" ); - const viewGlossary = authentication.hasAccess("ViewGlossary"); - const viewFormTemplate = authentication.hasAccess("ViewFormTemplate"); - const viewField = authentication.hasAccess("ViewField"); - const viewSequence = authentication.hasAccess("ViewSequence"); - const viewSsoManager = authentication.hasAccess("ViewSsoProviders"); - - const viewAdmin = viewUser || viewDomain || viewGlossary || viewFormTemplate || viewField || viewSequence; - - const viewAuditLog = authentication.hasAccess("ViewAuditLog"); - const viewBlockedIPAddresses = authentication.hasAccess("ViewBlockedIPAddresses"); - const viewErrorLogs = authentication.hasAccess("ViewErrorLogs"); + const viewAuditLog = authentication.hasAccess("ViewAuditLog"); + const viewBlockedIPAddresses = authentication.hasAccess( + "ViewBlockedIPAddresses", + ); + const viewErrorLogs = authentication.hasAccess("ViewErrorLogs"); - const viewSupport = viewAuditLog || viewBlockedIPAddresses || viewErrorLogs; - - const { openMenuItem } = this.state; + const viewSupport = viewAuditLog || viewBlockedIPAddresses || viewErrorLogs; - return ( - <> -
- - {viewOrganisation && } - {viewAdmin && - {viewUser && } - {viewDomain && } - {viewGlossary && } - {viewFormTemplate && } - {viewField && } - {viewSequence && } - {viewSsoManager && } - - } - {viewSupport && - {viewAuditLog && } - {viewBlockedIPAddresses && } - {viewErrorLogs && } - } -
- {openMenuItem &&
{openMenuItem.props.children}
} - - ); - } -} + return ( + <> +
+ - + {viewOrganisation && ( + + )} + + {viewAdmin && ( + + {viewUser && } + {viewDomain && ( + + )} + {viewGlossary && ( + + )} + {viewFormTemplate && ( + + )} + {viewField && ( + + )} + {viewSequence && ( + + )} + {viewSsoManager && ( + + )} + + )} + + {viewSupport && ( + + {viewAuditLog && } + {viewBlockedIPAddresses && ( + + )} + {viewErrorLogs && ( + + )} + + )} +
+ + {openMenuItem && ( +
{openMenuItem.props.children}
+ )} + + ); +}; export default LeftMenu; diff --git a/src/modules/frame/components/Mainframe.tsx b/src/modules/frame/components/Mainframe.tsx index 7df4f09..a8a0091 100644 --- a/src/modules/frame/components/Mainframe.tsx +++ b/src/modules/frame/components/Mainframe.tsx @@ -4,26 +4,23 @@ import LeftMenu from "./LeftMenu"; import "../../../Sass/_frame.scss"; - type MainFrameProps = { - title?: string; - children?: React.ReactNode; // 👈️ type children + title?: string | undefined | null; + children?: React.ReactNode; // 👈️ type children }; const Mainframe = (props: MainFrameProps): JSX.Element => { - return ( -
- -
-
- -
-
- {props.children} -
-
+ return ( +
+ +
+
+
- ); +
{props.children}
+
+
+ ); }; export default Mainframe; diff --git a/src/modules/frame/models/JwtToken.ts b/src/modules/frame/models/JwtToken.ts index 3a8ba4e..073b8c9 100644 --- a/src/modules/frame/models/JwtToken.ts +++ b/src/modules/frame/models/JwtToken.ts @@ -1,8 +1,9 @@ export default interface JwtToken { - expiry: Date; - primarysid: bigint; - name: string; - email: string; - domainid: bigint; - securityPrivileges: []; + expiry: Date; + primarysid: bigint; + name: string; + email: string; + domainid: bigint; + securityPrivileges: []; + language?: string; } diff --git a/src/modules/frame/services/lanugageService.ts b/src/modules/frame/services/lanugageService.ts new file mode 100644 index 0000000..c4090a8 --- /dev/null +++ b/src/modules/frame/services/lanugageService.ts @@ -0,0 +1,21 @@ +import { getCurrentUser } from "./authenticationService"; + +export function determineInitialLanguage() { + // 1. JWT preference + const currentUser = getCurrentUser(); + if (currentUser !== null) { + const jwtLang = currentUser.language || null; + if (jwtLang) return jwtLang; + } + + // 2. LocalStorage + //const storedLang = localStorage.getItem("appLanguage"); + //if (storedLang) return storedLang; + + // 3. Browser + const browserLang = navigator.language?.split("-")[0]; + if (browserLang) return browserLang; + + // 4. Default + return "en"; +} diff --git a/tsconfig.json b/tsconfig.json index ed38e30..8cfde08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -20,8 +16,5 @@ "noEmit": true, "jsx": "preserve" }, - "include": [ - "src", - "src/types" - ] + "include": ["src", "src/types", "i18next-parser.config.js"] }