refactor: Improve i18n for login

This commit is contained in:
Myzel394 2023-03-04 21:31:47 +01:00
parent 54c877a669
commit 8fbca78772
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
7 changed files with 88 additions and 83 deletions

View File

@ -0,0 +1,12 @@
{
"fields": {
"email": {
"label": "Email",
"placeholder": "johndoe@example.com"
},
"2faCode": {
"label": "Code",
"placeholder": "123456"
}
}
}

View 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"
}
}
}

View File

@ -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": {

View File

@ -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>
</> </>

View File

@ -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>

View File

@ -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}

View File

@ -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>