diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 54b7274..43698c4 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -490,5 +490,78 @@ "notConnected": "Real-time updates are enabled but not currently connected.", "basicPlanNotice": "Real-time updates are not available in the Basic plan. Upgrade to Plus to receive instant notifications when you or other circle members complete, skip, or modify tasks." } + }, + "subscription": { + "sessionFailed": "Failed to create subscription session", + "errorTitle": "Subscription Error", + "errorMessage": "Failed to start subscription process. Please try again.", + "subscribe": "Subscribe", + "cancelAnytime": "Cancel anytime. No hidden fees. Secure payment powered by Stripe." + }, + "childUsers": { + "errNameRequired": "Sub account name is required", + "errNameMin": "Sub account name must be at least 2 characters", + "errNameMax": "Sub account name must be less than 20 characters", + "errNameChars": "Sub account name can only contain lowercase letters, dot and dash", + "errPasswordRequired": "Password is required", + "errPasswordLength": "Password must be between 8 and 64 characters", + "errPasswordMatch": "Passwords do not match", + "errDisplayNameMax": "Display name must be less than 50 characters", + "createTitle": "Create Sub Account", + "createButton": "Create Account", + "nameLabel": "Sub Account Name *", + "namePlaceholder": "Enter sub account name (e.g., sarah)", + "displayNameLabel": "Display Name", + "displayNamePlaceholder": "Display name (optional, defaults to sub account name)", + "passwordLabel": "Password *", + "passwordPlaceholder": "Enter password (8-64 characters)", + "confirmPasswordLabel": "Confirm Password *", + "confirmPasswordPlaceholder": "Confirm password" + }, + "passwordChange": { + "minLength": "Password must be at least 8 characters", + "maxLength": "Password must be less than 64 characters" + }, + "payment": { + "cancelledTitle": "Payment has been cancelled", + "cancelledRedirect": "You will be redirected to the main page shortly.", + "successTitle": "Payment Successful!", + "successRedirect": "You will be redirected to the settings page shortly." + }, + "developer": { + "tokenRefreshed": "Token refreshed successfully", + "endpointCalled": "Refresh token endpoint called successfully", + "clearDbCleared": "Local offline database cleared. Starting full sync...", + "clearDbDone": "Full sync completed from the beginning", + "intro": "View technical information about your authentication tokens and session", + "refreshCookieNote": "Refresh tokens are managed via HTTP-only cookies on web platform", + "noReconnectInfo": "No reconnection information available", + "noTimeoutInfo": "No timeout information available", + "noDebugInfo": "No debug information available" + }, + "backup": { + "keyRequired": "Encryption key is required", + "created": "Backup created and downloaded successfully", + "createFailed": "Failed to create backup", + "selectFile": "Please select a backup file", + "restored": "Backup restored successfully. Please refresh the page.", + "restoreFailed": "Failed to restore backup", + "readFailed": "Failed to read backup file", + "createIntro": "Create an encrypted backup of your data. This backup will include all your chores, history, settings, and optionally your uploaded files.", + "keyPlaceholder": "Enter a strong encryption key", + "includeAssets": "Include uploaded files and assets", + "warningLabel": "Warning:", + "restoreWarning": "Restoring a backup will replace all your current data. This action cannot be undone.", + "restoreKeyPlaceholder": "Enter the encryption key used for this backup" + }, + "deletion": { + "passwordRequired": "Please enter your password to continue", + "passwordAndDelete": "Please enter your password and type DELETE to confirm", + "failed": "Failed to delete account", + "passwordPlaceholder": "Enter your password", + "transferIntro": "You own circles that require ownership transfer before deletion. Please select new owners:", + "selectOwner": "Select new owner", + "confirmPrompt": "Please enter your password and type DELETE to confirm", + "typeDelete": "DELETE" } } diff --git a/src/components/SubscriptionModal.jsx b/src/components/SubscriptionModal.jsx index fcd95b5..e5fe314 100644 --- a/src/components/SubscriptionModal.jsx +++ b/src/components/SubscriptionModal.jsx @@ -5,8 +5,10 @@ import AppModal from './common/AppModal' import ModalActions from './common/ModalActions' import { useNotification } from '../service/NotificationProvider' import { GetSubscriptionSession } from '../utils/Fetcher' +import { useTranslation } from 'react-i18next' const SubscriptionModal = ({ open, onClose }) => { + const { t } = useTranslation('settings') const [selectedPlan, setSelectedPlan] = useState('yearly') const [isLoading, setIsLoading] = useState(false) const { showError } = useNotification() @@ -45,7 +47,7 @@ const SubscriptionModal = ({ open, onClose }) => { const response = await GetSubscriptionSession() if (!response.ok) { - throw new Error('Failed to create subscription session') + throw new Error(t('subscription.sessionFailed')) } const data = await response.json() @@ -59,8 +61,8 @@ const SubscriptionModal = ({ open, onClose }) => { } catch (error) { console.error('Subscription error:', error) showError({ - title: 'Subscription Error', - message: 'Failed to start subscription process. Please try again.', + title: t('subscription.errorTitle'), + message: t('subscription.errorMessage'), }) } finally { setIsLoading(false) @@ -71,7 +73,7 @@ const SubscriptionModal = ({ open, onClose }) => { { footer={ { color='neutral' sx={{ textAlign: 'center', mt: 3 }} > - Cancel anytime. No hidden fees. Secure payment powered by Stripe. + {t('subscription.cancelAnytime')} ) diff --git a/src/views/Modals/Inputs/BackupRestoreModal.jsx b/src/views/Modals/Inputs/BackupRestoreModal.jsx index 8fb4795..4c5ca52 100644 --- a/src/views/Modals/Inputs/BackupRestoreModal.jsx +++ b/src/views/Modals/Inputs/BackupRestoreModal.jsx @@ -11,12 +11,14 @@ import { Tabs, Typography, } from '@mui/joy' +import { useTranslation } from 'react-i18next' import { useCallback, useEffect, useRef, useState } from 'react' import ModalActions from '../../../components/common/ModalActions' import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { CreateBackup, RestoreBackup } from '../../../utils/Fetcher' function BackupRestoreModal({ isOpen, onClose, showNotification }) { + const { t } = useTranslation('settings') const { ResponsiveModal } = useResponsiveModal() const [activeTab, setActiveTab] = useState(0) @@ -66,7 +68,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { const handleCreateBackup = async () => { if (!encryptionKey.trim()) { - setError('Encryption key is required') + setError(t('backup.keyRequired')) return } @@ -95,7 +97,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { showNotification({ type: 'success', - message: 'Backup created and downloaded successfully', + message: t('backup.created'), }) handleClose() @@ -104,7 +106,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { setError(errorData.message || 'Failed to create backup') } } catch (err) { - setError('Failed to create backup') + setError(t('backup.createFailed')) } finally { setLoading(false) } @@ -120,12 +122,12 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { const handleRestore = async () => { if (!restoreEncryptionKey.trim()) { - setError('Encryption key is required') + setError(t('backup.keyRequired')) return } if (!backupFile) { - setError('Please select a backup file') + setError(t('backup.selectFile')) return } @@ -142,7 +144,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { if (response.ok) { showNotification({ type: 'success', - message: 'Backup restored successfully. Please refresh the page.', + message: t('backup.restored'), }) // Refresh the page after a short delay to allow user to see the message @@ -156,20 +158,20 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { setError(errorData.message || 'Failed to restore backup') } } catch (err) { - setError('Failed to restore backup') + setError(t('backup.restoreFailed')) } finally { setLoading(false) } } reader.onerror = () => { - setError('Failed to read backup file') + setError(t('backup.readFailed')) setLoading(false) } reader.readAsText(backupFile) } catch (err) { - setError('Failed to restore backup') + setError(t('backup.restoreFailed')) setLoading(false) } } @@ -198,8 +200,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { const renderBackupTab = () => ( - Create an encrypted backup of your data. This backup will include all - your chores, history, settings, and optionally your uploaded files. + {t('backup.createIntro')} @@ -208,7 +209,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { type='password' value={encryptionKey} onChange={e => setEncryptionKey(e.target.value)} - placeholder='Enter a strong encryption key' + placeholder={t('backup.keyPlaceholder')} /> Keep this key safe—you'll need it to restore your backup @@ -228,7 +229,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { setIncludeAssets(e.target.checked)} - label='Include uploaded files and assets' + label={t('backup.includeAssets')} /> @@ -243,8 +244,8 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { const renderRestoreTab = () => ( - Warning: Restoring a backup will replace all your - current data. This action cannot be undone. + {t('backup.warningLabel')}{' '} + {t('backup.restoreWarning')} @@ -268,7 +269,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { type='password' value={restoreEncryptionKey} onChange={e => setRestoreEncryptionKey(e.target.value)} - placeholder='Enter the encryption key used for this backup' + placeholder={t('backup.restoreKeyPlaceholder')} /> @@ -293,7 +294,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { footer={ 20) { - newErrors.childName = 'Sub account name must be less than 20 characters' + newErrors.childName = t('childUsers.errNameMax') } else if (!/^[a-z.-]+$/.test(childName)) { - newErrors.childName = - 'Sub account name can only contain lowercase letters, dot and dash' + newErrors.childName = t('childUsers.errNameChars') } } if (touched.password) { if (!password) { - newErrors.password = 'Password is required' + newErrors.password = t('childUsers.errPasswordRequired') } else if (password.length < 8) { - newErrors.password = 'Password must be between 8 and 64 characters' + newErrors.password = t('childUsers.errPasswordLength') } else if (password.length > 64) { - newErrors.password = 'Password must be between 8 and 64 characters' + newErrors.password = t('childUsers.errPasswordLength') } } if (touched.confirmPassword) { if (password !== confirmPassword) { - newErrors.confirmPassword = 'Passwords do not match' + newErrors.confirmPassword = t('childUsers.errPasswordMatch') } } if (touched.displayName && displayName.length > 50) { - newErrors.displayName = 'Display name must be less than 50 characters' + newErrors.displayName = t('childUsers.errDisplayNameMax') } setErrors(newErrors) - }, [childName, displayName, password, confirmPassword, touched]) + }, [childName, displayName, password, confirmPassword, touched, t]) const handleSubmit = async () => { setTouched({ @@ -101,7 +102,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) { - Sub Account Name * + {t('childUsers.nameLabel')} { setChildName(e.target.value) @@ -145,13 +146,13 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) { - Display Name + {t('childUsers.displayNameLabel')} { setDisplayName(e.target.value) @@ -165,7 +166,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) { - Password * + {t('childUsers.passwordLabel')} { setPassword(e.target.value) @@ -185,7 +186,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) { - Confirm Password * + {t('childUsers.confirmPasswordLabel')} { setConfirmPassword(e.target.value) diff --git a/src/views/Modals/Inputs/PasswordChangeModal.jsx b/src/views/Modals/Inputs/PasswordChangeModal.jsx index 485f083..f1b34d8 100644 --- a/src/views/Modals/Inputs/PasswordChangeModal.jsx +++ b/src/views/Modals/Inputs/PasswordChangeModal.jsx @@ -2,8 +2,10 @@ import { FormControl, FormHelperText, Input, Typography } from '@mui/joy' import { useEffect, useState } from 'react' import ModalActions from '../../../components/common/ModalActions' import { useResponsiveModal } from '../../../hooks/useResponsiveModal' +import { useTranslation } from 'react-i18next' function PasswordChangeModal({ isOpen, onClose }) { + const { t } = useTranslation('settings') const { ResponsiveModal } = useResponsiveModal() const [password, setPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') @@ -17,13 +19,13 @@ function PasswordChangeModal({ isOpen, onClose }) { if (password !== confirmPassword) { setPasswordError('Passwords do not match') } else if (password.length < 8) { - setPasswordError('Password must be at least 8 characters') + setPasswordError(t('passwordChange.minLength')) } else if (password.length > 64) { - setPasswordError('Password must be less than 64 characters') + setPasswordError(t('passwordChange.maxLength')) } else { setPasswordError(null) } - }, [password, confirmPassword, passwordTouched, confirmPasswordTouched]) + }, [password, confirmPassword, passwordTouched, confirmPasswordTouched, t]) const handleAction = isConfirmed => onClose(isConfirmed ? password : null) const canSubmit = @@ -38,13 +40,13 @@ function PasswordChangeModal({ isOpen, onClose }) { open={isOpen} onClose={() => handleAction(false)} size='sm' - title='Change Password' + title={t('accountSettings.changePassword')} description='Choose a password between 8 and 64 characters.' footer={ handleAction(false) }} + secondary={{ label: t('accountSettings.cancel'), onClick: () => handleAction(false) }} primary={{ - label: 'Change Password', + label: t('accountSettings.changePassword'), disabled: !canSubmit, onClick: () => handleAction(true), }} diff --git a/src/views/Modals/Inputs/UserDeletionModal.jsx b/src/views/Modals/Inputs/UserDeletionModal.jsx index 6991650..65c6b1a 100644 --- a/src/views/Modals/Inputs/UserDeletionModal.jsx +++ b/src/views/Modals/Inputs/UserDeletionModal.jsx @@ -14,8 +14,10 @@ import ModalActions from '../../../components/common/ModalActions' import { useNavigate } from 'react-router-dom' import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { CheckUserDeletion, DeleteUser } from '../../../utils/Fetcher' +import { useTranslation } from 'react-i18next' function UserDeletionModal({ isOpen, onClose }) { + const { t } = useTranslation('settings') const { ResponsiveModal } = useResponsiveModal() const Navigate = useNavigate() const [step, setStep] = useState(1) // 1: Warning, 2: Transfer, 3: Confirm @@ -47,7 +49,7 @@ function UserDeletionModal({ isOpen, onClose }) { const checkDeletionRequirements = async () => { if (password.trim() === '') { - setError('Please enter your password to continue') + setError(t('deletion.passwordRequired')) return } @@ -97,7 +99,7 @@ function UserDeletionModal({ isOpen, onClose }) { const executeUserDeletion = async () => { if (password.trim() === '' || confirmation !== 'DELETE') { - setError('Please enter your password and type DELETE to confirm') + setError(t('deletion.passwordAndDelete')) return } @@ -120,7 +122,7 @@ function UserDeletionModal({ isOpen, onClose }) { } } catch (err) { console.error('Failed to delete account:', err) - setError('Failed to delete account') + setError(t('deletion.failed')) } finally { setLoading(false) } @@ -181,7 +183,7 @@ function UserDeletionModal({ isOpen, onClose }) { type='password' value={password} onChange={e => setPassword(e.target.value)} - placeholder='Enter your password' + placeholder={t('deletion.passwordPlaceholder')} /> @@ -196,7 +198,7 @@ function UserDeletionModal({ isOpen, onClose }) { const renderTransferStep = () => ( <> - You own circles that require ownership transfer before deletion. Please + {t('deletion.transferIntro')} select new owners: @@ -208,7 +210,7 @@ function UserDeletionModal({ isOpen, onClose }) { New Owner setConfirmation(e.target.value)} - placeholder='DELETE' + placeholder={t('deletion.typeDelete')} /> @@ -304,7 +305,7 @@ function UserDeletionModal({ isOpen, onClose }) { handleClose(false), }} primary={{ diff --git a/src/views/Payments/PaymentFailView.jsx b/src/views/Payments/PaymentFailView.jsx index 68ee7a8..1967156 100644 --- a/src/views/Payments/PaymentFailView.jsx +++ b/src/views/Payments/PaymentFailView.jsx @@ -1,9 +1,11 @@ import { Box, Container, Sheet, Typography } from '@mui/joy' +import { useTranslation } from 'react-i18next' import { useEffect } from 'react' import { useNavigate } from 'react-router-dom' import Logo from '../../Logo' const PaymentCancelledView = () => { + const { t } = useTranslation('settings') const navigate = useNavigate() useEffect(() => { @@ -37,10 +39,10 @@ const PaymentCancelledView = () => { > - Payment has been cancelled + {t('payment.cancelledTitle')} - You will be redirected to the main page shortly. + {t('payment.cancelledRedirect')} diff --git a/src/views/Payments/PaymentSuccessView.jsx b/src/views/Payments/PaymentSuccessView.jsx index b2fbb50..3cd5345 100644 --- a/src/views/Payments/PaymentSuccessView.jsx +++ b/src/views/Payments/PaymentSuccessView.jsx @@ -1,9 +1,11 @@ import { Box, Container, Sheet, Typography } from '@mui/joy' +import { useTranslation } from 'react-i18next' import { useEffect } from 'react' import { useNavigate } from 'react-router-dom' import Logo from '../../Logo' const PaymentSuccessView = () => { + const { t } = useTranslation('settings') const navigate = useNavigate() useEffect(() => { @@ -37,10 +39,10 @@ const PaymentSuccessView = () => { > - Payment Successful! + {t('payment.successTitle')} - You will be redirected to the settings page shortly. + {t('payment.successRedirect')} diff --git a/src/views/Settings/DeveloperSettings.jsx b/src/views/Settings/DeveloperSettings.jsx index 8f5d177..31f41b7 100644 --- a/src/views/Settings/DeveloperSettings.jsx +++ b/src/views/Settings/DeveloperSettings.jsx @@ -24,8 +24,10 @@ import { syncEngine } from '../../utils/SyncEngine' import { getRefreshTokenExpiry, isNative } from '../../utils/TokenStorage' import FeedbackModal from '../Modals/FeedbackModal' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' +import { useTranslation } from 'react-i18next' const DeveloperSettings = () => { + const { t } = useTranslation('settings') const queryClient = useQueryClient() const { confirmModalConfig, showConfirmation } = useConfirmationModal() const { data: userProfile } = useUserProfile() @@ -236,7 +238,7 @@ const DeveloperSettings = () => { if (result.success) { showNotification({ type: 'success', - message: 'Token refreshed successfully', + message: t('developer.tokenRefreshed'), }) // Reload token expiry data @@ -272,7 +274,7 @@ const DeveloperSettings = () => { const data = await response.json() showNotification({ type: 'success', - message: 'Refresh token endpoint called successfully', + message: t('developer.endpointCalled'), }) // Reload token expiry data @@ -341,7 +343,7 @@ const DeveloperSettings = () => { showNotification({ type: 'success', - message: 'Local offline database cleared. Starting full sync...', + message: t('developer.clearDbCleared'), }) const didSync = await syncEngine.sync() @@ -349,7 +351,7 @@ const DeveloperSettings = () => { await queryClient.invalidateQueries() showNotification({ type: 'success', - message: 'Full sync completed from the beginning', + message: t('developer.clearDbDone'), }) } else { showNotification({ @@ -437,7 +439,7 @@ const DeveloperSettings = () => { Developer Settings - View technical information about your authentication tokens and session + {t('developer.intro')} state. This information is useful for debugging and development purposes. @@ -535,7 +537,7 @@ const DeveloperSettings = () => { ) : ( - Refresh tokens are managed via HTTP-only cookies on web platform + {t('developer.refreshCookieNote')} )} @@ -1110,7 +1112,7 @@ const DeveloperSettings = () => { ) : ( - No reconnection information available + {t('developer.noReconnectInfo')} )} @@ -1150,7 +1152,7 @@ const DeveloperSettings = () => { ) : ( - No timeout information available + {t('developer.noTimeoutInfo')} )} @@ -1195,7 +1197,7 @@ const DeveloperSettings = () => { ) : ( - No debug information available + {t('developer.noDebugInfo')} )}