diff --git a/i18n/i18n.ts b/i18n/i18n.ts new file mode 100644 index 0000000..59cb72c --- /dev/null +++ b/i18n/i18n.ts @@ -0,0 +1,27 @@ +import i18n from 'i18next' +import settingsEN from './locales/en/settings.json' + +import settingsDE from './locales/de/settings.json' + +import settingsNL from './locales/nl/settings.json' + +i18n.init({ + resources: { + en: { + settings: settingsEN + }, + de: { + settings: settingsDE, + }, + nl: { + settings: settingsNL, + }, + }, + ns: ["settings"], + fallbackLng: 'en', + interpolation: { + escapeValue: false // not needed for react as it escapes by default + }, +}); + +export default i18n \ No newline at end of file diff --git a/i18n/locales/de/settings.json b/i18n/locales/de/settings.json new file mode 100644 index 0000000..b22a8ab --- /dev/null +++ b/i18n/locales/de/settings.json @@ -0,0 +1,19 @@ +{ + "title": "Kontoeinstellungen", + "heading": "Profilverwaltung und Sicherheitseinstellungen", + "subtitle": "Profileinstellungen", + "changePassword": "Passwort ändern", + "setPassword": "Passwort festlegen", + "backUpCodes": "Verwalten von 2FA-Backup-Wiederherstellungscodes", + "backUpCodesDescription": "Wiederherstellungscodes können in Paniksituationen verwendet werden, wenn Sie den Zugriff auf Ihr 2FA-Gerät verloren haben.", + "totpAuthenticator": "2FA TOTP Authenticator App verwalten", + "addTotpAuthenticator": "Fügen Sie Ihrem Konto eine TOTP-Authentifikator-App hinzu, um die Sicherheit Ihres Kontos zu verbessern.", + "popularAuthenticatorApps": "Beliebte Authenticator-Apps sind", + "and": "und", + "back": "Zurück", + "email": "E-Mail", + "firstName": "Vorname", + "lastName": "Nachname", + "password": "Passwort", + "save": "Speichern" +} \ No newline at end of file diff --git a/i18n/locales/en/settings.json b/i18n/locales/en/settings.json new file mode 100644 index 0000000..95b2368 --- /dev/null +++ b/i18n/locales/en/settings.json @@ -0,0 +1,19 @@ +{ + "title": "Account settings", + "heading": "Profile management and security settings", + "subtitle": "Profile Settings", + "changePassword": "Change Password", + "setPassword": "Set Password", + "backUpCodes": "Manage 2FA backup recovery codes", + "backUpCodesDescription": "Recovery codes can be used in panic situations where you have lost access to your 2FA device.", + "totpAuthenticator": "Manage 2FA TOTP Authenticator App", + "addTotpAuthenticator": "Add a TOTP Authenticator App to your account to improve your account security.", + "popularAuthenticatorApps": "Popular Authenticator Apps are", + "and": "and", + "back": "Back", + "email": "E-Mail", + "firstName": "First Name", + "lastName": "Last Name", + "password": "Password", + "save": "Save" +} \ No newline at end of file diff --git a/i18n/locales/nl/settings.json b/i18n/locales/nl/settings.json new file mode 100644 index 0000000..f19471b --- /dev/null +++ b/i18n/locales/nl/settings.json @@ -0,0 +1,19 @@ +{ + "title": "Accountinstellingen", + "heading": "Profielbeheer en beveiligingsinstellingen", + "subtitle": "Profielinstellingen", + "changePassword": "Wachtwoord wijzigen", + "setPassword": "Wachtwoord instellen", + "backUpCodes": "Beheer 2FA-back-upherstelcodes", + "backUpCodesDescription": "Herstelcodes kunnen worden gebruikt in panieksituaties waarin u geen toegang meer hebt tot uw 2FA-apparaat.", + "totpAuthenticator": "Beheer 2FA TOTP Authenticator-app", + "addTotpAuthenticator": "Voeg een TOTP Authenticator-app toe aan uw account om de beveiliging ervan te verbeteren.", + "popularAuthenticatorApps": "Populaire authenticatie-apps zijn", + "and": "En", + "back": "Rug", + "email": "E-mail", + "firstName": "Voornaam", + "lastName": "Achternaam", + "password": "Wachtwoord", + "save": "Redden" +} \ No newline at end of file diff --git a/package.json b/package.json index 9489b07..457ecaf 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "eslint": "8.38.0", "eslint-config-next": "13.3.0", "express": "^4.18.2", + "i18next": "^25.3.2", "next": "^12.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -42,4 +43,4 @@ "postcss": "^8.4.23", "tailwindcss": "^3.4.4" } -} \ No newline at end of file +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 96e8c82..16dfdb6 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,8 +1,10 @@ +import i18n from "@/i18n/i18n"; import "../styles/tailwind.css" import '@/submodules/tailwind-config/global.css'; import { theme, globalStyles, ThemeProps } from "@ory/themes" import type { AppProps } from "next/app" import Head from "next/head" +import { I18nextProvider } from "react-i18next"; import { ToastContainer } from "react-toastify" import "react-toastify/dist/ReactToastify.css" import { ThemeProvider } from "styled-components" @@ -14,7 +16,7 @@ const GlobalStyle = createGlobalStyle((props: ThemeProps) => function MyApp({ Component, pageProps }: AppProps) { return ( - <> + kern - + ) } diff --git a/pages/settings.tsx b/pages/settings.tsx index 9bffed2..f47ed52 100644 --- a/pages/settings.tsx +++ b/pages/settings.tsx @@ -17,6 +17,7 @@ import { Application, CurrentPage } from "@/submodules/react-components/hooks/we import { AdminMessage } from "@/submodules/react-components/types/admin-messages" import { postProcessAdminMessages } from "@/submodules/react-components/helpers/admin-messages-helper" import AdminMessages from "@/submodules/react-components/components/AdminMessages" +import { useTranslation } from "react-i18next" const Settings: NextPage = () => { const [initialFlow, setInitialFlow]: any = useState() @@ -32,6 +33,32 @@ const Settings: NextPage = () => { // Get ?flow=... from the URL const router = useRouter() const { flow: flowId, return_to: returnTo } = router.query + const { t, i18n } = useTranslation('settings'); + const [language, setLanguage] = useState(undefined); + const [showPassword, setShowPassword] = useState(false); + const [showAuthenticator, setShowAuthenticator] = useState(false); + const [loadPage, setLoadPage] = useState(false); + const [canShow, setCanShow] = useState(false); + + useEffect(() => { + if (loadPage) return; + setTimeout(() => { + setLoadPage(true); + const emailButtonVal = (document.querySelector('input[name="traits.email"]') as HTMLInputElement)?.value; + const firstNameButtonVal = (document.querySelector('input[name="traits.name.first"]') as HTMLInputElement)?.value; + const lastNameButtonVal = (document.querySelector('input[name="traits.name.last"]') as HTMLInputElement)?.value; + if (firstNameButtonVal !== "" && lastNameButtonVal !== "" && firstNameButtonVal !== undefined && lastNameButtonVal !== undefined && emailButtonVal !== "" && emailButtonVal !== undefined && flowId) { + setShowPassword(true); + document.querySelector('button[value="profile"]')?.setAttribute("class", "hidden"); + setTimeout(() => { + const existsPassword = document.querySelector('input[name="password"]') !== null; + if (!existsPassword) { + document.querySelector('button[value="profile"]')?.setAttribute("class", "block"); + } + }, 100); + } + }, 1000); + }, [loadPage, flowId]); useEffect(() => { getUserInfoExtended((res) => { @@ -42,6 +69,7 @@ const Settings: NextPage = () => { WebSocketsService.setConnectionOpened(true); WebSocketsService.initWsNotifications(); } + setLanguage(res?.languageDisplay); } }); }, []); @@ -62,6 +90,11 @@ const Settings: NextPage = () => { useWebsocket(user?.organizationId, Application.ENTRY, CurrentPage.ENTRY_LAYOUT, handleWebsocketNotification) + useEffect(() => { + if (!language) return; + i18n.changeLanguage(language); + }, [language, i18n]); + useEffect(() => { // If the router is not ready yet, or we already have a flow, do nothing. if (!router.isReady || initialFlow) { @@ -91,7 +124,7 @@ const Settings: NextPage = () => { }, [flowId, router, router.isReady, returnTo, initialFlow]) useEffect(() => { - if (!initialFlow) return; + if (!initialFlow || !loadPage) return; initialFlow.ui.nodes = prepareNodes(initialFlow); const checkIfTotp = initialFlow.ui.nodes.find((node: UiNode) => node.group === "totp"); const checkIfBackupCodes = initialFlow.ui.nodes.find((node: UiNode) => node.group === "lookup_secret"); @@ -104,10 +137,11 @@ const Settings: NextPage = () => { setChangedFlow(initialFlow) //prevent password change option display if sso - requestAnimationFrame(() => { + setTimeout(() => { if (["microsoft", "google"].includes(initialFlow.identity.metadata_public?.registration_scope?.provider_id)) { initialFlow.ui.nodes = initialFlow.ui.nodes.filter((node: UiNode) => node.group !== "password"); setIsOidc(true); + setCanShow(true); if (initialFlow.identity.metadata_public?.registration_scope?.invitation_sso) { setIsOidcInvitation(true); } @@ -118,15 +152,19 @@ const Settings: NextPage = () => { document.querySelector('button[value="Google"]')?.setAttribute("class", "hidden"); } } - }); - }, [initialFlow]) + else { + setIsOidc(false); + setCanShow(true); + } + }, 100); + }, [initialFlow, loadPage]) useEffect(() => { - if (!changedFlow || !initialFlow) return; + if (!changedFlow || !initialFlow || !loadPage) return; const firstNameButtonVal = (document.querySelector('input[name="traits.name.first"]') as HTMLInputElement)?.value; const lastNameButtonVal = (document.querySelector('input[name="traits.name.last"]') as HTMLInputElement)?.value; if (isOidc && isOidcInvitation) { - if (firstNameButtonVal === "" || lastNameButtonVal === "") { + if ((firstNameButtonVal === "" || lastNameButtonVal === "" || firstNameButtonVal === undefined || lastNameButtonVal === undefined) && flowId) { setBackButtonDisabled(true); } else { setBackButtonDisabled(false); @@ -134,20 +172,45 @@ const Settings: NextPage = () => { } else { const emailButtonVal = (document.querySelector('input[name="traits.email"]') as HTMLInputElement)?.value; const passwordButtonVal = (document.querySelector('input[name="password"]') as HTMLInputElement)?.value; - if (firstNameButtonVal === "" || lastNameButtonVal === "" || emailButtonVal === "" || passwordButtonVal === "") { + if (firstNameButtonVal !== "" && lastNameButtonVal !== "" && firstNameButtonVal !== undefined && lastNameButtonVal !== undefined) { + setShowPassword(true); + if (flowId && !isOidc) { + document.querySelector('button[value="profile"]')?.setAttribute("class", "hidden"); + } + } + if (firstNameButtonVal !== "" && lastNameButtonVal !== "" && passwordButtonVal !== "" && passwordButtonVal !== undefined) { + setShowAuthenticator(true); + } + if ((firstNameButtonVal === "" || lastNameButtonVal === "" || emailButtonVal === "" || passwordButtonVal === "" || firstNameButtonVal === undefined || lastNameButtonVal === undefined || emailButtonVal === undefined || passwordButtonVal === undefined) && flowId) { setBackButtonDisabled(true); } else { setBackButtonDisabled(false); } } - }, [isOidc, isOidcInvitation, initialFlow, changedFlow]); + }, [isOidc, isOidcInvitation, initialFlow, changedFlow, flowId, loadPage]); useEffect(() => { if (!changedFlow) return; if (changedFlow.ui.messages) { - setMessages(changedFlow.ui.messages); + const messagesCopy = [...initialFlow.ui.messages]; + const messagesMapped = messagesCopy.map((message: any) => { + if (flowId) { + if (message.id === 1060001 && isOidc && isOidcInvitation) { + return { ...message, id: 1060001 + 'a' }; + } + if (message.id === 1050001 && showPassword) { + return { ...message, id: 1050001 + 'a' }; + } + if (message.id === 1060001 && !isOidc && !isOidcInvitation) { + return { ...message, id: 1060001 + 'b' }; + + } + } + return message; + }); + setMessages(messagesMapped); } - }, [changedFlow]) + }, [changedFlow, isOidc, isOidcInvitation, showPassword, flowId, isOidc]) const onSubmit = (values: UpdateSettingsFlowBody) => ory @@ -170,21 +233,36 @@ const Settings: NextPage = () => { return Promise.reject(err) }) + + + useEffect(() => { + if (backButtonDisabled || !changedFlow || !changedFlow.ui.messages || !flowId) return; + const messagesCopy = [...changedFlow.ui.messages]; + const messagesMapped = messagesCopy.map((message: any) => { + if (message.id === 1050001 && !isOidc && showPassword && !backButtonDisabled) { + router.push('/cognition'); + return { ...message, id: '1050001ab' }; + } + return message; + }); + setMessages(messagesMapped); + }, [backButtonDisabled, changedFlow, flowId, isOidc, showPassword]); + return ( <> - Account settings + {t('title')}
-
-

Profile management and security settings

+ {(language && loadPage) &&
+

{t('heading')}

-

Profile Settings

+

{t('subtitle')}

{ flow={changedFlow} />
- {!isOidc ? -
-

{flowId ? 'Set' : 'Change'} password

+ + {(!isOidc && canShow) && <> + {((flowId && showPassword) || !flowId) && ( +
+

{!flowId ? t('changePassword') : t('setPassword')}

+ +
+ )} + } + + {showAuthenticator && <> + {containsBackupCodes ? (
+

{t('backUpCodes')}

+

{t('backUpCodesDescription')}

-
: null} - - {containsBackupCodes ? (
-

Manage 2FA backup recovery codes

-

Recovery codes can be used in panic situations where you have lost access to your 2FA device.

- -
) : (<> )} +
) : (<> )} - {containsTotp ? (
-

Manage 2FA TOTP Authenticator App

-

Add a TOTP Authenticator App to your account to improve your account security. - Popular Authenticator Apps are LastPass and Google - Authenticator (iOS, Android). -

- -
) : (<> )} + {containsTotp ? (
+

{t('totpAuthenticator')}

+

{t('addTotpAuthenticator')} + {t('popularAuthenticatorApps')} LastPass{t('and')} Google + Authenticator (iOS, Android). +

+ +
) : (<> )} + } - {isOidc && isOidcInvitation ? (
+ {(isOidc && isOidcInvitation) ? (
{
+ }}>{t('back')}
-
+
}
diff --git a/pkg/ui/Messages.tsx b/pkg/ui/Messages.tsx index ddcb3ad..bcb4000 100644 --- a/pkg/ui/Messages.tsx +++ b/pkg/ui/Messages.tsx @@ -1,16 +1,20 @@ import { displayMessage } from "@/util/helper-functions" import { UiText } from "@ory/client" import { Alert, AlertContent } from "@ory/themes" +import { useTranslation } from "react-i18next" interface MessageProps { message: UiText } export const Message = ({ message }: MessageProps) => { + const { i18n } = useTranslation(); + const language = i18n.language; + return ( - {displayMessage(message)} + {displayMessage(message, language)} ) diff --git a/pkg/ui/NodeInputDefault.tsx b/pkg/ui/NodeInputDefault.tsx index 0619fce..0e5e64b 100644 --- a/pkg/ui/NodeInputDefault.tsx +++ b/pkg/ui/NodeInputDefault.tsx @@ -3,9 +3,14 @@ import { TextInput } from "@ory/themes" import { NodeInputProps } from "./helpers" import { getNodeLabel } from "@ory/integrations/ui" import { Message } from "./Messages" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" export function NodeInputDefault(props: NodeInputProps) { const { node, attributes, value = "", setValue, disabled } = props + const { t } = useTranslation('settings'); + const [labelName, setLabelName] = useState(""); + // Some attributes have dynamic JavaScript - this is for example required for WebAuthn. const onClick = () => { @@ -18,10 +23,30 @@ export function NodeInputDefault(props: NodeInputProps) { } } + useEffect(() => { + // The settings page needs translated labels & placeholder. + switch (getNodeLabel(node)) { + case "E-Mail": + setLabelName(t('email')); + break; + case "First Name": + setLabelName(t('firstName')); + break; + case "Last Name": + setLabelName(t('lastName')); + break; + case "Password": + setLabelName(t('password')); + break; + default: + setLabelName(getNodeLabel(node)); + } + }, [node, t]); + // Render a generic text input field. return ( { setValue(e.target.value) @@ -29,7 +54,7 @@ export function NodeInputDefault(props: NodeInputProps) { className={"text-input" + (props.visible ? "" : " hidden")} type={attributes.type} name={attributes.name} - placeholder={getNodeLabel(node)} + placeholder={labelName} value={value} disabled={attributes.disabled || disabled} help={node.messages.length > 0} diff --git a/pkg/ui/NodeInputSubmit.tsx b/pkg/ui/NodeInputSubmit.tsx index 331c7b3..1c79504 100644 --- a/pkg/ui/NodeInputSubmit.tsx +++ b/pkg/ui/NodeInputSubmit.tsx @@ -2,6 +2,8 @@ import { getNodeLabel } from "@ory/integrations/ui" import { Button } from "@ory/themes" import { NodeInputProps } from "./helpers" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" export function NodeInputSubmit({ @@ -9,6 +11,20 @@ export function NodeInputSubmit({ attributes, disabled, }: NodeInputProps) { + const { t } = useTranslation('settings'); + const [buttonName, setButtonName] = useState(""); + + useEffect(() => { + // The settings page needs translated labels. + switch (getNodeLabel(node)) { + case "Save": + setButtonName(t('save')); + break; + default: + setButtonName(getNodeLabel(node)); + } + }, [node, t]); + return ( <> {node.meta.label?.text == "Sign up" ? @@ -22,7 +38,7 @@ export function NodeInputSubmit({ value={attributes.value || ""} disabled={attributes.disabled || disabled} > - {getNodeLabel(node)} + {buttonName} ) diff --git a/styles/tailwind.css b/styles/tailwind.css index a182057..e01732f 100644 --- a/styles/tailwind.css +++ b/styles/tailwind.css @@ -257,5 +257,5 @@ button.button-sign-in:focus { } .hidden { - display: none; + display: none !important; } \ No newline at end of file diff --git a/submodules/javascript-functions b/submodules/javascript-functions index 5a07039..dcbd2e8 160000 --- a/submodules/javascript-functions +++ b/submodules/javascript-functions @@ -1 +1 @@ -Subproject commit 5a070394a45b5ed5a86efd0fc42370331e0ab101 +Subproject commit dcbd2e811fdf0f3f950ec0a612371fd38f6152b7 diff --git a/submodules/react-components b/submodules/react-components index 66af8ce..1f1dc08 160000 --- a/submodules/react-components +++ b/submodules/react-components @@ -1 +1 @@ -Subproject commit 66af8ce324318e31af8d6e2d812396d32dad57ef +Subproject commit 1f1dc08839bbf7e02e1ddafa774af43fdbb7197a diff --git a/util/helper-functions.ts b/util/helper-functions.ts index 466d040..d9c6221 100644 --- a/util/helper-functions.ts +++ b/util/helper-functions.ts @@ -1,7 +1,39 @@ -const customMessageOverrides = { - 1060001: "Welcome to the app! You have successfully registered. Set your first, last name and password to continue.", +const customMessageOverridesEnglish = { + 1060001: "Welcome to the app! You have successfully registered. Set your first and last name to continue.", + '1060001a': "Welcome to the app! You have successfully registered. Set your first, last name and link your account to continue.", + '1060001b': "Welcome to the app! You have successfully recovered your account.", + 1050001: "Your changes are saved!", + '1050001a': "Your changes are saved! Please set your password to continue.", + '1050001ab': "Your password has been set successfully! ", + 4000032: "The password must be at least 8 characters long, but got less.", + 4000034: "The password has been found in data breaches and must no longer be used.", + 4060004: "This link is invalid, add your email below to receive a new one" }; +const customMessageOverridesGerman = { + 1060001: "Willkommen in der App! Sie haben sich erfolgreich registriert. Bitte geben Sie Ihren Vor- und Nachnamen ein, um fortzufahren.", + '1060001a': "Willkommen in der App! Sie haben sich erfolgreich registriert. Bitte geben Sie Ihren Vor- und Nachnamen ein und verknüpfen Sie Ihr Konto, um fortzufahren.", + '1060001b': "Willkommen in der App! Sie haben Ihr Konto erfolgreich wiederhergestellt.", + 1050001: "Ihre Änderungen wurden gespeichert!", + '1050001a': "Ihre Änderungen wurden gespeichert! Bitte setzen Sie Ihr Passwort, um fortzufahren.", + '1050001ab': "Ihr Passwort wurde erfolgreich gesetzt! ", + 4000032: "Das Passwort muss mindestens 8 Zeichen lang sein, aber es wurden weniger als 8 Zeichen eingegeben.", + 4000034: "Das Passwort wurde in Datenpannen gefunden und darf nicht mehr verwendet werden.", + 4060004: "Dieser Link ist ungültig, geben Sie Ihre E-Mail unten ein, um einen neuen zu erhalten" +} + +const customMessageOverridesDutch = { + 1060001: "Welkom bij de app! Je bent succesvol geregistreerd. Vul je voor- en achternaam in om verder te gaan.", + '1060001a': "Welkom bij de app! Je bent succesvol geregistreerd. Vul je voor- en achternaam in en koppel je account om verder te gaan.", + '1060001b': "Welkom bij de app! Je hebt je account succesvol hersteld.", + 1050001: "Uw wijzigingen zijn opgeslagen!", + '1050001a': "Uw wijzigingen zijn opgeslagen! Stel uw wachtwoord in om verder te gaan.", + '1050001ab': "Uw wachtwoord is succesvol ingesteld! ", + 4000032: "Het wachtwoord moet minimaal 8 tekens lang zijn, maar er zijn minder dan 8 tekens ingevoerd.", + 4000034: "Het wachtwoord is gevonden in datalekken en mag niet meer worden gebruikt.", + 4060004: "Deze link is ongeldig, voeg uw e-mailadres hieronder toe om een nieuwe te ontvangen." +} + export function getValueIdentifier(selectedRole: any) { let value = ''; if (selectedRole === 'engineer') { @@ -64,6 +96,7 @@ export function prepareNodes(flow: any) { return filteredNodes; } -export function displayMessage(msg: any): string { - return customMessageOverrides[msg.id] || msg.text; +export function displayMessage(msg: any, language: string): string { + const selectDictMessages = language === "de" ? customMessageOverridesGerman : language === "nl" ? customMessageOverridesDutch : customMessageOverridesEnglish; + return selectDictMessages[msg.id] || msg.text; } \ No newline at end of file