i18n: extract Authorization screens into a new auth namespace

Every user-facing string under src/views/Authorization/ moves to i18next.
English only — no translations in this PR, so it is language-agnostic and
reviewable as a pure refactor.

Covered:
- LoginView — primary/sub-account tabs, credential form, social and
  Authentik buttons, welcome-back state, every auth error toast
- Signup — account creation form and each field-validation message
- ForgotPasswordView / UpdatePasswordView — reset flow and its toasts
- Authenticating — the OAuth landing screen, including its MFA branches
- MFAVerificationModal — code entry, backup codes, error states
- AuthFields / LoginSettings — shared field labels and server settings

`auth` is registered in src/i18n/config.js; public/locales/en/auth.json
holds the 103 keys. The Crowdin config picks up /public/locales/en/*.json
by glob, so the namespace flows into the pipeline with no change to
crowdin.yml.

No behaviour change: every t() value is the string that rendered before,
character for character — checked mechanically against this branch's base.
The e2e suite selects auth controls by visible text ('Create account',
'Username must be at least 4 characters'), so a green run is the proof.

Two fixes fell out of the extraction:
- AuthPasswordField had label='Password' as a prop default, so neither
  LoginView nor Signup passed one. A default cannot be translated at module
  scope, so the label moves to the call sites and both now pass it.
- AuthDivider defaulted children to 'or' for the same reason; it now falls
  back to t('or').
This commit is contained in:
everysingletear
2026-08-12 20:03:02 +08:00
parent 7eba12e718
commit 6dc7c3d370
10 changed files with 283 additions and 151 deletions

View File

@@ -12,6 +12,7 @@ import {
Typography,
} from '@mui/joy'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { authButtonSx, authInputSx } from './authStyles'
const labelSx = { fontSize: '0.875rem', fontWeight: 600, mb: 0.75 }
@@ -40,12 +41,13 @@ export const AuthTextField = ({ label, error, helper, sx, ...inputProps }) => (
)
export const AuthPasswordField = ({
label = 'Password',
label,
error,
helper,
sx,
...inputProps
}) => {
const { t } = useTranslation('auth')
const [visible, setVisible] = useState(false)
return (
@@ -60,7 +62,7 @@ export const AuthPasswordField = ({
color='neutral'
size='sm'
tabIndex={-1}
aria-label={visible ? 'Hide password' : 'Show password'}
aria-label={visible ? t('hidePassword') : t('showPassword')}
onClick={() => setVisible(v => !v)}
sx={{ borderRadius: '8px' }}
>
@@ -110,27 +112,31 @@ export const SocialButton = ({ icon, children, sx, ...props }) => (
</Button>
)
export const AuthDivider = ({ children = 'or' }) => (
<Box
role='separator'
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
my: 2.5,
'&::before, &::after': {
content: '""',
flex: 1,
height: '1px',
bgcolor: 'divider',
},
}}
>
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
{children}
</Typography>
</Box>
)
export const AuthDivider = ({ children }) => {
const { t } = useTranslation('auth')
return (
<Box
role='separator'
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
my: 2.5,
'&::before, &::after': {
content: '""',
flex: 1,
height: '1px',
bgcolor: 'divider',
},
}}
>
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
{children ?? t('or')}
</Typography>
</Box>
)
}
export const LegalLinks = () => (
<Typography

View File

@@ -10,16 +10,18 @@ import { apiClient } from '../../utils/ApiClient'
import { endOAuthExchange } from '../../utils/OAuthExchangeState'
import { GetUserProfile } from '../../utils/Fetcher'
import { saveTokens } from '../../utils/TokenStorage'
import { useTranslation } from 'react-i18next'
import AuthShell from './AuthShell'
import { authButtonSx } from './authStyles'
import MFAVerificationModal from './MFAVerificationModal'
const AuthenticationLoading = () => {
const { t } = useTranslation('auth')
const { refetch: refetchUserProfile } = useUserProfile()
const Navigate = useNavigate()
const hasCalledHandleOAuth2 = useRef(false)
const [message, setMessage] = useState('Signing you in')
const [subMessage, setSubMessage] = useState('This will only take a moment.')
const [message, setMessage] = useState(t('authenticating.signingIn'))
const [subMessage, setSubMessage] = useState(t('authenticating.signingInSub'))
const [status, setStatus] = useState('pending')
const [mfaModalOpen, setMfaModalOpen] = useState(false)
const [mfaSessionToken, setMfaSessionToken] = useState('')
@@ -31,8 +33,8 @@ const AuthenticationLoading = () => {
// suppress a genuine session expiry later on.
handleOAuth2().finally(endOAuthExchange)
} else if (provider !== 'oauth2') {
setMessage('Unknown sign-in provider')
setSubMessage('Please contact support.')
setMessage(t('authenticating.unknownProvider'))
setSubMessage(t('authenticating.contactSupport'))
setStatus('error')
}
return endOAuthExchange
@@ -71,8 +73,8 @@ const AuthenticationLoading = () => {
const handleMFAClose = () => {
setMfaModalOpen(false)
setMfaSessionToken('')
setMessage('Sign-in failed')
setSubMessage('Two-factor authentication was cancelled.')
setMessage(t('authenticating.signInFailed'))
setSubMessage(t('authenticating.mfaCancelled'))
setStatus('error')
}
@@ -85,8 +87,8 @@ const AuthenticationLoading = () => {
const storedState = localStorage.getItem('authState')
if (returnedState !== storedState) {
setMessage('Sign-in failed')
setSubMessage('The sign-in request could not be verified.')
setMessage(t('authenticating.signInFailed'))
setSubMessage(t('authenticating.requestNotVerified'))
setStatus('error')
return
}
@@ -112,8 +114,8 @@ const AuthenticationLoading = () => {
if (!response.ok) {
console.error('Authentication failed')
setMessage('Sign-in failed')
setSubMessage('Please try again.')
setMessage(t('authenticating.signInFailed'))
setSubMessage(t('authenticating.tryAgain'))
setStatus('error')
return
}
@@ -122,22 +124,22 @@ const AuthenticationLoading = () => {
if (data.mfaRequired) {
if (!data.sessionToken) {
setMessage('Sign-in failed')
setSubMessage('The MFA session is missing. Please try again.')
setMessage(t('authenticating.signInFailed'))
setSubMessage(t('authenticating.mfaSessionMissing'))
setStatus('error')
return
}
setMfaSessionToken(data.sessionToken)
setMfaModalOpen(true)
setMessage('Two-factor authentication')
setSubMessage('Verify your login to continue.')
setMessage(t('authenticating.twoFactor'))
setSubMessage(t('authenticating.verifyToContinue'))
return
}
if (!data.token && !data.access_token) {
setMessage('Sign-in failed')
setSubMessage('No valid authentication token was returned.')
setMessage(t('authenticating.signInFailed'))
setSubMessage(t('authenticating.noToken'))
setStatus('error')
return
}
@@ -158,8 +160,8 @@ const AuthenticationLoading = () => {
}
} catch (error) {
console.error('Authentication request failed', error)
setMessage('Sign-in failed')
setSubMessage('Please try again.')
setMessage(t('authenticating.signInFailed'))
setSubMessage(t('authenticating.tryAgain'))
setStatus('error')
}
}
@@ -188,7 +190,7 @@ const AuthenticationLoading = () => {
fullWidth
sx={authButtonSx}
>
Back to sign in
{t('authenticating.backToSignIn')}
</Button>
)}
</Box>
@@ -199,8 +201,8 @@ const AuthenticationLoading = () => {
sessionToken={mfaSessionToken}
onSuccess={handleMFASuccess}
onError={() => {
setMessage('Sign-in failed')
setSubMessage('Two-factor authentication failed. Please try again.')
setMessage(t('authenticating.signInFailed'))
setSubMessage(t('authenticating.mfaFailed'))
}}
/>
</AuthShell>

View File

@@ -1,6 +1,7 @@
import MarkEmailReadOutlined from '@mui/icons-material/MarkEmailReadOutlined'
import { Box, Button, Link, Typography } from '@mui/joy'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useNotification } from '../../service/NotificationProvider'
import { ResetPassword } from '../../utils/Fetcher'
@@ -12,6 +13,7 @@ const isInvalidEmail = email =>
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)
const ForgotPasswordView = () => {
const { t } = useTranslation('auth')
const navigate = useNavigate()
const [resetStatusOk, setResetStatusOk] = useState(null)
const [email, setEmail] = useState('')
@@ -40,21 +42,21 @@ const ForgotPasswordView = () => {
setResetStatusOk(true)
showNotification({
type: 'success',
title: 'Reset Email Sent',
message: 'Check your email for password reset instructions',
title: t('resetEmailSent'),
message: t('resetEmailSentMsg'),
})
} else {
setResetStatusOk(false)
showError({
title: 'Reset Failed',
message: 'Failed to send reset email, please try again later',
title: t('resetFailed'),
message: t('resetFailedMsg'),
})
}
} catch (error) {
setResetStatusOk(false)
showError({
title: 'Reset Failed',
message: 'Failed to send reset email, please try again later',
title: t('resetFailed'),
message: t('resetFailedMsg'),
})
} finally {
setIsSubmitting(false)
@@ -79,7 +81,7 @@ const ForgotPasswordView = () => {
if (resetStatusOk !== null) {
return (
<AuthShell
title='Check your email'
title={t('checkYourEmail')}
subtitle={`If an account exists for ${email}, we've sent instructions for resetting your password.`}
footer={<LegalLinks />}
logoSize={0}
@@ -101,7 +103,7 @@ const ForgotPasswordView = () => {
sx={authButtonSx}
onClick={() => navigate('/login')}
>
Back to sign in
{t('backToSignIn')}
</Button>
</Box>
</AuthShell>
@@ -110,7 +112,7 @@ const ForgotPasswordView = () => {
return (
<AuthShell
title='Reset your password'
title={t('resetYourPassword')}
subtitle="Enter your email and we'll send you a link to get back into your account."
footer={<LegalLinks />}
logoSize={0}
@@ -126,7 +128,7 @@ const ForgotPasswordView = () => {
name='email'
type='email'
autoComplete='email'
placeholder='you@example.com'
placeholder={t('emailPlaceholder')}
autoFocus
value={email}
error={emailError}
@@ -152,7 +154,7 @@ const ForgotPasswordView = () => {
underline='hover'
onClick={() => navigate('/login')}
>
Back to sign in
{t('backToSignIn')}
</Link>
</Typography>
</AuthShell>

View File

@@ -12,10 +12,12 @@ import { offlineDB } from '../../utils/OfflineDB'
import { AuthSubmitButton, AuthTextField } from './AuthFields'
import AuthShell from './AuthShell'
import { authButtonSx } from './authStyles'
import { useTranslation } from 'react-i18next'
const CONNECTION_TIMEOUT_MS = 8000
const LoginSettings = () => {
const { t } = useTranslation('auth')
const Navigate = useNavigate()
const { refetch: refetchResource } = useResource()
const [serverURL, setServerURL] = React.useState('')
@@ -105,7 +107,7 @@ const LoginSettings = () => {
return {
ok: false,
message:
'Hostname could not be resolved. Check the URL for typos or verify DNS.',
t('server.dnsFailed'),
}
}
return {
@@ -118,7 +120,7 @@ const LoginSettings = () => {
return {
ok: false,
message:
'Unable to reach the server. Check the URL, port, and network connection.',
t('server.unreachable'),
}
}
@@ -126,7 +128,7 @@ const LoginSettings = () => {
return {
ok: false,
message:
'Unable to reach the server. Check the URL, port, and network connection.',
t('server.unreachable'),
}
}
}
@@ -144,7 +146,7 @@ const LoginSettings = () => {
if (!isValidURL(trimmedURL)) {
setStatus('error')
setErrorMessage(
'Invalid URL format. Include the protocol (http:// or https://) and port if needed.',
t('server.invalidUrl'),
)
return
}
@@ -187,7 +189,7 @@ const LoginSettings = () => {
return (
<AuthShell
title='Server settings'
title={t('serverSettings')}
subtitle='Point the app at your own self-hosted Donetick server.'
>
<Box
@@ -196,7 +198,7 @@ const LoginSettings = () => {
sx={{ display: 'flex', flexDirection: 'column' }}
>
<AuthTextField
label='Server URL'
label={t('server.url')}
id='serverURL'
name='serverURL'
inputMode='url'
@@ -243,7 +245,7 @@ const LoginSettings = () => {
startDecorator={<CheckCircleOutlineIcon />}
sx={{ mt: 2, borderRadius: '12px' }}
>
Connected. Taking you to sign in...
{t('serverConnected')}
</Alert>
)}
@@ -290,7 +292,7 @@ const LoginSettings = () => {
level='body-xs'
sx={{ mt: 2.5, textAlign: 'center', color: 'text.secondary' }}
>
Changing the server clears locally cached data on this device.
{t('serverChangeWarning')}
</Typography>
</AuthShell>
)

View File

@@ -10,6 +10,7 @@ import { Avatar, Box, Button, IconButton, Link, Typography } from '@mui/joy'
import { useQueryClient } from '@tanstack/react-query'
import Cookies from 'js-cookie'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { LoginSocialGoogle } from 'reactjs-social-login'
@@ -84,6 +85,7 @@ const SegmentedControl = ({ value, onChange, options }) => (
)
const LoginView = () => {
const { t } = useTranslation('auth')
// Use React Query client directly to invalidate the user profile query
const queryClient = useQueryClient()
const { data: userProfile } = useUserProfile()
@@ -158,23 +160,23 @@ const LoginView = () => {
if (loginType === 'sub') {
if (!parentUsername.trim()) {
showError({
title: 'Validation Error',
message: 'Primary username is required for sub account login',
title: t('validationError'),
message: t('primaryUsernameRequired'),
})
return
}
if (!childName.trim()) {
showError({
title: 'Validation Error',
message: 'Sub account name is required for sub account login',
title: t('validationError'),
message: t('subNameRequired'),
})
return
}
} else {
if (!username.trim()) {
showError({
title: 'Validation Error',
message: 'Username is required',
title: t('validationError'),
message: t('usernameRequired'),
})
return
}
@@ -182,8 +184,8 @@ const LoginView = () => {
if (!password) {
showError({
title: 'Validation Error',
message: 'Password is required',
title: t('validationError'),
message: t('passwordRequired'),
})
return
}
@@ -200,7 +202,7 @@ const LoginView = () => {
result = await authLogin({ username: actualUsername, password })
} catch (error) {
showError({
title: 'Login Failed',
title: t('loginFailed'),
message: error?.message || 'An error occurred, please try again',
})
return
@@ -227,8 +229,8 @@ const LoginView = () => {
}
} else {
showError({
title: 'Login Failed',
message: result.error || 'An error occurred, please try again',
title: t('loginFailed'),
message: result.error || t('genericError'),
})
}
}
@@ -298,15 +300,15 @@ const LoginView = () => {
} else {
const providerName = provider === 'apple' ? 'Apple' : 'Google'
showError({
title: `${providerName} Login Failed`,
message: `Couldn't log in with ${providerName}, please try again`,
title: t('providerLoginFailed', { provider: providerName }),
message: t('providerLoginFailedMsg', { provider: providerName }),
})
}
} catch (error) {
const providerName = provider === 'apple' ? 'Apple' : 'Google'
showError({
title: `${providerName} Login Error`,
message: 'Network error occurred, please try again',
title: t('providerLoginError', { provider: providerName }),
message: t('networkError'),
})
}
}
@@ -350,7 +352,7 @@ const LoginView = () => {
const handleMFAError = errorMessage => {
showError({
title: 'Two-Factor Authentication Failed',
title: t('mfaFailed'),
message: errorMessage,
})
}
@@ -398,8 +400,8 @@ const LoginView = () => {
} catch (error) {
console.error('Failed to open OAuth browser:', error)
showError({
title: 'OAuth Error',
message: 'Failed to open authentication browser',
title: t('oauthError'),
message: t('oauthBrowserFailed'),
})
}
} else {
@@ -439,7 +441,7 @@ const LoginView = () => {
<IconButton
variant='plain'
color='neutral'
aria-label='Server settings'
aria-label={t('serverSettings')}
onClick={() => Navigate('/login/settings')}
>
<SettingsOutlined />
@@ -465,7 +467,7 @@ const LoginView = () => {
<Typography level='title-md'>{displayName}</Typography>
{getUserDisplayInfo(userProfile).userType === 'child' && (
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
Sub Account
{t('subAccount')}
</Typography>
)}
</Box>
@@ -486,7 +488,7 @@ const LoginView = () => {
sx={authButtonSx}
onClick={() => apiClient.handleLogout()}
>
Use a different account
{t('useDifferentAccount')}
</Button>
</Box>
) : (
@@ -499,19 +501,19 @@ const LoginView = () => {
value={loginType}
onChange={handleLoginModeChange}
options={[
{ value: 'primary', label: 'Primary Account' },
{ value: 'sub', label: 'Sub Account' },
{ value: 'primary', label: t('primaryAccount') },
{ value: 'sub', label: t('subAccount') },
]}
/>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{loginType === 'primary' ? (
<AuthTextField
label='Username'
label={t('username')}
id='username'
name='username'
autoComplete='username'
placeholder='Your username'
placeholder={t('yourUsername')}
autoFocus
value={username}
onChange={e => setUsername(e.target.value)}
@@ -519,20 +521,20 @@ const LoginView = () => {
) : (
<>
<AuthTextField
label='Primary account username'
label={t('primaryAccountUsernameLabel')}
id='parentUsername'
name='parentUsername'
autoComplete='username'
placeholder='Enter primary account username'
placeholder={t('primaryUsernamePlaceholder')}
autoFocus
value={parentUsername}
onChange={e => setParentUsername(e.target.value)}
/>
<AuthTextField
label='Sub account name'
label={t('subAccountNameLabel')}
id='childName'
name='childName'
placeholder='Enter sub account name'
placeholder={t('subNamePlaceholder')}
value={childName}
onChange={e => setChildName(e.target.value)}
/>
@@ -544,7 +546,8 @@ const LoginView = () => {
id='password'
name='password'
autoComplete='current-password'
placeholder='Enter your password'
label={t('passwordLabel')}
placeholder={t('loginPasswordPlaceholder')}
value={password}
onChange={e => setPassword(e.target.value)}
/>
@@ -556,7 +559,7 @@ const LoginView = () => {
underline='hover'
onClick={handleForgotPassword}
>
Forgot password?
{t('forgotPassword')}
</Link>
</Box>
</Box>
@@ -568,7 +571,7 @@ const LoginView = () => {
</Box>
)}
{hasSocialOptions && <AuthDivider>or continue with</AuthDivider>}
{hasSocialOptions && <AuthDivider>{t('orContinueWith')}</AuthDivider>}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{showSocialLogin && !Capacitor.isNativePlatform() && (
@@ -584,7 +587,7 @@ const LoginView = () => {
}}
onReject={() => {
showError({
title: 'Google Login Failed',
title: t('googleLoginFailed'),
message: "Couldn't log in with Google, please try again",
})
}}
@@ -608,7 +611,7 @@ const LoginView = () => {
} catch (error) {
console.error('Google login error:', error)
showError({
title: 'Google Login Failed',
title: t('googleLoginFailed'),
message: `Couldn't log in with Google, please try again${
error?.message ? `: ${error.message}` : ''
}`,
@@ -637,7 +640,7 @@ const LoginView = () => {
.catch(error => {
console.error('Apple login error:', error)
showError({
title: 'Apple Login Failed',
title: t('appleLoginFailed'),
message: "Couldn't log in with Apple, please try again",
})
})
@@ -670,7 +673,7 @@ const LoginView = () => {
underline='hover'
onClick={() => Navigate('/signup')}
>
Create one
{t('createOne')}
</Link>
</Typography>
)}

View File

@@ -5,6 +5,7 @@ import ModalActions from '../../components/common/ModalActions'
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
import { VerifyMFA } from '../../utils/Fetcher'
import { authInputSx } from './authStyles'
import { useTranslation } from 'react-i18next'
const MFAVerificationModal = ({
open,
@@ -13,6 +14,7 @@ const MFAVerificationModal = ({
onSuccess,
onError,
}) => {
const { t } = useTranslation('auth')
const [verificationCode, setVerificationCode] = useState('')
const [isBackupCode, setIsBackupCode] = useState(false)
const [loading, setLoading] = useState(false)
@@ -21,7 +23,7 @@ const MFAVerificationModal = ({
const handleVerify = async () => {
if (!verificationCode.trim()) {
setError('Please enter a verification code')
setError(t('mfaModal.codeRequired'))
return
}
@@ -37,14 +39,14 @@ const MFAVerificationModal = ({
} else {
const errorData = await response.json()
const message =
errorData.message || 'Invalid verification code. Please try again.'
errorData.message || t('mfaModal.invalidCode')
setError(message)
onError?.(message)
}
} catch (error) {
// A wrong code is shown inline; a failed request is escalated to the
// caller so it can surface a toast instead of looking like a bad code.
const message = 'Failed to verify code. Please try again.'
const message = t('mfaModal.verifyFailed')
setError(message)
onError?.(message)
console.error('MFA verification error:', error)
@@ -73,23 +75,23 @@ const MFAVerificationModal = ({
open={open}
onClose={handleClose}
size='md'
title='Two-factor authentication'
title={t('mfaModal.title')}
description={
isBackupCode
? 'Enter one of the backup codes you saved when setting up two-factor authentication.'
: 'Enter the 6-digit code from your authenticator app.'
? t('mfaModal.backupHint')
: t('mfaModal.codeHint')
}
closeOnBackdrop={!loading}
closeOnEscape={!loading}
footer={
<ModalActions
secondary={{
label: 'Cancel',
label: t('common:cancel'),
onClick: handleClose,
disabled: loading,
}}
primary={{
label: 'Verify & Sign In',
label: t('mfaModal.verify'),
onClick: handleVerify,
loading,
disabled: !verificationCode.trim(),
@@ -105,12 +107,12 @@ const MFAVerificationModal = ({
level='body-sm'
sx={{ display: 'block', fontWeight: 600, mb: 0.75 }}
>
{isBackupCode ? 'Backup code' : 'Verification code'}
{isBackupCode ? t('mfaModal.backupLabel') : t('mfaModal.codeLabel')}
</Typography>
<Input
id='mfa-code'
size='lg'
placeholder={isBackupCode ? 'Enter backup code' : '000000'}
placeholder={isBackupCode ? t('mfaModal.backupPlaceholder') : '000000'}
value={verificationCode}
onChange={e => setVerificationCode(e.target.value)}
onKeyDown={handleKeyDown}
@@ -157,8 +159,8 @@ const MFAVerificationModal = ({
}}
>
{isBackupCode
? 'Use authenticator app instead'
: 'Use a backup code instead'}
? t('mfaModal.useAuthenticator')
: t('mfaModal.useBackup')}
</Link>
</Box>
@@ -167,7 +169,7 @@ const MFAVerificationModal = ({
level='body-xs'
sx={{ textAlign: 'center', color: 'text.secondary' }}
>
Each backup code can only be used once.
{t('mfaModal.backupOnce')}
</Typography>
)}
</Stack>

View File

@@ -1,6 +1,7 @@
import { Box, Link, Typography } from '@mui/joy'
import { useQueryClient } from '@tanstack/react-query'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../../hooks/useAuth.jsx'
@@ -16,6 +17,7 @@ import {
import AuthShell from './AuthShell'
const SignupView = () => {
const { t } = useTranslation('auth')
const [username, setUsername] = React.useState('')
const [password, setPassword] = React.useState('')
const Navigate = useNavigate()
@@ -38,7 +40,7 @@ const SignupView = () => {
showError({
title: 'Almost there',
message:
'Your account was created, but signing in failed. Please sign in.',
t('signupSignInFailed'),
})
Navigate('/login')
return
@@ -72,36 +74,36 @@ const SignupView = () => {
let isValid = true
if (!username.trim()) {
setUsernameError('Username is required')
setUsernameError(t('usernameRequired'))
isValid = false
}
if (username.length < 4) {
setUsernameError('Username must be at least 4 characters')
setUsernameError(t('usernameMinLength'))
isValid = false
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setEmailError('Invalid email address')
setEmailError(t('invalidEmail'))
isValid = false
}
if (password.length < 8) {
setPasswordError('Password must be between 8 and 64 characters')
setPasswordError(t('passwordLength'))
isValid = false
}
if (password.length > 64) {
setPasswordError('Password must be between 8 and 64 characters')
setPasswordError(t('passwordLength'))
isValid = false
}
if (!displayName.trim()) {
setDisplayNameError('Display name is required')
setDisplayNameError(t('displayNameRequired'))
isValid = false
}
// display name should only contain letters and spaces and numbers:
if (!/^[a-zA-Z0-9 ]+$/.test(displayName)) {
setDisplayNameError('Display name can only contain letters and numbers')
setDisplayNameError(t('displayNameChars'))
isValid = false
}
@@ -127,14 +129,14 @@ const SignupView = () => {
handleLogin(username, password)
} else if (response.status === 403) {
showError({
title: 'Signup Failed',
message: 'Signup disabled, please contact admin',
title: t('signupFailed'),
message: t('signupDisabled'),
})
} else {
console.log('Signup failed')
response.json().then(res => {
showError({
title: 'Signup Failed',
title: t('signupFailed'),
message: res.error || 'An error occurred during signup',
})
})
@@ -145,7 +147,7 @@ const SignupView = () => {
return (
<AuthShell
title='Create your account'
title={t('createYourAccount')}
subtitle={
getPendingInvite()
? 'Create an account and well send your circle join request right after.'
@@ -160,11 +162,11 @@ const SignupView = () => {
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
>
<AuthTextField
label='Display name'
label={t('displayNameLabel')}
id='displayName'
name='displayName'
autoComplete='name'
placeholder='How others see your name'
placeholder={t('displayNamePlaceholder')}
autoFocus
value={displayName}
error={displayNameError}
@@ -175,11 +177,11 @@ const SignupView = () => {
/>
<AuthTextField
label='Username'
label={t('username')}
id='username'
name='username'
autoComplete='username'
placeholder='lowercase letters, numbers, dot and dash'
placeholder={t('usernameHint')}
value={username}
error={usernameError}
onChange={e => {
@@ -189,12 +191,12 @@ const SignupView = () => {
/>
<AuthTextField
label='Email'
label={t('email')}
id='email'
name='email'
type='email'
autoComplete='email'
placeholder='you@example.com'
placeholder={t('emailPlaceholder')}
value={email}
error={emailError}
onChange={e => {
@@ -207,10 +209,11 @@ const SignupView = () => {
id='password'
name='password'
autoComplete='new-password'
placeholder='At least 8 characters'
label={t('passwordLabel')}
placeholder={t('signupPasswordPlaceholder')}
value={password}
error={passwordError}
helper='Use 8 to 64 characters.'
helper={t('signupPasswordHelper')}
onChange={e => {
setPasswordError(null)
setPassword(e.target.value)
@@ -218,14 +221,14 @@ const SignupView = () => {
/>
<AuthSubmitButton loading={isSubmitting} sx={{ mt: 1 }}>
Create account
{t('createAccountButton')}
</AuthSubmitButton>
<Typography
level='body-xs'
sx={{ textAlign: 'center', color: 'text.secondary' }}
>
By creating an account you agree to our Terms of Service and Privacy
{t('termsShort')}
Policy.
</Typography>
</Box>

View File

@@ -1,5 +1,6 @@
import { Box, Button } from '@mui/joy'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { useNotification } from '../../service/NotificationProvider'
@@ -9,6 +10,7 @@ import AuthShell from './AuthShell'
import { authButtonSx } from './authStyles'
const UpdatePasswordView = () => {
const { t } = useTranslation('auth')
const navigate = useNavigate()
const [password, setPassword] = useState('')
const [passwordConfirm, setPasswordConfirm] = useState('')
@@ -67,23 +69,22 @@ const UpdatePasswordView = () => {
if (response.ok) {
showNotification({
type: 'success',
title: 'Password Updated',
message:
'Your password has been updated successfully. Redirecting to login...',
title: t('passwordUpdated'),
message: t('passwordUpdatedMsg'),
})
setTimeout(() => {
navigate('/login')
}, 3000)
} else {
showError({
title: 'Password Update Failed',
message: 'Failed to update password, please try again later',
title: t('passwordUpdateFailed'),
message: t('passwordUpdateFailedMsg'),
})
}
} catch (error) {
showError({
title: 'Password Update Failed',
message: 'Failed to update password, please try again later',
title: t('passwordUpdateFailed'),
message: t('passwordUpdateFailedMsg'),
})
} finally {
setIsSubmitting(false)
@@ -93,7 +94,7 @@ const UpdatePasswordView = () => {
if (!verificationCode) {
return (
<AuthShell
title='This link is not valid'
title={t('linkInvalid')}
subtitle='The password reset link is incomplete or has already been used. Request a new one to continue.'
footer={<LegalLinks />}
showLogo
@@ -106,7 +107,7 @@ const UpdatePasswordView = () => {
sx={authButtonSx}
onClick={() => navigate('/forgot-password')}
>
Request a new link
{t('requestNewLink')}
</Button>
<Button
fullWidth
@@ -116,7 +117,7 @@ const UpdatePasswordView = () => {
sx={authButtonSx}
onClick={() => navigate('/login')}
>
Back to sign in
{t('backToSignIn')}
</Button>
</Box>
</AuthShell>
@@ -125,7 +126,7 @@ const UpdatePasswordView = () => {
return (
<AuthShell
title='Set a new password'
title={t('setNewPassword')}
subtitle='Choose a password you have not used on this account before.'
footer={<LegalLinks />}
// Reached from an emailed link, usually in a browser: an unbranded page
@@ -138,7 +139,7 @@ const UpdatePasswordView = () => {
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
>
<AuthPasswordField
label='New password'
label={t('newPassword')}
id='password'
name='password'
autoComplete='new-password'
@@ -151,18 +152,18 @@ const UpdatePasswordView = () => {
/>
<AuthPasswordField
label='Confirm new password'
label={t('confirmNewPassword')}
id='passwordConfirm'
name='passwordConfirm'
autoComplete='new-password'
placeholder='Re-enter your password'
placeholder={t('reenterPassword')}
value={passwordConfirm}
error={passwordConfirmationError}
onChange={handlePasswordConfirmChange}
/>
<AuthSubmitButton loading={isSubmitting} sx={{ mt: 1 }}>
Save password
{t('savePassword')}
</AuthSubmitButton>
<Button
@@ -174,7 +175,7 @@ const UpdatePasswordView = () => {
sx={authButtonSx}
onClick={() => navigate('/login')}
>
Cancel
{t('common:cancel')}
</Button>
</Box>
</AuthShell>