From 6dc7c3d3707ea3a78d36817a477cef5b78cea1ef Mon Sep 17 00:00:00 2001 From: everysingletear Date: Wed, 12 Aug 2026 20:03:02 +0800 Subject: [PATCH] i18n: extract Authorization screens into a new `auth` namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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'). --- public/locales/en/auth.json | 111 ++++++++++++++++++ src/i18n/config.js | 2 +- src/views/Authorization/AuthFields.jsx | 52 ++++---- src/views/Authorization/Authenticating.jsx | 44 +++---- .../Authorization/ForgotPasswordView.jsx | 24 ++-- src/views/Authorization/LoginSettings.jsx | 18 +-- src/views/Authorization/LoginView.jsx | 75 ++++++------ .../Authorization/MFAVerificationModal.jsx | 28 +++-- src/views/Authorization/Signup.jsx | 47 ++++---- .../Authorization/UpdatePasswordView.jsx | 33 +++--- 10 files changed, 283 insertions(+), 151 deletions(-) create mode 100644 public/locales/en/auth.json 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')}