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",
"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": {
"forms": {
"email": {

View File

@ -55,7 +55,7 @@ export default function ConfirmCodeForm({
}: ConfirmCodeFormProps): ReactElement {
const settings = useLoaderData() as ServerSettings
const expirationTime = isDev ? 70 : settings.emailLoginExpirationInSeconds
const {t} = useTranslation()
const {t} = useTranslation(["login", "common"])
const requestDate = useMemo(() => new Date(), [])
const [isExpiringSoon, setIsExpiringSoon] = useState<boolean>(false)
@ -67,7 +67,7 @@ export default function ConfirmCodeForm({
.max(settings.emailLoginTokenLength)
.test(
"chars",
t("routes.LoginRoute.forms.confirmCode.form.code.errors.invalidChars") as string,
t("forms.confirmCode.fields.code.errors.invalidChars") as string,
code => {
if (!code) {
return false
@ -78,7 +78,7 @@ export default function ConfirmCodeForm({
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<
@ -165,7 +165,7 @@ export default function ConfirmCodeForm({
>
<Grid item>
<Typography variant="h6" component="h1" align="center">
{t("routes.LoginRoute.forms.confirmCode.title")}
{t("forms.confirmCode.title")}
</Typography>
</Grid>
<Grid item>
@ -175,7 +175,7 @@ export default function ConfirmCodeForm({
</Grid>
<Grid item>
<Typography variant="subtitle1" component="p" align="center">
{t("routes.LoginRoute.forms.confirmCode.description")}
{t("forms.confirmCode.description")}
</Typography>
</Grid>
<Grid item>
@ -196,7 +196,7 @@ export default function ConfirmCodeForm({
}
labelPlacement="end"
label={t(
"routes.LoginRoute.forms.confirmCode.allowLoginFromDifferentDevices",
"forms.confirmCode.allowLoginFromDifferentDevices",
)}
/>
</Grid>
@ -206,9 +206,7 @@ export default function ConfirmCodeForm({
fullWidth
name="code"
id="code"
label={t(
"routes.LoginRoute.forms.confirmCode.form.code.label",
)}
label={t("forms.confirmCode.fields.code.label")}
value={formik.values.code}
onChange={event => {
formik.setFieldValue(
@ -250,7 +248,7 @@ export default function ConfirmCodeForm({
type="submit"
startIcon={<MdChevronRight />}
>
{t("routes.LoginRoute.forms.confirmCode.continueAction")}
{t("forms.confirmCode.continueActionLabel")}
</LoadingButton>
</Grid>
</Grid>
@ -260,7 +258,7 @@ export default function ConfirmCodeForm({
</MultiStepFormElement>
<Snackbar open={isExpiringSoon}>
<Alert severity="warning" variant="filled">
{t("routes.LoginRoute.forms.confirmCode.expiringSoon")}
{t("forms.confirmCode.expiringSoonWarning")}
</Alert>
</Snackbar>
</>

View File

@ -22,7 +22,7 @@ export default function ConfirmFromDifferentDevice({
token,
onConfirm,
}: ConfirmFromDifferentDeviceProps): ReactElement {
const {t} = useTranslation()
const {t} = useTranslation(["login"])
const {mutate, isLoading, isError} = useMutation<ServerUser, AxiosError, void>(
() =>
verifyLoginWithEmail({
@ -51,14 +51,12 @@ export default function ConfirmFromDifferentDevice({
<Grid container spacing={2} direction="column" alignItems="center">
<Grid item>
<Typography variant="h6" component="h1">
{t("routes.LoginRoute.forms.confirmFromDifferentDevice.title")}
{t("forms.confirmFromDifferentDevice.title")}
</Typography>
</Grid>
<Grid item>
<Typography variant="body1">
{t(
"routes.LoginRoute.forms.confirmFromDifferentDevice.description",
)}
{t("forms.confirmFromDifferentDevice.description")}
</Typography>
</Grid>
</Grid>

View File

@ -24,7 +24,7 @@ interface Form {
}
export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormProps): ReactElement {
const {t} = useTranslation()
const {t} = useTranslation(["login", "common"])
const $password = useRef<HTMLInputElement | null>(null)
const schema = yup.object().shape({
@ -32,7 +32,7 @@ export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormPro
.string()
.email()
.required()
.label(t("routes.LoginRoute.forms.email.form.email.label")),
.label(t("fields.email.label", {ns: "common"})),
})
const {mutateAsync} = useMutation<LoginWithEmailResult, AxiosError, string>(loginWithEmail, {
@ -63,9 +63,9 @@ export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormPro
<MultiStepFormElement>
<form onSubmit={formik.handleSubmit}>
<SimpleForm
title={t("routes.LoginRoute.forms.email.title")}
description={t("routes.LoginRoute.forms.email.description")}
continueActionLabel={t("routes.LoginRoute.forms.email.continueAction")}
title={t("title", {ns: "login"})}
description={t("forms.email.description", {ns: "login"})}
continueActionLabel={t("forms.email.continueActionLabel", {ns: "login"})}
nonFieldError={formik.errors.detail}
isSubmitting={formik.isSubmitting}
>
@ -77,7 +77,7 @@ export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormPro
name="email"
id="email"
label="Email"
placeholder={t("routes.LoginRoute.forms.email.form.email.placeholder")}
placeholder={t("fields.email.placeholder", {ns: "common"})}
inputRef={$password}
inputMode="email"
value={formik.values.email}

View File

@ -1,19 +1,21 @@
import * as yup from "yup"
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 {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 {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 {Link as RouterLink} from "react-router-dom"
import {ServerUser} from "~/server-types"
interface Form {
code: string
@ -30,7 +32,7 @@ export default function OTPForm({
onConfirm,
onCodeUnavailable,
}: OTPFormProps): ReactElement {
const {t} = useTranslation()
const {t} = useTranslation(["login", "common"])
const {showError} = useErrorSuccessSnacks()
const {mutateAsync} = useMutation<ServerUser, AxiosError, string>(
code =>
@ -42,7 +44,7 @@ export default function OTPForm({
onSuccess: onConfirm,
onError: error => {
if (error.response?.status === 410 || error.response?.status === 404) {
showError(t("routes.LoginRoute.forms.otp.unavailable").toString())
showError(t("forms.otp.isUnavailable").toString())
onCodeUnavailable()
}
},
@ -50,7 +52,10 @@ export default function OTPForm({
)
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>({
@ -81,7 +86,7 @@ export default function OTPForm({
>
<Grid item>
<Typography variant="h6" component="h1" align="center">
{t("routes.LoginRoute.forms.otp.title")}
{t("forms.otp.title")}
</Typography>
</Grid>
<Grid item>
@ -91,7 +96,7 @@ export default function OTPForm({
</Grid>
<Grid item>
<Typography variant="subtitle1" component="p" align="center">
{t("routes.LoginRoute.forms.otp.description")}
{t("forms.otp.description")}
</Typography>
</Grid>
<Grid item>
@ -101,7 +106,8 @@ export default function OTPForm({
fullWidth
name="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}
onChange={formik.handleChange}
disabled={formik.isSubmitting}
@ -125,12 +131,12 @@ export default function OTPForm({
type="submit"
startIcon={<MdChevronRight />}
>
{t("routes.LoginRoute.forms.otp.submit")}
{t("forms.otp.continueActionLabel")}
</LoadingButton>
</Grid>
<Grid item>
<Button component={RouterLink} to="/auth/recover-2fa">
{t("routes.LoginRoute.forms.otp.lost")}
{t("forms.otp.codesLostActionLabel")}
</Button>
</Grid>
</Grid>