feat(web): i18n (#2697)

This adds support for i18n so that users may be presented a familiar language to the language the browser language they are using automatically. Currently supported languages: en, es.

Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
This commit is contained in:
Manuel Nuñez 2022-01-21 09:05:46 -03:00 committed by GitHub
parent a7a2bc63fe
commit db046b2d1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 308 additions and 63 deletions

View File

@ -12,11 +12,15 @@
"@material-ui/styles": "4.11.4",
"axios": "0.25.0",
"classnames": "2.3.1",
"i18next": "21.6.0",
"i18next-browser-languagedetector": "6.1.2",
"i18next-http-backend": "1.3.1",
"qrcode.react": "1.0.1",
"query-string": "7.1.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-ga": "3.3.0",
"react-i18next": "11.14.3",
"react-loading": "2.0.3",
"react-otp-input": "2.4.0",
"react-router-dom": "6.2.1",
@ -77,6 +81,9 @@
"^@hooks/(.*)$": [
"<rootDir>/src/hooks/$1"
],
"^@i18n/(.*)$": [
"<rootDir>/src/i18n/$1"
],
"^@layouts/(.*)$": [
"<rootDir>/src/layouts/$1"
],

View File

@ -33,6 +33,9 @@ specifiers:
eslint-plugin-react: 7.28.0
eslint-plugin-react-hooks: 4.3.0
husky: 7.0.4
i18next: 21.6.0
i18next-browser-languagedetector: 6.1.2
i18next-http-backend: 1.3.1
jest: 27.4.7
jest-transform-stub: 2.0.0
jest-watch-typeahead: 1.0.0
@ -42,6 +45,7 @@ specifiers:
react: 17.0.2
react-dom: 17.0.2
react-ga: 3.3.0
react-i18next: 11.14.3
react-loading: 2.0.3
react-otp-input: 2.4.0
react-router-dom: 6.2.1
@ -64,11 +68,15 @@ dependencies:
'@material-ui/styles': 4.11.4_b3482aaf5744fc7c2aeb7941b0e0a78f
axios: 0.25.0
classnames: 2.3.1
i18next: 21.6.0
i18next-browser-languagedetector: 6.1.2
i18next-http-backend: 1.3.1
qrcode.react: 1.0.1_react@17.0.2
query-string: 7.1.0
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
react-ga: 3.3.0_react@17.0.2
react-i18next: 11.14.3_i18next@21.6.0+react@17.0.2
react-loading: 2.0.3_react@17.0.2
react-otp-input: 2.4.0_react-dom@17.0.2+react@17.0.2
react-router-dom: 6.2.1_react-dom@17.0.2+react@17.0.2
@ -3677,6 +3685,12 @@ packages:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
/cross-fetch/3.1.4:
resolution: {integrity: sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==}
dependencies:
node-fetch: 2.6.1
dev: false
/cross-spawn/6.0.5:
resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
engines: {node: '>=4.8'}
@ -5004,6 +5018,12 @@ packages:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
dev: true
/html-parse-stringify/3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
dependencies:
void-elements: 3.1.0
dev: false
/http-proxy-agent/4.0.1:
resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
engines: {node: '>= 6'}
@ -5040,6 +5060,24 @@ packages:
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
dev: false
/i18next-browser-languagedetector/6.1.2:
resolution: {integrity: sha512-YDzIGHhMRvr7M+c8B3EQUKyiMBhfqox4o1qkFvt4QXuu5V2cxf74+NCr+VEkUuU0y+RwcupA238eeolW1Yn80g==}
dependencies:
'@babel/runtime': 7.16.3
dev: false
/i18next-http-backend/1.3.1:
resolution: {integrity: sha512-o79n4GBBRpl20hByC+ne/S1UaSZ4iGAn59Hu2TEZGjN0WLB72L7WrM39Cshziyrssp6MQfdI8wjToU2Q6kpSvA==}
dependencies:
cross-fetch: 3.1.4
dev: false
/i18next/21.6.0:
resolution: {integrity: sha512-RjNuACL35wWZgtkyMcjcCmK7R72u3P6jTNbGKzrvHGI9M0iK5Vn1DsBIwOByppaXLIbe0viJ79Nz2h8w1UwPoQ==}
dependencies:
'@babel/runtime': 7.16.3
dev: false
/iconv-lite/0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@ -6474,6 +6512,11 @@ packages:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
dev: true
/node-fetch/2.6.1:
resolution: {integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==}
engines: {node: 4.x || >=6.0.0}
dev: false
/node-int64/0.4.0:
resolution: {integrity: sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=}
dev: true
@ -6951,6 +6994,18 @@ packages:
react: 17.0.2
dev: false
/react-i18next/11.14.3_i18next@21.6.0+react@17.0.2:
resolution: {integrity: sha512-Hf2aanbKgYxPjG8ZdKr+PBz9sY6sxXuZWizxCYyJD2YzvJ0W9JTQcddVEjDaKyBoCyd3+5HTerdhc9ehFugc6g==}
peerDependencies:
i18next: '>= 19.0.0'
react: '>= 16.8.0'
dependencies:
'@babel/runtime': 7.16.3
html-parse-stringify: 3.0.1
i18next: 21.6.0
react: 17.0.2
dev: false
/react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@ -8102,6 +8157,11 @@ packages:
fsevents: 2.3.2
dev: true
/void-elements/3.1.0:
resolution: {integrity: sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=}
engines: {node: '>=0.10.0'}
dev: false
/w3c-hr-time/1.0.2:
resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
dependencies:

35
web/src/i18n/index.ts Normal file
View File

@ -0,0 +1,35 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import XHR from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import langEn from "@i18n/locales/en.json";
import langEs from "@i18n/locales/es.json";
const resources = {
en: langEn,
es: langEs,
};
const options = {
order: ["querystring", "navigator"],
lookupQuerystring: "lng",
};
i18n.use(XHR)
.use(LanguageDetector)
.use(initReactI18next)
.init({
detection: options,
resources,
ns: [""],
defaultNS: "",
fallbackLng: "en",
supportedLngs: ["en", "es"],
interpolation: {
escapeValue: false,
},
debug: false,
});
export default i18n;

View File

@ -0,0 +1,53 @@
{
"Portal": {
"An email has been sent to your address to complete the process": "An email has been sent to your address to complete the process.",
"Authenticated": "Authenticated",
"Cancel": "Cancel",
"Contact your administrator to register a device": "Contact your administrator to register a device.",
"Could not obtain user settings": "Could not obtain user settings",
"Done": "Done",
"Enter new password": "Enter new password",
"Enter one-time password": "Enter one-time password",
"Failed to register device, the provided link is expired or has already been used": "Failed to register device, the provided link is expired or has already been used",
"Hi": "Hi",
"Incorrect username or password": "Incorrect username or password.",
"Loading": "Loading",
"Logout": "Logout",
"Lost your device?": "Lost your device?",
"Methods": "Methods",
"Need Google Authenticator?": "Need Google Authenticator?",
"New password": "New password",
"No verification token provided": "No verification token provided",
"OTP Secret copied to clipboard": "OTP Secret copied to clipboard.",
"OTP URL copied to clipboard": "OTP URL copied to clipboard.",
"One-Time Password": "One-Time Password",
"Password has been reset": "Password has been reset.",
"Password": "Password",
"Passwords do not match": "Passwords do not match.",
"Push Notification": "Push Notification",
"Register device": "Register device",
"Register your first device by clicking on the link below": "Register your first device by clicking on the link below.",
"Remember me": "Remember me",
"Repeat new password": "Repeat new password",
"Reset password": "Reset password",
"Reset password?": "Reset password?",
"Reset": "Reset",
"Scan QR Code": "Scan QR Code",
"Secret": "Secret",
"Security Key - U2F": "Security Key - U2F",
"Select a Device": "Select a Device",
"Sign in": "Sign in",
"Sign out": "Sign out",
"The resource you're attempting to access requires two-factor authentication": "The resource you're attempting to access requires two-factor authentication.",
"There was a problem initiating the registration process": "There was a problem initiating the registration process",
"There was an issue completing the process. The verification token might have expired": "There was an issue completing the process. The verification token might have expired.",
"There was an issue initiating the password reset process": "There was an issue initiating the password reset process.",
"There was an issue resetting the password": "There was an issue resetting the password",
"There was an issue signing out": "There was an issue signing out",
"Time-based One-Time Password": "Time-based One-Time Password",
"Username": "Username",
"You must open the link from the same device and browser that initiated the registration process": "You must open the link from the same device and browser that initiated the registration process",
"You're being signed out and redirected": "You're being signed out and redirected",
"Your supplied password does not meet the password policy requirements": "Your supplied password does not meet the password policy requirements."
}
}

View File

@ -0,0 +1,53 @@
{
"Portal": {
"An email has been sent to your address to complete the process": "Un correo ha sido enviado a su cuenta para completar el proceso",
"Authenticated": "Autenticado",
"Cancel": "Cancelar",
"Contact your administrator to register a device": "Contacte a su administrador para registrar un dispositivo.",
"Could not obtain user settings": "Error al obtener configuración de usuario",
"Done": "Hecho",
"Enter new password": "Ingrese una nueva contraseña",
"Enter one-time password": "Ingrese contraseña de un solo uso (OTP)",
"Failed to register device, the provided link is expired or has already been used": "Error al registrar dispositivo, el link expiró o ya ha sido utilizado",
"Hi": "Hola",
"Incorrect username or password": "Usuario y/o contraseña incorrectos",
"Loading": "Cargando",
"Logout": "Cerrar Sesión",
"Lost your device?": "Perdió su dispositivo?",
"Methods": "Métodos",
"Need Google Authenticator?": "Necesita Google Authenticator?",
"New password": "Nueva contraseña",
"No verification token provided": "No se ha recibido el token de verificación",
"OTP Secret copied to clipboard": "La clave OTP ha sido copiada al portapapeles",
"OTP URL copied to clipboard": "la URL OTP ha sido copiada al portapapeles.",
"One-Time Password": "Contraseña de un solo uso (OTP)",
"Password has been reset": "La contraseña ha sido restablecida.",
"Password": "Contraseña",
"Passwords do not match": "Las contraseñas no coinciden.",
"Push Notification": "Notificaciones Push",
"Register device": "Registrar Dispositivo",
"Register your first device by clicking on the link below": "Registre su primer dispositivo, haciendo click en el siguiente link.",
"Remember me": "Recordarme",
"Repeat new password": "Repetir la contraseña",
"Reset password": "Restablecer Contraseña",
"Reset password?": "Olvidé mi contraseña",
"Reset": "Restablecer",
"Scan QR Code": "Escanear Código QR",
"Secret": "Secreto",
"Security Key - U2F": "Llave de Seguridad - U2F",
"Select a Device": "Seleccionar Dispositivo",
"Sign in": "Iniciar Sesión",
"Sign out": "Cerrar Sesión",
"The resource you're attempting to access requires two-factor authentication": "El recurso que intenta alcanzar requiere un segundo factor de autenticación (2FA).",
"There was a problem initiating the registration process": "Ocurrió un problema al iniciar el proceso de registración",
"There was an issue completing the process. The verification token might have expired": "Ocurrió un problema mientras se completaba el proceso. El token de verificación pudo haber expirado.",
"There was an issue initiating the password reset process": "Ha ocurrido un error al iniciar el proceso de proceso de restauración de contraseña.",
"There was an issue resetting the password": "Ocurrió un error al intentar restablecer la contraseña",
"There was an issue signing out": "Ocurrió un error al intentar cerrar sesión",
"Time-based One-Time Password": "Contraseña de uso único - OTP",
"Username": "Usuario",
"You must open the link from the same device and browser that initiated the registration process": "Debe abrir el link desde el mismo dispositivo y navegador desde el que inició el proceso de registración",
"You're being signed out and redirected": "Cerrando Sesión y redirigiendo",
"Your supplied password does not meet the password policy requirements": "La contraseña suministrada no cumple con los requerimientos de la política de contraseñas"
}
}

View File

@ -5,6 +5,7 @@ import ReactDOM from "react-dom";
import "@root/index.css";
import App from "@root/App";
import * as serviceWorker from "@root/serviceWorker";
import "./i18n/index.ts";
ReactDOM.render(<App />, document.getElementById("root"));

View File

@ -6,6 +6,7 @@ import { makeStyles, Typography, Button, IconButton, Link, CircularProgress, Tex
import { red } from "@material-ui/core/colors";
import classnames from "classnames";
import QRCode from "qrcode.react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
import AppStoreBadges from "@components/AppStoreBadges";
@ -26,6 +27,7 @@ const RegisterOneTimePassword = function () {
const { createSuccessNotification, createErrorNotification } = useNotifications();
const [hasErrored, setHasErrored] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { t: translate } = useTranslation("Portal");
// Get the token from the query param to give it back to the API when requesting
// the secret for OTP.
@ -49,17 +51,19 @@ const RegisterOneTimePassword = function () {
console.error(err);
if ((err as Error).message.includes("Request failed with status code 403")) {
createErrorNotification(
translate(
"You must open the link from the same device and browser that initiated the registration process",
),
);
} else {
createErrorNotification(
"Failed to register device, the provided link is expired or has already been used",
translate("Failed to register device, the provided link is expired or has already been used"),
);
}
setHasErrored(true);
}
setIsLoading(false);
}, [processToken, createErrorNotification]);
}, [processToken, createErrorNotification, translate]);
useEffect(() => {
completeRegistrationProcess();
@ -82,10 +86,12 @@ const RegisterOneTimePassword = function () {
const qrcodeFuzzyStyle = isLoading || hasErrored ? style.fuzzy : undefined;
return (
<LoginLayout title="Scan QR Code">
<LoginLayout title={translate("Scan QR Code")}>
<div className={style.root}>
<div className={style.googleAuthenticator}>
<Typography className={style.googleAuthenticatorText}>Need Google Authenticator?</Typography>
<Typography className={style.googleAuthenticatorText}>
{translate("Need Google Authenticator?")}
</Typography>
<AppStoreBadges
iconSize={128}
targetBlank
@ -105,7 +111,7 @@ const RegisterOneTimePassword = function () {
{secretURL !== "empty" ? (
<TextField
id="secret-url"
label="Secret"
label={translate("Secret")}
className={style.secret}
value={secretURL}
InputProps={{
@ -113,8 +119,12 @@ const RegisterOneTimePassword = function () {
}}
/>
) : null}
{secretBase32 ? SecretButton(secretBase32, "OTP Secret copied to clipboard.", faKey) : null}
{secretURL !== "empty" ? SecretButton(secretURL, "OTP URL copied to clipboard.", faCopy) : null}
{secretBase32
? SecretButton(secretBase32, translate("OTP Secret copied to clipboard"), faKey)
: null}
{secretURL !== "empty"
? SecretButton(secretURL, translate("OTP URL copied to clipboard"), faCopy)
: null}
</div>
<Button
variant="contained"
@ -123,7 +133,7 @@ const RegisterOneTimePassword = function () {
onClick={handleDoneClick}
disabled={isLoading}
>
Done
{translate("Done")}
</Button>
</div>
</LoginLayout>

View File

@ -1,15 +1,17 @@
import React from "react";
import { useTheme, Typography, Grid } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import ReactLoading from "react-loading";
const LoadingPage = function () {
const theme = useTheme();
const { t: translate } = useTranslation("Portal");
return (
<Grid container alignItems="center" justifyContent="center" style={{ minHeight: "100vh" }}>
<Grid item style={{ textAlign: "center", display: "inline-block" }}>
<ReactLoading width={64} height={64} color={theme.custom.loadingBar} type="bars" />
<Typography>Loading...</Typography>
<Typography>{translate("Loading")}...</Typography>
</Grid>
</Grid>
);

View File

@ -1,17 +1,19 @@
import React from "react";
import { Typography, makeStyles } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import SuccessIcon from "@components/SuccessIcon";
const Authenticated = function () {
const classes = useStyles();
const { t: translate } = useTranslation("Portal");
return (
<div id="authenticated-stage">
<div className={classes.iconContainer}>
<SuccessIcon />
</div>
<Typography>Authenticated</Typography>
<Typography>{translate("Authenticated")}</Typography>
</div>
);
};

View File

@ -1,6 +1,7 @@
import React from "react";
import { Grid, makeStyles, Button } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { LogoutRoute as SignOutRoute } from "@constants/Routes";
@ -14,17 +15,18 @@ export interface Props {
const AuthenticatedView = function (props: Props) {
const style = useStyles();
const navigate = useNavigate();
const { t: translate } = useTranslation("Portal");
const handleLogoutClick = () => {
navigate(SignOutRoute);
};
return (
<LoginLayout id="authenticated-stage" title={`Hi ${props.name}`} showBrand>
<LoginLayout id="authenticated-stage" title={`${translate("Hi")} ${props.name}`} showBrand>
<Grid container>
<Grid item xs={12}>
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
Logout
{translate("Logout")}
</Button>
</Grid>
<Grid item xs={12} className={style.mainContainer}>

View File

@ -2,6 +2,7 @@ import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { makeStyles, Grid, Button, FormControlLabel, Checkbox, Link } from "@material-ui/core";
import classnames from "classnames";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import FixedTextField from "@components/FixedTextField";
@ -37,6 +38,7 @@ const FirstFactorForm = function (props: Props) {
// TODO (PR: #806, Issue: #511) potentially refactor
const usernameRef = useRef() as MutableRefObject<HTMLInputElement>;
const passwordRef = useRef() as MutableRefObject<HTMLInputElement>;
const { t: translate } = useTranslation("Portal");
useEffect(() => {
const timeout = setTimeout(() => usernameRef.current.focus(), 10);
return () => clearTimeout(timeout);
@ -66,7 +68,7 @@ const FirstFactorForm = function (props: Props) {
props.onAuthenticationSuccess(res ? res.redirect : undefined);
} catch (err) {
console.error(err);
createErrorNotification("Incorrect username or password.");
createErrorNotification(translate("Incorrect username or password"));
props.onAuthenticationFailure();
setPassword("");
passwordRef.current.focus();
@ -78,14 +80,14 @@ const FirstFactorForm = function (props: Props) {
};
return (
<LoginLayout id="first-factor-stage" title="Sign in" showBrand>
<LoginLayout id="first-factor-stage" title={translate("Sign in")} showBrand>
<Grid container spacing={2}>
<Grid item xs={12}>
<FixedTextField
// TODO (PR: #806, Issue: #511) potentially refactor
inputRef={usernameRef}
id="username-textfield"
label="Username"
label={translate("Username")}
variant="outlined"
required
value={username}
@ -115,7 +117,7 @@ const FirstFactorForm = function (props: Props) {
// TODO (PR: #806, Issue: #511) potentially refactor
inputRef={passwordRef}
id="password-textfield"
label="Password"
label={translate("Password")}
variant="outlined"
required
fullWidth
@ -163,7 +165,7 @@ const FirstFactorForm = function (props: Props) {
/>
}
className={style.rememberMe}
label="Remember me"
label={translate("Remember me")}
/>
</Grid>
) : null}
@ -176,7 +178,7 @@ const FirstFactorForm = function (props: Props) {
disabled={disabled}
onClick={handleSignIn}
>
Sign in
{translate("Sign in")}
</Button>
</Grid>
{props.resetPassword ? (
@ -187,7 +189,7 @@ const FirstFactorForm = function (props: Props) {
onClick={handleResetPasswordClick}
className={style.resetLink}
>
Reset password?
{translate("Reset password?")}
</Link>
</Grid>
) : null}

View File

@ -2,6 +2,7 @@ import React, { ReactNode, Fragment } from "react";
import { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
import classnames from "classnames";
import { useTranslation } from "react-i18next";
import InformationIcon from "@components/InformationIcon";
import Authenticated from "@views/LoginPortal/Authenticated";
@ -27,12 +28,13 @@ export interface Props {
const DefaultMethodContainer = function (props: Props) {
const style = useStyles();
const { t: translate } = useTranslation("Portal");
const registerMessage = props.registered
? props.title === "Push Notification"
? ""
: "Lost your device?"
: "Register device";
const selectMessage = "Select a Device";
: translate("Lost your device?")
: translate("Register device");
const selectMessage = translate("Select a Device");
let container: ReactNode;
let stateClass: string = "";
@ -95,6 +97,7 @@ interface NotRegisteredContainerProps {
}
function NotRegisteredContainer(props: NotRegisteredContainerProps) {
const { t: translate } = useTranslation("Portal");
const theme = useTheme();
return (
<Fragment>
@ -102,14 +105,14 @@ function NotRegisteredContainer(props: NotRegisteredContainerProps) {
<InformationIcon />
</div>
<Typography style={{ color: "#5858ff" }}>
The resource you're attempting to access requires two-factor authentication.
{translate("The resource you're attempting to access requires two-factor authentication")}
</Typography>
<Typography style={{ color: "#5858ff" }}>
{props.title === "Push Notification"
? props.duoSelfEnrollment
? "Register your first device by clicking on the link below."
: "Contact your administrator to register a device."
: "Register your first device by clicking on the link below."}
? translate("Register your first device by clicking on the link below")
: translate("Contact your administrator to register a device.")
: translate("Register your first device by clicking on the link below")}
</Typography>
</Fragment>
);

View File

@ -10,6 +10,7 @@ import {
Typography,
useTheme,
} from "@material-ui/core";
import { useTranslation } from "react-i18next";
import FingerTouchIcon from "@components/FingerTouchIcon";
import PushNotificationIcon from "@components/PushNotificationIcon";
@ -28,6 +29,7 @@ export interface Props {
const MethodSelectionDialog = function (props: Props) {
const style = useStyles();
const theme = useTheme();
const { t: translate } = useTranslation("Portal");
const pieChartIcon = (
<TimerIcon width={24} height={24} period={15} color={theme.palette.primary.main} backgroundColor={"white"} />
@ -40,7 +42,7 @@ const MethodSelectionDialog = function (props: Props) {
{props.methods.has(SecondFactorMethod.TOTP) ? (
<MethodItem
id="one-time-password-option"
method="Time-based One-Time Password"
method={translate("Time-based One-Time Password")}
icon={pieChartIcon}
onClick={() => props.onClick(SecondFactorMethod.TOTP)}
/>
@ -48,7 +50,7 @@ const MethodSelectionDialog = function (props: Props) {
{props.methods.has(SecondFactorMethod.U2F) && props.u2fSupported ? (
<MethodItem
id="security-key-option"
method="Security Key - U2F"
method={translate("Security Key - U2F")}
icon={<FingerTouchIcon size={32} />}
onClick={() => props.onClick(SecondFactorMethod.U2F)}
/>
@ -56,7 +58,7 @@ const MethodSelectionDialog = function (props: Props) {
{props.methods.has(SecondFactorMethod.MobilePush) ? (
<MethodItem
id="push-notification-option"
method="Push Notification"
method={translate("Push Notification")}
icon={<PushNotificationIcon width={32} height={32} />}
onClick={() => props.onClick(SecondFactorMethod.MobilePush)}
/>

View File

@ -1,5 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration";
import { completeTOTPSignIn } from "@services/OneTimePassword";
@ -31,20 +33,20 @@ const OneTimePasswordMethod = function (props: Props) {
props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,
);
const redirectionURL = useRedirectionURL();
const { t: translate } = useTranslation("Portal");
const { onSignInSuccess, onSignInError } = props;
const onSignInErrorCallback = useRef(onSignInError).current;
const onSignInSuccessCallback = useRef(onSignInSuccess).current;
const [resp, fetch, , err] = useUserInfoTOTPConfiguration();
useEffect(() => {
if (err) {
console.error(err);
onSignInErrorCallback(new Error("Could not obtain user settings"));
onSignInErrorCallback(new Error(translate("Could not obtain user settings")));
setState(State.Failure);
}
}, [onSignInErrorCallback, err]);
}, [onSignInErrorCallback, err, translate]);
useEffect(() => {
if (props.registered && props.authenticationLevel === AuthenticationLevel.OneFactor) {
@ -105,8 +107,8 @@ const OneTimePasswordMethod = function (props: Props) {
return (
<MethodContainer
id={props.id}
title="One-Time Password"
explanation="Enter one-time password"
title={translate("One-Time Password")}
explanation={translate("Enter one-time password")}
duoSelfEnrollment={false}
registered={props.registered}
state={methodState}

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Grid, makeStyles, Button } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import { Route, Routes, useNavigate } from "react-router-dom";
import u2fApi from "u2f-api";
@ -23,7 +24,7 @@ import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswo
import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod";
import SecurityKeyMethod from "@views/LoginPortal/SecondFactor/SecurityKeyMethod";
const EMAIL_SENT_NOTIFICATION = "An email has been sent to your address to complete the process.";
const EMAIL_SENT_NOTIFICATION = "An email has been sent to your address to complete the process";
export interface Props {
authenticationLevel: AuthenticationLevel;
@ -42,6 +43,7 @@ const SecondFactorForm = function (props: Props) {
const { createInfoNotification, createErrorNotification } = useNotifications();
const [registrationInProgress, setRegistrationInProgress] = useState(false);
const [u2fSupported, setU2fSupported] = useState(false);
const { t: translate } = useTranslation("Portal");
// Check that U2F is supported.
useEffect(() => {
@ -59,10 +61,10 @@ const SecondFactorForm = function (props: Props) {
setRegistrationInProgress(true);
try {
await initiateRegistrationFunc();
createInfoNotification(EMAIL_SENT_NOTIFICATION);
createInfoNotification(translate(EMAIL_SENT_NOTIFICATION));
} catch (err) {
console.error(err);
createErrorNotification("There was a problem initiating the registration process");
createErrorNotification(translate("There was a problem initiating the registration process"));
}
setRegistrationInProgress(false);
};
@ -88,7 +90,7 @@ const SecondFactorForm = function (props: Props) {
};
return (
<LoginLayout id="second-factor-stage" title={`Hi ${props.userInfo.display_name}`} showBrand>
<LoginLayout id="second-factor-stage" title={`${translate("Hi")} ${props.userInfo.display_name}`} showBrand>
<MethodSelectionDialog
open={methodSelectionOpen}
methods={props.configuration.available_methods}
@ -99,11 +101,11 @@ const SecondFactorForm = function (props: Props) {
<Grid container>
<Grid item xs={12}>
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
Logout
{translate("Logout")}
</Button>
{" | "}
<Button color="secondary" onClick={handleMethodSelectionClick} id="methods-button">
Methods
{translate("Methods")}
</Button>
</Grid>
<Grid item xs={12} className={style.methodContainer}>

View File

@ -1,6 +1,7 @@
import React, { useEffect, useCallback, useState } from "react";
import { Typography, makeStyles } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import { Navigate } from "react-router-dom";
import { FirstFactorRoute } from "@constants/Routes";
@ -21,6 +22,7 @@ const SignOut = function (props: Props) {
const redirector = useRedirector();
const [timedOut, setTimedOut] = useState(false);
const [safeRedirect, setSafeRedirect] = useState(false);
const { t: translate } = useTranslation("Portal");
const doSignOut = useCallback(async () => {
try {
@ -36,9 +38,9 @@ const SignOut = function (props: Props) {
}, 2000);
} catch (err) {
console.error(err);
createErrorNotification("There was an issue signing out");
createErrorNotification(translate("There was an issue signing out"));
}
}, [createErrorNotification, redirectionURL, setSafeRedirect, setTimedOut, mounted]);
}, [createErrorNotification, redirectionURL, setSafeRedirect, setTimedOut, mounted, translate]);
useEffect(() => {
doSignOut();
@ -53,8 +55,8 @@ const SignOut = function (props: Props) {
}
return (
<LoginLayout title="Sign out">
<Typography className={style.typo}>You're being signed out and redirected...</Typography>
<LoginLayout title={translate("Sign out")}>
<Typography className={style.typo}>{translate("You're being signed out and redirected")}...</Typography>
</LoginLayout>
);
};

View File

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Grid, Button, makeStyles } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import FixedTextField from "@components/FixedTextField";
@ -15,6 +16,7 @@ const ResetPasswordStep1 = function () {
const [error, setError] = useState(false);
const { createInfoNotification, createErrorNotification } = useNotifications();
const navigate = useNavigate();
const { t: translate } = useTranslation("Portal");
const doInitiateResetPasswordProcess = async () => {
if (username === "") {
@ -24,9 +26,9 @@ const ResetPasswordStep1 = function () {
try {
await initiateResetPasswordProcess(username);
createInfoNotification("An email has been sent to your address to complete the process.");
createInfoNotification(translate("An email has been sent to your address to complete the process"));
} catch (err) {
createErrorNotification("There was an issue initiating the password reset process.");
createErrorNotification(translate("There was an issue initiating the password reset process"));
}
};
@ -39,12 +41,12 @@ const ResetPasswordStep1 = function () {
};
return (
<LoginLayout title="Reset password" id="reset-password-step1-stage">
<LoginLayout title={translate("Reset password")} id="reset-password-step1-stage">
<Grid container className={style.root} spacing={2}>
<Grid item xs={12}>
<FixedTextField
id="username-textfield"
label="Username"
label={translate("Username")}
variant="outlined"
fullWidth
error={error}
@ -60,7 +62,7 @@ const ResetPasswordStep1 = function () {
</Grid>
<Grid item xs={6}>
<Button id="reset-button" variant="contained" color="primary" fullWidth onClick={handleResetClick}>
Reset
{translate("Reset")}
</Button>
</Grid>
<Grid item xs={6}>
@ -71,7 +73,7 @@ const ResetPasswordStep1 = function () {
fullWidth
onClick={handleCancelClick}
>
Cancel
{translate("Cancel")}
</Button>
</Grid>
</Grid>

View File

@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect } from "react";
import { Grid, Button, makeStyles } from "@material-ui/core";
import classnames from "classnames";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
import FixedTextField from "@components/FixedTextField";
@ -20,6 +21,7 @@ const ResetPasswordStep2 = function () {
const [errorPassword1, setErrorPassword1] = useState(false);
const [errorPassword2, setErrorPassword2] = useState(false);
const { createSuccessNotification, createErrorNotification } = useNotifications();
const { t: translate } = useTranslation("Portal");
const navigate = useNavigate();
// Get the token from the query param to give it back to the API when requesting
// the secret for OTP.
@ -28,7 +30,7 @@ const ResetPasswordStep2 = function () {
const completeProcess = useCallback(async () => {
if (!processToken) {
setFormDisabled(true);
createErrorNotification("No verification token provided");
createErrorNotification(translate("No verification token provided"));
return;
}
@ -39,11 +41,11 @@ const ResetPasswordStep2 = function () {
} catch (err) {
console.error(err);
createErrorNotification(
"There was an issue completing the process. The verification token might have expired.",
translate("There was an issue completing the process. The verification token might have expired"),
);
setFormDisabled(true);
}
}, [processToken, createErrorNotification]);
}, [processToken, createErrorNotification, translate]);
useEffect(() => {
completeProcess();
@ -62,21 +64,23 @@ const ResetPasswordStep2 = function () {
if (password1 !== password2) {
setErrorPassword1(true);
setErrorPassword2(true);
createErrorNotification("Passwords do not match.");
createErrorNotification(translate("Passwords do not match"));
return;
}
try {
await resetPassword(password1);
createSuccessNotification("Password has been reset.");
createSuccessNotification(translate("Password has been reset"));
setTimeout(() => navigate(FirstFactorRoute), 1500);
setFormDisabled(true);
} catch (err) {
console.error(err);
if ((err as Error).message.includes("0000052D.")) {
createErrorNotification("Your supplied password does not meet the password policy requirements.");
createErrorNotification(
translate("Your supplied password does not meet the password policy requirements"),
);
} else {
createErrorNotification("There was an issue resetting the password.");
createErrorNotification(translate("There was an issue resetting the password"));
}
}
};
@ -86,12 +90,12 @@ const ResetPasswordStep2 = function () {
const handleCancelClick = () => navigate(FirstFactorRoute);
return (
<LoginLayout title="Enter new password" id="reset-password-step2-stage">
<LoginLayout title={translate("Enter new password")} id="reset-password-step2-stage">
<Grid container className={style.root} spacing={2}>
<Grid item xs={12}>
<FixedTextField
id="password1-textfield"
label="New password"
label={translate("New password")}
variant="outlined"
type="password"
value={password1}
@ -105,7 +109,7 @@ const ResetPasswordStep2 = function () {
<Grid item xs={12}>
<FixedTextField
id="password2-textfield"
label="Repeat new password"
label={translate("Repeat new password")}
variant="outlined"
type="password"
disabled={formDisabled}
@ -132,7 +136,7 @@ const ResetPasswordStep2 = function () {
onClick={handleResetClick}
className={style.fullWidth}
>
Reset
{translate("Reset")}
</Button>
</Grid>
<Grid item xs={6}>
@ -144,7 +148,7 @@ const ResetPasswordStep2 = function () {
onClick={handleCancelClick}
className={style.fullWidth}
>
Cancel
{translate("Cancel")}
</Button>
</Grid>
</Grid>

View File

@ -7,6 +7,7 @@
"@components/*": ["components/*"],
"@constants/*": ["constants/*"],
"@hooks/*": ["hooks/*"],
"@i18n/*": ["i18n/*"],
"@layouts/*": ["layouts/*"],
"@models/*": ["models/*"],
"@services/*": ["services/*"],