diff --git a/package-lock.json b/package-lock.json index c50f492..d692d3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "@jcesarmobile/capacitor-ocr": "^0.3.0", "@meauxt/react-swipeable-list": "^1.0.0", "@mui/icons-material": "^5.16.13", - "@mui/joy": "^5.0.0-beta.20", + "@mui/joy": "5.0.0-beta.52", "@mui/material": "^5.15.2", "@openreplay/tracker": "^14.0.4", "@revenuecat/purchases-capacitor": "^12.0.0", diff --git a/package.json b/package.json index 28d443c..9c20283 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@jcesarmobile/capacitor-ocr": "^0.3.0", "@meauxt/react-swipeable-list": "^1.0.0", "@mui/icons-material": "^5.16.13", - "@mui/joy": "^5.0.0-beta.20", + "@mui/joy": "5.0.0-beta.52", "@mui/material": "^5.15.2", "@openreplay/tracker": "^14.0.4", "@revenuecat/purchases-capacitor": "^12.0.0", 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/public/locales/en/common.json b/public/locales/en/common.json index 1a1056e..d8dff3b 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -29,7 +29,8 @@ "filters": "Filters", "activities": "Activities", "points": "Points", - "settings": "Settings" + "settings": "Settings", + "reportBug": "Report a Bug" }, "search": { "title": "Search", @@ -114,5 +115,37 @@ "subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.", "open": "Open the issue" } + }, + "done": "Done", + "clearAll": "Clear all", + "upload": { + "quotaTitle": "Storage Quota Exceeded", + "quotaMessage": "You have exceeded your quota for uploading files.", + "tooLargeTitle": "File Too Large", + "tooLargeMessage": "The file you are trying to upload is too large.", + "deniedTitle": "Permission Denied", + "deniedMessage": "You do not have permission to upload files." + }, + "getStarted": "Get Started!", + "imageLoadFailed": "Failed to load image.", + "typeHere": "Type in here…", + "summaryOfChores": "This is a summary of your chores", + "loadingOffline": "You are offline", + "loadingOfflineSub": "This not available while offline. Please check your internet connection and try again.", + "loadingSlow": "This is taking longer than usual. There might be an issue.", + "navigateBack": "Navigate Back", + "bottomNav": "Bottom Navigation", + "profile": "Profile", + "autocompletePlaceholder": "Type here...", + "errorScreen": { + "title": "Something went wrong", + "fallback": "An unexpected error occurred. Try reloading — it usually fixes it.", + "tryAgain": "Try again", + "home": "Home", + "login": "Login", + "hideDetails": "Hide error details", + "showDetails": "Show error details", + "copyToClipboard": "Copy to clipboard", + "copied": "Error details copied to clipboard" } } diff --git a/src/components/animations/LoadingScreen.jsx b/src/components/animations/LoadingScreen.jsx index 6ee18c9..743e7e2 100644 --- a/src/components/animations/LoadingScreen.jsx +++ b/src/components/animations/LoadingScreen.jsx @@ -63,11 +63,14 @@ const LogoContainer = styled(Box)({ marginBottom: '24px', }) +import { useTranslation } from 'react-i18next' + const LoadingScreen = ({ - message = 'Loading...', + message = null, showLogo = true, size = 'lg', }) => { + const { t } = useTranslation('common') return ( @@ -82,7 +85,7 @@ const LoadingScreen = ({ mb: 1, }} > - Done + {t('done')} tick @@ -96,7 +99,7 @@ const LoadingScreen = ({ }} /> - {message} + {message ?? t('loading')} ) diff --git a/src/components/common/AppModal.jsx b/src/components/common/AppModal.jsx index 55fefc6..72c84a7 100644 --- a/src/components/common/AppModal.jsx +++ b/src/components/common/AppModal.jsx @@ -2,7 +2,6 @@ import { Close } from '@mui/icons-material' import { Box, Divider, IconButton, Modal, Sheet, Typography } from '@mui/joy' import useMediaQuery from '@mui/material/useMediaQuery' import { forwardRef, useId } from 'react' -import { Z_INDEX } from '../../constants/zIndex' const WIDTH_BY_SIZE = { sm: 400, @@ -27,6 +26,7 @@ const AppModal = forwardRef( size = 'md', fullWidth = true, isMobile: isMobileProp, + keepMounted = false, mobilePresentation = 'sheet', role = 'dialog', showCloseButton = true, @@ -63,9 +63,16 @@ const AppModal = forwardRef( onClose={handleClose} aria-labelledby={titleId} aria-describedby={descriptionId} - keepMounted + keepMounted={keepMounted} sx={{ - zIndex: Z_INDEX.MODAL_BACKDROP, + // Joy raises portaled listboxes above modals, but its selector misses + // Menu because Menu renders with role="menu". Keep the workaround in + // the modal primitive so individual menus never need a z-index. + ...(open && { + '& ~ [role="menu"]': { + '--unstable_popup-zIndex': 'calc(var(--joy-zIndex-modal) + 1)', + }, + }), display: 'flex', alignItems: isSheet ? 'flex-end' : 'center', justifyContent: 'center', @@ -85,7 +92,6 @@ const AppModal = forwardRef( aria-describedby={descriptionId} variant='outlined' sx={{ - zIndex: Z_INDEX.MODAL_CONTENT, display: 'flex', flexDirection: 'column', width: isFullscreen diff --git a/src/components/common/filter/ActiveFilterChips.jsx b/src/components/common/filter/ActiveFilterChips.jsx index 7978629..faef0df 100644 --- a/src/components/common/filter/ActiveFilterChips.jsx +++ b/src/components/common/filter/ActiveFilterChips.jsx @@ -1,6 +1,8 @@ import { Add, Close } from '@mui/icons-material' import { Box, Button, Chip, ChipDelete, Typography } from '@mui/joy' +import { useTranslation } from 'react-i18next' + const ActiveFilterChips = ({ chipSize = 'md', chipSx, @@ -18,6 +20,7 @@ const ActiveFilterChips = ({ showAddChip = false, totalCount, }) => { + const { t } = useTranslation('common') if (!chips.length) { return null } @@ -161,7 +164,7 @@ const ActiveFilterChips = ({ ...clearButtonSx, }} > - Clear all + {t('clearAll')} )} diff --git a/src/constants/settingsSections.js b/src/constants/settingsSections.js new file mode 100644 index 0000000..3d64d8c --- /dev/null +++ b/src/constants/settingsSections.js @@ -0,0 +1,34 @@ +import { + AccountCircle, + Api, + Circle, + Code, + FamilyRestroom, + Language, + Notifications, + Palette, + Person, + Security, + Settings, + Storage, + ViewSidebar, +} from '@mui/icons-material' + +// Single source of truth for the settings sections: id, icon, and access +// gating. Titles/descriptions live in locales/settings.json under +// `overview.sections.`, keyed off the same ids. +export const SETTINGS_SECTIONS = [ + { id: 'profile', icon: Person }, + { id: 'circle', icon: Circle, parentOnly: true }, + { id: 'account', icon: AccountCircle, parentOnly: true }, + { id: 'subaccounts', icon: FamilyRestroom }, + { id: 'notifications', icon: Notifications }, + { id: 'mfa', icon: Security, parentOnly: true }, + { id: 'apitokens', icon: Api, parentOnly: true }, + { id: 'storage', icon: Storage }, + { id: 'sidepanel', icon: ViewSidebar }, + { id: 'theme', icon: Palette }, + { id: 'localization', icon: Language, isBeta: true }, + { id: 'advanced', icon: Settings }, + { id: 'developer', icon: Code }, +] diff --git a/src/constants/zIndex.js b/src/constants/zIndex.js deleted file mode 100644 index 59ab5e1..0000000 --- a/src/constants/zIndex.js +++ /dev/null @@ -1,38 +0,0 @@ -// Z-index constants for consistent layering -// Lower values appear behind higher values - -export const Z_INDEX = { - // Base layer (0-99) - BASE: 0, - CARD_OVERLAY: 1, - DROPDOWN_ITEM: 2, - TOOLTIP: 3, - - // UI Components (100-999) - SAFE_AREA: 100, - CALENDAR: 110, - SMART_INPUT: 110, - AUTOCOMPLETE: 200, - - // Navigation (1000-1999) - NAVBAR: 1000, - DRAWER: 999, - - // Modals and Overlays (2000-8999) - MODAL_BACKDROP: 2000, - MODAL_CONTENT: 2001, - MODAL_CLOSE_BUTTON: 2002, - // Popups that must float above open modals (portaled to document.body) - MODAL_POPOVER: 2100, - TOAST: 3000, - - // Critical System UI (9000-9999) - LOADING_SCREEN: 9000, - ALERTS: 9500, - NETWORK_BANNER: 9600, - - // Maximum (10000+) - Reserved for absolute emergencies - EMERGENCY: 10000, -} - -export default Z_INDEX diff --git a/src/hooks/useConfirmationModal.js b/src/hooks/useConfirmationModal.js index 674ef47..516f0ca 100644 --- a/src/hooks/useConfirmationModal.js +++ b/src/hooks/useConfirmationModal.js @@ -1,14 +1,16 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' const useConfirmationModal = () => { + const { t } = useTranslation('common') const [confirmModalConfig, setConfirmModalConfig] = useState({}) const showConfirmation = ( message, title, onConfirm, - confirmText = 'Confirm', - cancelText = 'Cancel', + confirmText = t('confirm'), + cancelText = t('cancel'), color = 'primary', ) => { setConfirmModalConfig({ diff --git a/src/hooks/useFileUpload.js b/src/hooks/useFileUpload.js index ec84b7e..f4430b4 100644 --- a/src/hooks/useFileUpload.js +++ b/src/hooks/useFileUpload.js @@ -5,12 +5,14 @@ import { useUserProfile } from '../queries/UserQueries' import { useNotification } from '../service/NotificationProvider' import { apiClient } from '../utils/ApiClient' import { isPlusAccount, resolvePhotoURL } from '../utils/Helpers' +import { useTranslation } from 'react-i18next' export const useFileUpload = ({ draftId, entityId, entityType = 'chore_attachment', } = {}) => { + const { t } = useTranslation('common') const { showError } = useNotification() const { data: userProfile } = useUserProfile() @@ -58,14 +60,14 @@ export const useFileUpload = ({ if (response.status === 507) { showError({ - title: 'Storage Quota Exceeded', - message: 'You have exceeded your quota for uploading files.', + title: t('upload.quotaTitle'), + message: t('upload.quotaMessage'), }) return null } else if (response.status === 413) { showError({ - title: 'File Too Large', - message: 'The file you are trying to upload is too large.', + title: t('upload.tooLargeTitle'), + message: t('upload.tooLargeMessage'), }) return null } else if (response.status === 403 && !isPlusAccount(userProfile)) { @@ -76,8 +78,8 @@ export const useFileUpload = ({ return null } else if (response.status === 403) { showError({ - title: 'Permission Denied', - message: 'You do not have permission to upload files.', + title: t('upload.deniedTitle'), + message: t('upload.deniedMessage'), }) return null } else if (!response.ok) { @@ -105,7 +107,7 @@ export const useFileUpload = ({ return null } }, - [entityType, entityId, draftId, showError, userProfile], + [entityType, entityId, draftId, showError, userProfile, t], ) return { uploadFile, isPlus: isPlusAccount(userProfile) } diff --git a/src/i18n/config.js b/src/i18n/config.js index 5e30e52..35b4027 100644 --- a/src/i18n/config.js +++ b/src/i18n/config.js @@ -23,6 +23,7 @@ i18n }, ns: [ + 'auth' 'common', 'settings', 'chores', diff --git a/src/search/GlobalSearchContext.jsx b/src/search/GlobalSearchContext.jsx index 3ebae49..eabc6ed 100644 --- a/src/search/GlobalSearchContext.jsx +++ b/src/search/GlobalSearchContext.jsx @@ -8,6 +8,7 @@ import { useMemo, useState, } from 'react' +import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import { offlineDB } from '../utils/OfflineDB' @@ -34,6 +35,7 @@ const uniqueBy = (items, getId) => [ export const GlobalSearchProvider = ({ children }) => { const queryClient = useQueryClient() + const { t } = useTranslation('settings') const location = useLocation() const navigate = useNavigate() const isMobile = useMediaQuery('(max-width:768px)') @@ -110,6 +112,7 @@ export const GlobalSearchProvider = ({ children }) => { labels, members, isParent: isParentUser(profile), + t, choresById: new Map(chores.map(item => [String(item.id), item])), projectsById: new Map(projects.map(item => [String(item.id), item])), membersById: new Map(members.map(item => [String(item.userId), item])), @@ -127,7 +130,7 @@ export const GlobalSearchProvider = ({ children }) => { } finally { setIsLoading(false) } - }, [queryClient]) + }, [queryClient, t]) const openSearch = useCallback( (query = '') => { diff --git a/src/search/searchProviders.js b/src/search/searchProviders.js index 7b61a54..13f5288 100644 --- a/src/search/searchProviders.js +++ b/src/search/searchProviders.js @@ -1,3 +1,5 @@ +import { SETTINGS_SECTIONS } from '../constants/settingsSections' + const stripHtml = value => { if (!value) return '' if (typeof globalThis.document === 'undefined') @@ -17,26 +19,6 @@ const HISTORY_STATUS = { 6: 'rescheduled', } -const SETTINGS = [ - ['profile', 'Profile', 'Name, avatar and personal details'], - ['circle', 'Circle', 'Members and household settings', true], - ['account', 'Account', 'Subscription and account management', true], - ['subaccounts', 'Subaccounts', 'Manage child accounts'], - ['notifications', 'Notifications', 'Reminders and notification preferences'], - ['mfa', 'Multi-factor authentication', 'Secure your account', true], - ['apitokens', 'API tokens', 'Manage integrations and access tokens', true], - ['storage', 'Storage', 'Files, backups and device storage'], - ['sidepanel', 'Side panel', 'Customize navigation'], - ['theme', 'Appearance', 'Theme, dark mode and colors'], - ['localization', 'Language and region', 'Language, dates and time formats'], - [ - 'advanced', - 'Advanced settings', - 'Offline support, webhooks and application behavior', - ], - ['developer', 'Developer settings', 'Diagnostics and experimental tools'], -] - const providers = [] export const registerSearchProvider = provider => { @@ -166,15 +148,15 @@ registerSearchProvider({ registerSearchProvider({ id: 'settings', - getDocuments: ({ isParent }) => - SETTINGS.filter(([, , , parentOnly]) => !parentOnly || isParent).map( - ([id, title, description]) => + getDocuments: ({ isParent, t }) => + SETTINGS_SECTIONS.filter(({ parentOnly }) => !parentOnly || isParent).map( + ({ id }) => document('settings', { id: `setting:${id}`, entityId: id, - title, + title: t(`overview.sections.${id}.title`), subtitle: 'Settings', - body: description, + body: t(`overview.sections.${id}.description`), keywords: `preferences configuration ${id}`, route: `/settings/${id}`, }), diff --git a/src/service/AlertsProvider.jsx b/src/service/AlertsProvider.jsx index 470c36a..62c2afd 100644 --- a/src/service/AlertsProvider.jsx +++ b/src/service/AlertsProvider.jsx @@ -1,7 +1,6 @@ import { Alert, Box } from '@mui/joy' import PropTypes from 'prop-types' import { createContext, useCallback, useContext, useState } from 'react' -import Z_INDEX from '../constants/zIndex' const FADE_DURATION = 400 // ms const ALERT_DURATION = 5000 // ms @@ -48,7 +47,7 @@ export const AlertsProvider = ({ children }) => { top: 0, left: 0, width: '100%', - zIndex: Z_INDEX.ALERTS, + zIndex: 'var(--joy-zIndex-snackbar)', overflow: 'hidden', }} > 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')} diff --git a/src/views/Chores/components/ChoreToolbarPrototype.jsx b/src/views/Chores/components/ChoreToolbarPrototype.jsx index 29960b4..cd4e28d 100644 --- a/src/views/Chores/components/ChoreToolbarPrototype.jsx +++ b/src/views/Chores/components/ChoreToolbarPrototype.jsx @@ -38,16 +38,17 @@ import { ButtonGroup, Chip, Divider, + Dropdown, IconButton, Input, Menu, + MenuButton, MenuItem, Typography, } from '@mui/joy' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import AppModal from '../../../components/common/AppModal' import ActiveFilterChips from '../../../components/common/filter/ActiveFilterChips' -import { Z_INDEX } from '../../../constants/zIndex' import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint' import { FILTER_COLORS } from '../../../utils/Colors' import Priorities from '../../../utils/Priorities' @@ -229,9 +230,8 @@ const ChoreToolbar = ({ const [localSelections, setLocalSelections] = useState(defaultSelections()) const [savingFilter, setSavingFilter] = useState(false) const [saveFilterName, setSaveFilterName] = useState('') - const [saveMenuAnchorEl, setSaveMenuAnchorEl] = useState(null) + const [saveMenuOpen, setSaveMenuOpen] = useState(false) const [editingSavedFilter, setEditingSavedFilter] = useState(null) - const saveMenuRef = useRef(null) const activeConditions = selectionsToConditions(localSelections) // ── badge counts ───────────────────────────────────────────────────────────── @@ -419,13 +419,13 @@ const ChoreToolbar = ({ } setSavingFilter(false) setSaveFilterName('') - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) setFilterSheetOpen(true) } useEffect(() => { if (!filterSheetOpen || savingFilter || activeConditions.length === 0) { - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) } }, [filterSheetOpen, savingFilter, activeConditions.length]) @@ -504,7 +504,7 @@ const ChoreToolbar = ({ onFilterSaved?.(editingSavedFilter.name) }) - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) setFilterSheetOpen(false) } @@ -681,7 +681,7 @@ const ChoreToolbar = ({ open={filterSheetOpen} isMobile onClose={() => { - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) setFilterSheetOpen(false) }} maxHeight='92vh' @@ -747,7 +747,7 @@ const ChoreToolbar = ({ disabled={!hasAnyActive && activeConditions.length === 0} onClick={() => { setLocalSelections(defaultSelections()) - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) onClearAllFilters?.() setFilterSheetOpen(false) }} @@ -756,33 +756,29 @@ const ChoreToolbar = ({ {activeConditions.length > 0 ? ( - <> + setSaveMenuOpen(isOpen)} + > - setSaveMenuAnchorEl(e.currentTarget)} > - + - setSaveMenuAnchorEl(null)} - placement='top-end' - sx={{ zIndex: Z_INDEX.MODAL_CONTENT + 10 }} - > + { - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) setSaveFilterName( editingSavedFilter ? `${editingSavedFilter.name} Copy` @@ -805,13 +801,13 @@ const ChoreToolbar = ({ Save as New Filter - + ) : ( {/* Reporting is one tap from the failure, where the context is still @@ -246,7 +247,7 @@ const Error = () => { size='lg' startDecorator={} > - Home + {t('errorScreen.home')} @@ -293,7 +294,9 @@ const Error = () => { } sx={{ mb: 1 }} > - {showDetails ? 'Hide' : 'Show'} error details + {showDetails + ? t('errorScreen.hideDetails') + : t('errorScreen.showDetails')} {showDetails && ( @@ -312,7 +315,7 @@ const Error = () => { color='neutral' onClick={handleCopy} sx={{ position: 'absolute', top: 8, right: 8 }} - title='Copy to clipboard' + title={t('errorScreen.copyToClipboard')} > @@ -347,7 +350,7 @@ const Error = () => { anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} size='sm' > - Error details copied to clipboard + {t('errorScreen.copied')} ) diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 894e7a7..5600c1b 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -1,10 +1,12 @@ import { Box, Button, Container, Typography } from '@mui/joy' +import { useTranslation } from 'react-i18next' import { useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { useState } from 'react' import Logo from '../Logo' const Home = () => { + const { t } = useTranslation('common') const Navigate = useNavigate() const getCurrentUser = () => { return JSON.parse(localStorage.getItem('user')) @@ -36,7 +38,7 @@ const Home = () => { Navigate('/chores') }} > - Get Started! + {t('getStarted')} diff --git a/src/views/Modals/Inputs/AttachmentViewerModal.jsx b/src/views/Modals/Inputs/AttachmentViewerModal.jsx index c0eefc2..1da2fb7 100644 --- a/src/views/Modals/Inputs/AttachmentViewerModal.jsx +++ b/src/views/Modals/Inputs/AttachmentViewerModal.jsx @@ -1,4 +1,5 @@ import { Browser } from '@capacitor/browser' +import { useTranslation } from 'react-i18next' import { Capacitor } from '@capacitor/core' import { Download } from '@mui/icons-material' import { Box, CircularProgress, Typography } from '@mui/joy' @@ -29,6 +30,7 @@ const downloadUrl = (url, fileName) => { } function AttachmentViewerModal({ config }) { + const { t } = useTranslation('common') const { ResponsiveModal } = useResponsiveModal() const [imgLoaded, setImgLoaded] = useState(false) const [imgError, setImgError] = useState(false) @@ -49,7 +51,7 @@ function AttachmentViewerModal({ config }) { maxHeight='92vh' footer={ , @@ -73,7 +75,7 @@ function AttachmentViewerModal({ config }) { )} {imgError ? ( - Failed to load image. + {t('imageLoadFailed')} ) : ( } > diff --git a/src/views/Modals/Inputs/TextModal.jsx b/src/views/Modals/Inputs/TextModal.jsx index 1a1c8a2..1e945ef 100644 --- a/src/views/Modals/Inputs/TextModal.jsx +++ b/src/views/Modals/Inputs/TextModal.jsx @@ -2,6 +2,7 @@ import { Textarea } from '@mui/joy' import { useState } from 'react' import ModalActions from '../../../components/common/ModalActions' import { useResponsiveModal } from '../../../hooks/useResponsiveModal' +import { useTranslation } from 'react-i18next' function TextModal({ isOpen, @@ -12,6 +13,7 @@ function TextModal({ okText, cancelText, }) { + const { t } = useTranslation('common') const { ResponsiveModal } = useResponsiveModal() const [text, setText] = useState(current) @@ -35,7 +37,7 @@ function TextModal({ >