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>
pull/2820/head
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:

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(
"You must open the link from the same device and browser that initiated the registration process",
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/*"],