diff --git a/public/locales/en/auth.json b/public/locales/en/auth.json
new file mode 100644
index 0000000..28cdec1
--- /dev/null
+++ b/public/locales/en/auth.json
@@ -0,0 +1,111 @@
+{
+ "validationError": "Validation Error",
+ "primaryUsernameRequired": "Primary username is required for sub account login",
+ "subNameRequired": "Sub account name is required for sub account login",
+ "usernameRequired": "Username is required",
+ "passwordRequired": "Password is required",
+ "loginFailed": "Login Failed",
+ "genericError": "An error occurred, please try again",
+ "providerLoginFailed": "{{provider}} Login Failed",
+ "providerLoginFailedMsg": "Couldn't log in with {{provider}}, please try again",
+ "providerLoginError": "{{provider}} Login Error",
+ "networkError": "Network error occurred, please try again",
+ "mfaFailed": "Two-Factor Authentication Failed",
+ "oauthError": "OAuth Error",
+ "oauthBrowserFailed": "Failed to open authentication browser",
+ "primaryAccount": "Primary Account",
+ "subAccount": "Sub Account",
+ "username": "Username",
+ "primaryUsernamePlaceholder": "Enter primary account username",
+ "subNamePlaceholder": "Enter sub account name",
+ "forgotPassword": "Forgot password?",
+ "or": "or",
+ "email": "Email",
+ "displayNamePlaceholder": "How others see your name",
+ "signupFailed": "Signup Failed",
+ "signupDisabled": "Signup disabled, please contact admin",
+ "usernameMinLength": "Username must be at least 4 characters",
+ "invalidEmail": "Invalid email address",
+ "passwordLength": "Password must be between 8 and 64 characters",
+ "displayNameRequired": "Display name is required",
+ "displayNameChars": "Display name can only contain letters and numbers",
+ "resetEmailSent": "Reset Email Sent",
+ "resetEmailSentMsg": "Check your email for password reset instructions",
+ "resetFailed": "Reset Failed",
+ "resetFailedMsg": "Failed to send reset email, please try again later",
+ "emailPlaceholder": "you@example.com",
+ "passwordUpdated": "Password Updated",
+ "passwordUpdatedMsg": "Your password has been updated successfully. Redirecting to login...",
+ "passwordUpdateFailed": "Password Update Failed",
+ "passwordUpdateFailedMsg": "Failed to update password, please try again later",
+ "savePassword": "Save password",
+ "server": {
+ "url": "Server URL",
+ "dnsFailed": "Hostname could not be resolved. Check the URL for typos or verify DNS.",
+ "unreachable": "Unable to reach the server. Check the URL, port, and network connection.",
+ "invalidUrl": "Invalid URL format. Include the protocol (http:// or https://) and port if needed."
+ },
+ "mfaModal": {
+ "title": "Two-factor authentication",
+ "codeRequired": "Please enter a verification code",
+ "verifyFailed": "Failed to verify code. Please try again.",
+ "backupHint": "Enter one of the backup codes you saved when setting up two-factor authentication.",
+ "codeHint": "Enter the 6-digit code from your authenticator app.",
+ "verify": "Verify & Sign In",
+ "backupLabel": "Backup code",
+ "codeLabel": "Verification code",
+ "backupPlaceholder": "Enter backup code",
+ "useAuthenticator": "Use authenticator app instead",
+ "useBackup": "Use a backup code instead",
+ "backupOnce": "Each backup code can only be used once.",
+ "invalidCode": "Invalid verification code. Please try again."
+ },
+ "appleLoginFailed": "Apple Login Failed",
+ "googleLoginFailed": "Google Login Failed",
+ "createOne": "Create one",
+ "primaryAccountUsernameLabel": "Primary account username",
+ "subAccountNameLabel": "Sub account name",
+ "serverSettings": "Server settings",
+ "useDifferentAccount": "Use a different account",
+ "yourUsername": "Your username",
+ "backToSignIn": "Back to sign in",
+ "confirmNewPassword": "Confirm new password",
+ "newPassword": "New password",
+ "reenterPassword": "Re-enter your password",
+ "requestNewLink": "Request a new link",
+ "setNewPassword": "Set a new password",
+ "linkInvalid": "This link is not valid",
+ "createAccountButton": "Create account",
+ "createYourAccount": "Create your account",
+ "displayNameLabel": "Display name",
+ "usernameHint": "lowercase letters, numbers, dot and dash",
+ "termsShort": "By creating an account you agree to our Terms of Service and Privacy",
+ "checkYourEmail": "Check your email",
+ "resetYourPassword": "Reset your password",
+ "serverChangeWarning": "Changing the server clears locally cached data on this device.",
+ "serverConnected": "Connected. Taking you to sign in...",
+ "passwordLabel": "Password",
+ "showPassword": "Show password",
+ "hidePassword": "Hide password",
+ "loginPasswordPlaceholder": "Enter your password",
+ "signupPasswordPlaceholder": "At least 8 characters",
+ "signupPasswordHelper": "Use 8 to 64 characters.",
+ "orContinueWith": "or continue with",
+ "signupSignInFailed": "Your account was created, but signing in failed. Please sign in.",
+ "authenticating": {
+ "signingIn": "Signing you in",
+ "signingInSub": "This will only take a moment.",
+ "unknownProvider": "Unknown sign-in provider",
+ "contactSupport": "Please contact support.",
+ "signInFailed": "Sign-in failed",
+ "mfaCancelled": "Two-factor authentication was cancelled.",
+ "requestNotVerified": "The sign-in request could not be verified.",
+ "tryAgain": "Please try again.",
+ "mfaSessionMissing": "The MFA session is missing. Please try again.",
+ "twoFactor": "Two-factor authentication",
+ "verifyToContinue": "Verify your login to continue.",
+ "noToken": "No valid authentication token was returned.",
+ "backToSignIn": "Back to sign in",
+ "mfaFailed": "Two-factor authentication failed. Please try again."
+ }
+}
diff --git a/src/i18n/config.js b/src/i18n/config.js
index 698fc83..a6e4593 100644
--- a/src/i18n/config.js
+++ b/src/i18n/config.js
@@ -22,7 +22,7 @@ i18n
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
- ns: ['common', 'settings', 'chores'],
+ ns: ['common', 'settings', 'chores', 'auth'],
defaultNS: 'common',
detection: {
diff --git a/src/views/Authorization/AuthFields.jsx b/src/views/Authorization/AuthFields.jsx
index be3ca93..30c308d 100644
--- a/src/views/Authorization/AuthFields.jsx
+++ b/src/views/Authorization/AuthFields.jsx
@@ -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 }) => (
)
-export const AuthDivider = ({ children = 'or' }) => (
-
-
- {children}
-
-
-)
+export const AuthDivider = ({ children }) => {
+ const { t } = useTranslation('auth')
+
+ return (
+
+
+ {children ?? t('or')}
+
+
+ )
+}
export const LegalLinks = () => (
{
+ 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')}
)}
@@ -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'))
}}
/>
diff --git a/src/views/Authorization/ForgotPasswordView.jsx b/src/views/Authorization/ForgotPasswordView.jsx
index 6bd73a9..de6e0e8 100644
--- a/src/views/Authorization/ForgotPasswordView.jsx
+++ b/src/views/Authorization/ForgotPasswordView.jsx
@@ -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 (
}
logoSize={0}
@@ -101,7 +103,7 @@ const ForgotPasswordView = () => {
sx={authButtonSx}
onClick={() => navigate('/login')}
>
- Back to sign in
+ {t('backToSignIn')}
@@ -110,7 +112,7 @@ const ForgotPasswordView = () => {
return (
}
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')}
diff --git a/src/views/Authorization/LoginSettings.jsx b/src/views/Authorization/LoginSettings.jsx
index 6cd1d3d..3087c3d 100644
--- a/src/views/Authorization/LoginSettings.jsx
+++ b/src/views/Authorization/LoginSettings.jsx
@@ -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 (
{
sx={{ display: 'flex', flexDirection: 'column' }}
>
{
startDecorator={}
sx={{ mt: 2, borderRadius: '12px' }}
>
- Connected. Taking you to sign in...
+ {t('serverConnected')}
)}
@@ -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')}
)
diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx
index 32e7a37..ab5e492 100644
--- a/src/views/Authorization/LoginView.jsx
+++ b/src/views/Authorization/LoginView.jsx
@@ -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 = () => {
Navigate('/login/settings')}
>
@@ -465,7 +467,7 @@ const LoginView = () => {
{displayName}
{getUserDisplayInfo(userProfile).userType === 'child' && (
- Sub Account
+ {t('subAccount')}
)}
@@ -486,7 +488,7 @@ const LoginView = () => {
sx={authButtonSx}
onClick={() => apiClient.handleLogout()}
>
- Use a different account
+ {t('useDifferentAccount')}
) : (
@@ -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') },
]}
/>
{loginType === 'primary' ? (
setUsername(e.target.value)}
@@ -519,20 +521,20 @@ const LoginView = () => {
) : (
<>
setParentUsername(e.target.value)}
/>
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')}
@@ -568,7 +571,7 @@ const LoginView = () => {
)}
- {hasSocialOptions && or continue with}
+ {hasSocialOptions && {t('orContinueWith')}}
{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')}
)}
diff --git a/src/views/Authorization/MFAVerificationModal.jsx b/src/views/Authorization/MFAVerificationModal.jsx
index 8261301..6bf4011 100644
--- a/src/views/Authorization/MFAVerificationModal.jsx
+++ b/src/views/Authorization/MFAVerificationModal.jsx
@@ -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={
- {isBackupCode ? 'Backup code' : 'Verification code'}
+ {isBackupCode ? t('mfaModal.backupLabel') : t('mfaModal.codeLabel')}
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')}
@@ -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')}
)}
diff --git a/src/views/Authorization/Signup.jsx b/src/views/Authorization/Signup.jsx
index 8600ce4..698d28a 100644
--- a/src/views/Authorization/Signup.jsx
+++ b/src/views/Authorization/Signup.jsx
@@ -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 (
{
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
>
{
/>
{
@@ -189,12 +191,12 @@ const SignupView = () => {
/>
{
@@ -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 = () => {
/>
- Create account
+ {t('createAccountButton')}
- By creating an account you agree to our Terms of Service and Privacy
+ {t('termsShort')}
Policy.
diff --git a/src/views/Authorization/UpdatePasswordView.jsx b/src/views/Authorization/UpdatePasswordView.jsx
index 0122e7c..bb672ec 100644
--- a/src/views/Authorization/UpdatePasswordView.jsx
+++ b/src/views/Authorization/UpdatePasswordView.jsx
@@ -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 (
}
showLogo
@@ -106,7 +107,7 @@ const UpdatePasswordView = () => {
sx={authButtonSx}
onClick={() => navigate('/forgot-password')}
>
- Request a new link
+ {t('requestNewLink')}
@@ -125,7 +126,7 @@ const UpdatePasswordView = () => {
return (
}
// 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 }}
>
{
/>
- Save password
+ {t('savePassword')}