mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
refactor: Improve i18n for login
This commit is contained in:
parent
54c877a669
commit
8fbca78772
12
public/locales/en-US/common.json
Normal file
12
public/locales/en-US/common.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"email": {
|
||||||
|
"label": "Email",
|
||||||
|
"placeholder": "johndoe@example.com"
|
||||||
|
},
|
||||||
|
"2faCode": {
|
||||||
|
"label": "Code",
|
||||||
|
"placeholder": "123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
public/locales/en-US/login.json
Normal file
35
public/locales/en-US/login.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"title": "Log in",
|
||||||
|
"forms": {
|
||||||
|
"email": {
|
||||||
|
"description": "We will send you a code to log in",
|
||||||
|
"continueActionLabel": "Send code"
|
||||||
|
},
|
||||||
|
"confirmCode": {
|
||||||
|
"title": "You got mail!",
|
||||||
|
"description": "We sent you a code to your email. Enter it below to login",
|
||||||
|
"continueActionLabel": "Log in",
|
||||||
|
"allowLoginFromDifferentDevices": "Allow login from different devices",
|
||||||
|
"expiringSoonWarning": "Your code will expire in less than a minute.",
|
||||||
|
"fields": {
|
||||||
|
"code": {
|
||||||
|
"label": "Verification Code",
|
||||||
|
"errors": {
|
||||||
|
"invalidChars": "Invalid verification code"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmFromDifferentDevice": {
|
||||||
|
"title": "Login failed",
|
||||||
|
"description": "You could not be logged in. This could either be because you are not allowed to login from different devices or the verification code is invalid or expired."
|
||||||
|
},
|
||||||
|
"otp": {
|
||||||
|
"title": "Two-Factor Authentication",
|
||||||
|
"description": "Enter the code from your authenticator app",
|
||||||
|
"isUnavailable": "Your OTP verification time expired or you exceeded the maximum number of attempts. Please log in again.",
|
||||||
|
"codesLostActionLabel": "I lost my codes",
|
||||||
|
"continueActionLabel": "Log in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,50 +26,6 @@
|
|||||||
"title": "Overview",
|
"title": "Overview",
|
||||||
"description": "Not much to see here, yet."
|
"description": "Not much to see here, yet."
|
||||||
},
|
},
|
||||||
"LoginRoute": {
|
|
||||||
"forms": {
|
|
||||||
"email": {
|
|
||||||
"title": "Sign in",
|
|
||||||
"description": "We'll send you a verification code to your email.",
|
|
||||||
"continueAction": "Send Code",
|
|
||||||
"form": {
|
|
||||||
"email": {
|
|
||||||
"label": "Email",
|
|
||||||
"placeholder": "johndoe@example.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"confirmCode": {
|
|
||||||
"title": "You got mail!",
|
|
||||||
"description": "We sent you a code to your email. Enter it below to login",
|
|
||||||
"continueAction": "Log in",
|
|
||||||
"allowLoginFromDifferentDevices": "Allow login from different devices",
|
|
||||||
"expiringSoon": "Your code will expire in less than a minute.",
|
|
||||||
"form": {
|
|
||||||
"code": {
|
|
||||||
"label": "Verification Code",
|
|
||||||
"errors": {
|
|
||||||
"invalidChars": "Invalid verification code"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"confirmFromDifferentDevice": {
|
|
||||||
"title": "Login failed",
|
|
||||||
"description": "You could not be logged in. This could either be because you are not allowed to login from different devices or the verification code is invalid or expired."
|
|
||||||
},
|
|
||||||
"otp": {
|
|
||||||
"title": "Two-factor authentication",
|
|
||||||
"description": "Please enter the code from your authenticator app.",
|
|
||||||
"submit": "Log in",
|
|
||||||
"lost": "I lost my codes",
|
|
||||||
"code": {
|
|
||||||
"label": "Code"
|
|
||||||
},
|
|
||||||
"unavailable": "Your OTP verification time expired or you exceeded the maximum number of attempts. Please log in again."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SignupRoute": {
|
"SignupRoute": {
|
||||||
"forms": {
|
"forms": {
|
||||||
"email": {
|
"email": {
|
||||||
|
@ -55,7 +55,7 @@ export default function ConfirmCodeForm({
|
|||||||
}: ConfirmCodeFormProps): ReactElement {
|
}: ConfirmCodeFormProps): ReactElement {
|
||||||
const settings = useLoaderData() as ServerSettings
|
const settings = useLoaderData() as ServerSettings
|
||||||
const expirationTime = isDev ? 70 : settings.emailLoginExpirationInSeconds
|
const expirationTime = isDev ? 70 : settings.emailLoginExpirationInSeconds
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation(["login", "common"])
|
||||||
const requestDate = useMemo(() => new Date(), [])
|
const requestDate = useMemo(() => new Date(), [])
|
||||||
const [isExpiringSoon, setIsExpiringSoon] = useState<boolean>(false)
|
const [isExpiringSoon, setIsExpiringSoon] = useState<boolean>(false)
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ export default function ConfirmCodeForm({
|
|||||||
.max(settings.emailLoginTokenLength)
|
.max(settings.emailLoginTokenLength)
|
||||||
.test(
|
.test(
|
||||||
"chars",
|
"chars",
|
||||||
t("routes.LoginRoute.forms.confirmCode.form.code.errors.invalidChars") as string,
|
t("forms.confirmCode.fields.code.errors.invalidChars") as string,
|
||||||
code => {
|
code => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return false
|
return false
|
||||||
@ -78,7 +78,7 @@ export default function ConfirmCodeForm({
|
|||||||
return code.split("").every(char => chars.includes(char))
|
return code.split("").every(char => chars.includes(char))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.label(t("routes.LoginRoute.forms.confirmCode.form.code.label")),
|
.label(t("forms.confirmCode.fields.code.label")),
|
||||||
})
|
})
|
||||||
|
|
||||||
const {mutateAsync} = useMutation<
|
const {mutateAsync} = useMutation<
|
||||||
@ -165,7 +165,7 @@ export default function ConfirmCodeForm({
|
|||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h1" align="center">
|
<Typography variant="h6" component="h1" align="center">
|
||||||
{t("routes.LoginRoute.forms.confirmCode.title")}
|
{t("forms.confirmCode.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -175,7 +175,7 @@ export default function ConfirmCodeForm({
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="subtitle1" component="p" align="center">
|
<Typography variant="subtitle1" component="p" align="center">
|
||||||
{t("routes.LoginRoute.forms.confirmCode.description")}
|
{t("forms.confirmCode.description")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -196,7 +196,7 @@ export default function ConfirmCodeForm({
|
|||||||
}
|
}
|
||||||
labelPlacement="end"
|
labelPlacement="end"
|
||||||
label={t(
|
label={t(
|
||||||
"routes.LoginRoute.forms.confirmCode.allowLoginFromDifferentDevices",
|
"forms.confirmCode.allowLoginFromDifferentDevices",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -206,9 +206,7 @@ export default function ConfirmCodeForm({
|
|||||||
fullWidth
|
fullWidth
|
||||||
name="code"
|
name="code"
|
||||||
id="code"
|
id="code"
|
||||||
label={t(
|
label={t("forms.confirmCode.fields.code.label")}
|
||||||
"routes.LoginRoute.forms.confirmCode.form.code.label",
|
|
||||||
)}
|
|
||||||
value={formik.values.code}
|
value={formik.values.code}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
@ -250,7 +248,7 @@ export default function ConfirmCodeForm({
|
|||||||
type="submit"
|
type="submit"
|
||||||
startIcon={<MdChevronRight />}
|
startIcon={<MdChevronRight />}
|
||||||
>
|
>
|
||||||
{t("routes.LoginRoute.forms.confirmCode.continueAction")}
|
{t("forms.confirmCode.continueActionLabel")}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -260,7 +258,7 @@ export default function ConfirmCodeForm({
|
|||||||
</MultiStepFormElement>
|
</MultiStepFormElement>
|
||||||
<Snackbar open={isExpiringSoon}>
|
<Snackbar open={isExpiringSoon}>
|
||||||
<Alert severity="warning" variant="filled">
|
<Alert severity="warning" variant="filled">
|
||||||
{t("routes.LoginRoute.forms.confirmCode.expiringSoon")}
|
{t("forms.confirmCode.expiringSoonWarning")}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
</>
|
</>
|
||||||
|
@ -22,7 +22,7 @@ export default function ConfirmFromDifferentDevice({
|
|||||||
token,
|
token,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: ConfirmFromDifferentDeviceProps): ReactElement {
|
}: ConfirmFromDifferentDeviceProps): ReactElement {
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation(["login"])
|
||||||
const {mutate, isLoading, isError} = useMutation<ServerUser, AxiosError, void>(
|
const {mutate, isLoading, isError} = useMutation<ServerUser, AxiosError, void>(
|
||||||
() =>
|
() =>
|
||||||
verifyLoginWithEmail({
|
verifyLoginWithEmail({
|
||||||
@ -51,14 +51,12 @@ export default function ConfirmFromDifferentDevice({
|
|||||||
<Grid container spacing={2} direction="column" alignItems="center">
|
<Grid container spacing={2} direction="column" alignItems="center">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h1">
|
<Typography variant="h6" component="h1">
|
||||||
{t("routes.LoginRoute.forms.confirmFromDifferentDevice.title")}
|
{t("forms.confirmFromDifferentDevice.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
{t(
|
{t("forms.confirmFromDifferentDevice.description")}
|
||||||
"routes.LoginRoute.forms.confirmFromDifferentDevice.description",
|
|
||||||
)}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -24,7 +24,7 @@ interface Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormProps): ReactElement {
|
export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormProps): ReactElement {
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation(["login", "common"])
|
||||||
|
|
||||||
const $password = useRef<HTMLInputElement | null>(null)
|
const $password = useRef<HTMLInputElement | null>(null)
|
||||||
const schema = yup.object().shape({
|
const schema = yup.object().shape({
|
||||||
@ -32,7 +32,7 @@ export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormPro
|
|||||||
.string()
|
.string()
|
||||||
.email()
|
.email()
|
||||||
.required()
|
.required()
|
||||||
.label(t("routes.LoginRoute.forms.email.form.email.label")),
|
.label(t("fields.email.label", {ns: "common"})),
|
||||||
})
|
})
|
||||||
|
|
||||||
const {mutateAsync} = useMutation<LoginWithEmailResult, AxiosError, string>(loginWithEmail, {
|
const {mutateAsync} = useMutation<LoginWithEmailResult, AxiosError, string>(loginWithEmail, {
|
||||||
@ -63,9 +63,9 @@ export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormPro
|
|||||||
<MultiStepFormElement>
|
<MultiStepFormElement>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<SimpleForm
|
<SimpleForm
|
||||||
title={t("routes.LoginRoute.forms.email.title")}
|
title={t("title", {ns: "login"})}
|
||||||
description={t("routes.LoginRoute.forms.email.description")}
|
description={t("forms.email.description", {ns: "login"})}
|
||||||
continueActionLabel={t("routes.LoginRoute.forms.email.continueAction")}
|
continueActionLabel={t("forms.email.continueActionLabel", {ns: "login"})}
|
||||||
nonFieldError={formik.errors.detail}
|
nonFieldError={formik.errors.detail}
|
||||||
isSubmitting={formik.isSubmitting}
|
isSubmitting={formik.isSubmitting}
|
||||||
>
|
>
|
||||||
@ -77,7 +77,7 @@ export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormPro
|
|||||||
name="email"
|
name="email"
|
||||||
id="email"
|
id="email"
|
||||||
label="Email"
|
label="Email"
|
||||||
placeholder={t("routes.LoginRoute.forms.email.form.email.placeholder")}
|
placeholder={t("fields.email.placeholder", {ns: "common"})}
|
||||||
inputRef={$password}
|
inputRef={$password}
|
||||||
inputMode="email"
|
inputMode="email"
|
||||||
value={formik.values.email}
|
value={formik.values.email}
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import * as yup from "yup"
|
import * as yup from "yup"
|
||||||
import {ReactElement} from "react"
|
import {ReactElement} from "react"
|
||||||
import {useMutation} from "@tanstack/react-query"
|
|
||||||
import {ServerUser} from "~/server-types"
|
|
||||||
import {AxiosError} from "axios"
|
|
||||||
import {verifyOTP} from "~/apis"
|
|
||||||
import {useTranslation} from "react-i18next"
|
import {useTranslation} from "react-i18next"
|
||||||
import {useFormik} from "formik"
|
import {useFormik} from "formik"
|
||||||
|
import {BsPhone, BsShieldLockFill} from "react-icons/bs"
|
||||||
|
import {MdChevronRight} from "react-icons/md"
|
||||||
|
import {Link as RouterLink} from "react-router-dom"
|
||||||
|
import {AxiosError} from "axios"
|
||||||
|
|
||||||
|
import {useMutation} from "@tanstack/react-query"
|
||||||
|
import {LoadingButton} from "@mui/lab"
|
||||||
|
import {Box, Button, Grid, InputAdornment, TextField, Typography} from "@mui/material"
|
||||||
|
|
||||||
|
import {verifyOTP} from "~/apis"
|
||||||
import {parseFastAPIError} from "~/utils"
|
import {parseFastAPIError} from "~/utils"
|
||||||
import {MultiStepFormElement} from "~/components"
|
import {MultiStepFormElement} from "~/components"
|
||||||
import {Box, Button, Grid, InputAdornment, TextField, Typography} from "@mui/material"
|
|
||||||
import {BsPhone, BsShieldLockFill} from "react-icons/bs"
|
|
||||||
import {LoadingButton} from "@mui/lab"
|
|
||||||
import {MdChevronRight} from "react-icons/md"
|
|
||||||
import {useErrorSuccessSnacks} from "~/hooks"
|
import {useErrorSuccessSnacks} from "~/hooks"
|
||||||
import {Link as RouterLink} from "react-router-dom"
|
import {ServerUser} from "~/server-types"
|
||||||
|
|
||||||
interface Form {
|
interface Form {
|
||||||
code: string
|
code: string
|
||||||
@ -30,7 +32,7 @@ export default function OTPForm({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
onCodeUnavailable,
|
onCodeUnavailable,
|
||||||
}: OTPFormProps): ReactElement {
|
}: OTPFormProps): ReactElement {
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation(["login", "common"])
|
||||||
const {showError} = useErrorSuccessSnacks()
|
const {showError} = useErrorSuccessSnacks()
|
||||||
const {mutateAsync} = useMutation<ServerUser, AxiosError, string>(
|
const {mutateAsync} = useMutation<ServerUser, AxiosError, string>(
|
||||||
code =>
|
code =>
|
||||||
@ -42,7 +44,7 @@ export default function OTPForm({
|
|||||||
onSuccess: onConfirm,
|
onSuccess: onConfirm,
|
||||||
onError: error => {
|
onError: error => {
|
||||||
if (error.response?.status === 410 || error.response?.status === 404) {
|
if (error.response?.status === 410 || error.response?.status === 404) {
|
||||||
showError(t("routes.LoginRoute.forms.otp.unavailable").toString())
|
showError(t("forms.otp.isUnavailable").toString())
|
||||||
onCodeUnavailable()
|
onCodeUnavailable()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -50,7 +52,10 @@ export default function OTPForm({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const schema = yup.object().shape({
|
const schema = yup.object().shape({
|
||||||
code: yup.string().required().label(t("routes.LoginRoute.forms.otp.code.label")),
|
code: yup
|
||||||
|
.string()
|
||||||
|
.required()
|
||||||
|
.label(t("fields.2faCode.label", {ns: "common"})),
|
||||||
})
|
})
|
||||||
|
|
||||||
const formik = useFormik<Form>({
|
const formik = useFormik<Form>({
|
||||||
@ -81,7 +86,7 @@ export default function OTPForm({
|
|||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h6" component="h1" align="center">
|
<Typography variant="h6" component="h1" align="center">
|
||||||
{t("routes.LoginRoute.forms.otp.title")}
|
{t("forms.otp.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -91,7 +96,7 @@ export default function OTPForm({
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="subtitle1" component="p" align="center">
|
<Typography variant="subtitle1" component="p" align="center">
|
||||||
{t("routes.LoginRoute.forms.otp.description")}
|
{t("forms.otp.description")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -101,7 +106,8 @@ export default function OTPForm({
|
|||||||
fullWidth
|
fullWidth
|
||||||
name="code"
|
name="code"
|
||||||
id="code"
|
id="code"
|
||||||
label={t("routes.LoginRoute.forms.otp.code.label")}
|
placeholder={t("fields.2faCode.placeholder", {ns: "common"})}
|
||||||
|
label={t("fields.2faCode.label", {ns: "common"})}
|
||||||
value={formik.values.code}
|
value={formik.values.code}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
@ -125,12 +131,12 @@ export default function OTPForm({
|
|||||||
type="submit"
|
type="submit"
|
||||||
startIcon={<MdChevronRight />}
|
startIcon={<MdChevronRight />}
|
||||||
>
|
>
|
||||||
{t("routes.LoginRoute.forms.otp.submit")}
|
{t("forms.otp.continueActionLabel")}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button component={RouterLink} to="/auth/recover-2fa">
|
<Button component={RouterLink} to="/auth/recover-2fa">
|
||||||
{t("routes.LoginRoute.forms.otp.lost")}
|
{t("forms.otp.codesLostActionLabel")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user