diff --git a/public/locales/en/chores.json b/public/locales/en/chores.json index f929aac..892905d 100644 --- a/public/locales/en/chores.json +++ b/public/locales/en/chores.json @@ -420,5 +420,138 @@ "dueAlert": "Due Alert", "followUp": "Follow-up", "rememberFuture": "Remember for Future Tasks" + }, + "choreEdit": { + "errNameRequired": "Name is required", + "errFrequencyInvalid": "Invalid frequency, the {{unit}} should be > 0", + "errSelectDayOfWeek": "Please select at least one day of the week", + "errSelectDayOccurrence": "Please select at least one day occurrence for the month", + "errSelectMonth": "Please select at least one month", + "errStartDateRequired": "Start date is required", + "errDueDateRequired": "Due date is required", + "errThingTrigger": "Thing trigger is invalid", + "errTitle": "Please resolve the following errors:", + "savedOfflineTitle": "Saved Offline", + "savedOfflineMessage": "Your changes will sync when you are back online.", + "savedTitle": "Chore Saved", + "savedMessage": "Your task has been saved successfully!", + "saveFailedTitle": "Save Failed", + "saveFailedMessage": "Failed to save chore, please try again.", + "uploadFailedTitle": "Upload Failed", + "uploadFailedMessage": "Failed to upload attachment.", + "scanFailedTitle": "Scan Failed", + "scanFailedMessage": "Could not scan the document.", + "scanReadFailedMessage": "Could not read the scanned image.", + "deleteFailedTitle": "Delete Failed", + "deleteAttachmentFailed": "Failed to delete attachment.", + "deleteChoreFailed": "Failed to delete chore: {{error}}", + "deleteChoreTitle": "Delete Chore", + "name": "Name", + "nameDesc": "What is the name of this task?", + "description": "Description", + "descriptionDesc": "What is this task about?", + "priority": "Priority", + "priorityDesc": "How important is this task?", + "noPriority": "No Priority", + "project": "Project", + "projectDesc": "Which project does this task belong to?", + "defaultProject": "Default Project", + "labels": "Labels", + "labelsDesc": "Things to remember about this task or to tag it", + "addNewLabel": "Add New Label", + "subTasks": "Sub Tasks", + "attachments": "Attachments", + "attachmentsDesc": "Files attached to this task", + "uploadFile": "Upload File", + "scan": "Scan", + "assignees": "Assignees", + "assigneesDesc": "Who can do this task?", + "anyone": "Anyone", + "rememberFuture": "Remember for Future Tasks", + "currentlyAssigned": "Currently Assigned To", + "currentlyAssignedDesc": "Who is assigned the next due?", + "noAssigneesPlaceholder": "No Assignees yet can perform this task", + "selectAssigneePlaceholder": "Select an assignee for this task", + "assignStrategy": "Assignment Strategy", + "assignStrategyDesc": "How to pick the next assignee for the following task?", + "strategy": { + "random": "Random", + "least_assigned": "Least Assigned", + "least_completed": "Least Completed", + "keep_last_assigned": "Keep Last Assigned", + "random_except_last_assigned": "Random Except Last Assigned", + "round_robin": "Round Robin", + "no_assignee": "No Assignee" + }, + "startDate": "Start Date", + "dueDate": "Due Date", + "triggerDueHint": "Due Date will be set when the trigger of the thing is met", + "giveDueDate": "Give this task a due date", + "giveDueDateHelp": "Task needs to be completed by a specific time", + "startWhen": "When does this task start?", + "dueWhen": "When is the next first time this task is due?", + "setSpecificTime": "Set a specific time", + "dueAtSpecifiedTime": "Task will be due at the specified time", + "dueEndOfDay": "Task will be due at the end of the day (11:59 PM)", + "timeLabel": "Time:", + "taskWindow": "Task Window", + "taskWindowDesc": "Define when this task can be completed and when it expires", + "earliestCompletion": "Set earliest completion time", + "earliestCompletionHelp": "Task becomes available to complete X hours before the due date", + "hoursLabel": "Hours:", + "hoursPlaceholder": "Hours", + "afterDueDate": "after due date", + "schedulingPrefs": "Scheduling Preferences", + "schedulingPrefsDesc": "How to reschedule the next due date?", + "rescheduleFromDue": "Reschedule from due date", + "rescheduleFromDueHelp": "the next task will be scheduled from the original due date, even if the previous task was completed late", + "rescheduleFromCompletion": "Reschedule from completion date", + "rescheduleFromCompletionHelp": "the next task will be scheduled from the actual completion date of the previous task", + "notifications": "Notifications", + "notificationsPlanWarning": "Task notifications are not available in the Basic plan. Upgrade to Plus to receive reminders when tasks are due or completed.", + "notifyForTask": "Notify for this task", + "notifyForTaskHelp": "When should receive notifications for this task", + "notificationSchedule": "Notification Schedule", + "whoToNotify": "Who to Notify", + "allAssignees": "All Assignees", + "allAssigneesHelp": "Notify all assignees", + "specificGroup": "Specific Group", + "specificGroupHelp": "Notify a specific group", + "telegramGroupIdLabel": "Telegram Group ID:", + "telegramGroupIdPlaceholder": "Telegram Group ID", + "taskSettings": "Task Settings:", + "pointsSystem": "Points System", + "assignPoints": "Assign points for completion", + "assignPointsHelp": "Assign points to this task and user will earn points when they completed it", + "pointsLabel": "Points:", + "pointsPlaceholder": "Points", + "approvalRequirement": "Approval Requirement", + "requireApproval": "Require admin approval", + "requireApprovalHelp": "This task will need approval from an admin before being marked as complete", + "privacySettings": "Privacy Settings", + "privacyDesc": "Who can see this task?", + "public": "Public", + "publicHelp": "Everyone in your circle", + "limited": "Limited", + "limitedHelp": "You and others that are assigned to the task", + "limitedDisabledHint": " (No assignees selected, Limited option is disabled)", + "createdBy": "Created by", + "updatedBy": "Updated by", + "archive": "Archive", + "unarchive": "Unarchive", + "create": "Create" + }, + "addTask": { + "title": "Create new task", + "defaultProject": "Default Project", + "anyone": "Anyone", + "taskPlaceholder": "Type your task...", + "descriptionChip": "Description", + "subtasksChip": "Subtasks", + "creating": "Creating…", + "createCount": "Create {{count}} Tasks", + "useTask": "Use Task", + "processing": "Processing", + "create": "Create" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index f2a5667..6f175b8 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -243,5 +243,90 @@ "cancelAll": "Cancel All" }, "attachments": "Attachments", - "fileNumbered": "File {{index}}" + "fileNumbered": "File {{index}}", + "onboarding": { + "sources": { + "speak": "Speak", + "snap": "Snap", + "type": "Type" + }, + "capture": { + "voiceCaption": "Dates, labels and points from your words", + "deviceCaption": "Read on your device", + "typeCaption": "#labels @people *points as you type", + "trashTitle": "♻️ Take out the trash", + "trashDue": "Due tomorrow", + "trashRepeat": "Every Monday", + "homeLabel": "Home", + "vehicleTitle": "🚗 Vehicle Registration Renewal", + "vehicleDue": "Due Aug 3", + "acTitle": "💨 Change the AC filter", + "acDue": "Due Fri", + "acRepeat": "Every 3 months" + }, + "schedule": { + "fromDueDate": "Reschedule from due date", + "fromCompletionDate": "Reschedule from completion date", + "due": "Due", + "done": "Done", + "next": "Next" + }, + "nfc": { + "tagOnWasher": "Tag on the washer", + "tapToOpen": "Tap to open", + "swapFilter": "Swap the washer filter", + "done": "Done", + "skip": "Skip", + "startTimer": "Start timer", + "waitingApproval": "{{name}} marked it done · waiting on you", + "approve": "Approve", + "reject": "Reject", + "completed": "Completed", + "note": "Used the delicate cycle — filter needs a clean next time.", + "metaJustNow": "Just now · ★ 5 pts", + "metaLastWeek": "Last week · ★ 5 pts" + }, + "circle": { + "kitchenTitle": "Kitchen deep clean", + "kitchenDue": "Due Sat", + "kitchenRepeat": "Every week", + "takesTurns": "Takes turns" + }, + "turns": { + "binsTitle": "🗑️ Take bins to the curb", + "binsDue": "Due Tomorrow", + "binsRepeat": "Every week", + "completed": "Completed", + "metaJustNow": "Just now · ★ 5 pts" + }, + "widget": { + "schoolForms": "School forms", + "takeOutTrash": "Take out the trash", + "waterPlants": "Water the plants", + "today": "Today", + "left": "{{count}} left" + }, + "reminders": { + "trashTitle": "Take out the trash", + "trashBody": "Due in 30 minutes · Bin night", + "finishedTitle": "Amalie finished Kitchen deep clean", + "finishedBody": "Your turn is next Saturday" + }, + "things": { + "waterBill": "Water bill", + "acFilter": "AC filter", + "trashDay": "Trash day", + "dogMedicine": "Dog's medicine", + "whoseTurn": "Whose turn to cook", + "monthly": "Monthly", + "everyThreeMonths": "Every 3 months", + "everyMonday": "Every Monday", + "daily": "Daily", + "weekly": "Weekly", + "tagBills": "Bills", + "tagHome": "Home", + "tagPets": "Pets", + "tagCooking": "Cooking" + } + } } diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index ba1e81c..18847d2 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -168,7 +168,8 @@ "joinCircle": "Join Circle", "joinedPending": "Joined circle successfully, wait for the circle owner to accept your request.", "alreadyMember": "You are already a member of this circle", - "joinFailed": "Failed to join circle" + "joinFailed": "Failed to join circle", + "copyLink": "Copy Link" }, "accountSettings": { "title": "Account Settings", @@ -423,7 +424,8 @@ "name": "Weekly Goals", "description": "Shows weekly progress and family completion stats" } - } + }, + "detailedDescription": "Customize the layout and visibility of cards in the sidepanel. the section only available on large screen devices such as tablets and desktops.." }, "theme": { "title": "Theme Preferences", @@ -457,7 +459,8 @@ "ymd": "YYYY-MM-DD (ISO)", "long": "Long format (e.g., January 1, 2024)", "short": "Short format (e.g., Jan 1, 2024)" - } + }, + "descriptionLong": "Customize language, date format, and regional preferences for your account. These settings will apply throughout the application." }, "advanced": { "title": "Advanced Settings", diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index fbceee6..a599a6c 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -37,6 +37,7 @@ import { } from '@mui/joy' import moment from 'moment' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import DurationInput from '../../components/common/DurationInput' @@ -94,6 +95,7 @@ const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month'] const NO_DUE_DATE_REQUIRED_TYPE = ['no_repeat', 'once'] const NO_DUE_DATE_ALLOWED_TYPE = ['trigger'] const ChoreEdit = () => { + const { t } = useTranslation('chores') const { data: userProfile, isLoading: isUserProfileLoading } = useUserProfile() @@ -207,16 +209,18 @@ const ChoreEdit = () => { const errors = {} if (name.trim() === '') { - errors.name = 'Name is required' + errors.name = t('choreEdit.errNameRequired') } if (frequencyType === 'interval' && !frequency > 0) { - errors.frequency = `Invalid frequency, the ${frequencyMetadata.unit} should be > 0` + errors.frequency = t('choreEdit.errFrequencyInvalid', { + unit: frequencyMetadata.unit, + }) } if ( frequencyType === 'days_of_the_week' && frequencyMetadata['days']?.length === 0 ) { - errors.frequency = 'Please select at least one day of the week' + errors.frequency = t('choreEdit.errSelectDayOfWeek') } // Validate advanced scheduling patterns @@ -226,14 +230,13 @@ const ChoreEdit = () => { (!frequencyMetadata?.occurrences || frequencyMetadata.occurrences.length === 0) ) { - errors.frequency = - 'Please select at least one day occurrence for the month' + errors.frequency = t('choreEdit.errSelectDayOccurrence') } if ( frequencyType === 'day_of_the_month' && frequencyMetadata['months']?.length === 0 ) { - errors.frequency = 'Please select at least one month' + errors.frequency = t('choreEdit.errSelectMonth') } if ( dueDate === null && @@ -243,14 +246,14 @@ const ChoreEdit = () => { if (REPEAT_ON_TYPE.includes(frequencyType)) { console.log('VALIDATION:', dueDate, frequencyType) - errors.dueDate = 'Start date is required' + errors.dueDate = t('choreEdit.errStartDateRequired') } else { - errors.dueDate = 'Due date is required' + errors.dueDate = t('choreEdit.errDueDateRequired') } } if (frequencyType === 'trigger') { if (!isThingValid) { - errors.thingTrigger = 'Thing trigger is invalid' + errors.thingTrigger = t('choreEdit.errThingTrigger') } } @@ -263,7 +266,7 @@ const ChoreEdit = () => { {errors[key]} )) showError({ - title: 'Please resolve the following errors:', + title: t('choreEdit.errTitle'), message: {errorList}, }) return false @@ -417,13 +420,13 @@ const ChoreEdit = () => { result?.res?._pendingCreate ) { showSuccess({ - title: 'Saved Offline', - message: 'Your changes will sync when you are back online.', + title: t('choreEdit.savedOfflineTitle'), + message: t('choreEdit.savedOfflineMessage'), }) } else { showSuccess({ - title: 'Chore Saved', - message: 'Your task has been saved successfully!', + title: t('choreEdit.savedTitle'), + message: t('choreEdit.savedMessage'), }) } Navigate('/chores') @@ -431,10 +434,10 @@ const ChoreEdit = () => { .catch(error => { console.error('Failed to save chore:', error) showError({ - title: 'Save Failed', + title: t('choreEdit.saveFailedTitle'), message: error?.isServerMessage ? error.message - : 'Failed to save chore, please try again.', + : t('choreEdit.saveFailedMessage'), }) }) } @@ -693,8 +696,8 @@ const ChoreEdit = () => { }) if (!response.ok) { showError({ - title: 'Upload Failed', - message: 'Failed to upload attachment.', + title: t('choreEdit.uploadFailedTitle'), + message: t('choreEdit.uploadFailedMessage'), }) return } @@ -710,8 +713,8 @@ const ChoreEdit = () => { ]) } catch { showError({ - title: 'Upload Failed', - message: 'Failed to upload attachment.', + title: t('choreEdit.uploadFailedTitle'), + message: t('choreEdit.uploadFailedMessage'), }) } finally { setIsUploadingAttachment(false) @@ -725,16 +728,16 @@ const ChoreEdit = () => { if (cancelled) return if (error || !image) { showError({ - title: 'Scan Failed', - message: error || 'Could not scan the document.', + title: t('choreEdit.scanFailedTitle'), + message: error || t('choreEdit.scanFailedMessage'), }) return } const file = await imageSourceToFile(image, `scan-${Date.now()}.jpg`) if (!file) { showError({ - title: 'Scan Failed', - message: 'Could not read the scanned image.', + title: t('choreEdit.scanFailedTitle'), + message: t('choreEdit.scanReadFailedMessage'), }) return } @@ -744,10 +747,10 @@ const ChoreEdit = () => { const handleDelete = () => { setConfirmModelConfig({ isOpen: true, - title: 'Delete Chore', - confirmText: 'Delete', - cancelText: 'Cancel', - message: 'Are you sure you want to delete this chore?', + title: t('choreEdit.deleteChoreTitle'), + confirmText: t('common:delete'), + cancelText: t('common:cancel'), + message: t('edit.deleteConfirm'), onClose: isConfirmed => { if (isConfirmed === true) { deleteChores.mutate([choreId], { @@ -756,8 +759,10 @@ const ChoreEdit = () => { }, onError: error => { showError({ - title: 'Delete Failed', - message: `Failed to delete chore: ${error.message}`, + title: t('choreEdit.deleteFailedTitle'), + message: t('choreEdit.deleteChoreFailed', { + error: error.message, + }), }) }, }) @@ -789,10 +794,8 @@ const ChoreEdit = () => { - Name - - What is the name of this task? - + {t('choreEdit.name')} + {t('choreEdit.nameDesc')} setName(e.target.value)} /> {errors.name} @@ -800,8 +803,10 @@ const ChoreEdit = () => { - Description - What is this task about? + {t('choreEdit.description')} + + {t('choreEdit.descriptionDesc')} + { - Priority - How important is this task? + {t('choreEdit.priority')} + {t('choreEdit.priorityDesc')} {/* Priority Chip Selection */} { minHeight: 34, }} > - No Priority + {t('choreEdit.noPriority')} @@ -869,9 +874,9 @@ const ChoreEdit = () => { {/* Project Selection - Show only if there are multiple projects */} {projects.length >= 1 && ( - Project + {t('choreEdit.project')} - Which project does this task belong to? + {t('choreEdit.projectDesc')} { @@ -1010,13 +1013,13 @@ const ChoreEdit = () => { }} > - Add New Label + {t('choreEdit.addNewLabel')} - Sub Tasks + {t('choreEdit.subTasks')} {/* { @@ -1049,8 +1052,10 @@ const ChoreEdit = () => { - Attachments - Files attached to this task + {t('choreEdit.attachments')} + + {t('choreEdit.attachmentsDesc')} + {attachments.length > 0 && ( { }) .catch(() => { showError({ - title: 'Delete Failed', - message: 'Failed to delete attachment.', + title: t('choreEdit.deleteFailedTitle'), + message: t('choreEdit.deleteAttachmentFailed'), }) }) }} @@ -1166,8 +1171,8 @@ const ChoreEdit = () => { }) .catch(() => { showError({ - title: 'Delete Failed', - message: 'Failed to delete attachment.', + title: t('choreEdit.deleteFailedTitle'), + message: t('choreEdit.deleteAttachmentFailed'), }) }) }} @@ -1188,7 +1193,7 @@ const ChoreEdit = () => { startDecorator={isUploadingAttachment ? null : } loading={isUploadingAttachment} > - Upload File + {t('choreEdit.uploadFile')} { disabled={isUploadingAttachment} onClick={handleScanAttachment} > - Scan + {t('choreEdit.scan')} )} @@ -1219,8 +1224,10 @@ const ChoreEdit = () => { {/* Section 2: Assignment & Responsibility */} - Assignees - Who can do this task? + {t('choreEdit.assignees')} + + {t('choreEdit.assigneesDesc')} + { overlay disableIcon variant='soft' - label='Anyone' + label={t('choreEdit.anyone')} /> @@ -1308,7 +1315,7 @@ const ChoreEdit = () => { setShowSaveAssigneeDefault(false) }} > - Remember for Future Tasks + {t('choreEdit.rememberFuture')} )} @@ -1323,15 +1330,17 @@ const ChoreEdit = () => { assignStrategyValue === 'no_assignee' ? 'none' : 'block', }} > - Currently Assigned To + + {t('choreEdit.currentlyAssigned')} + - Who is assigned the next due? + {t('choreEdit.currentlyAssignedDesc')} { checked={useCustomTime} onChange={e => handleUseCustomTimeChange(e.target.checked)} overlay - label='Set a specific time' + label={t('choreEdit.setSpecificTime')} /> {useCustomTime - ? 'Task will be due at the specified time' - : 'Task will be due at the end of the day (11:59 PM)'} + ? t('choreEdit.dueAtSpecifiedTime') + : t('choreEdit.dueEndOfDay')} {useCustomTime && ( - Time: + {t('choreEdit.timeLabel')} { {dueDate && ( - Task Window + {t('choreEdit.taskWindow')} - Define when this task can be completed and when it expires + {t('choreEdit.taskWindowDesc')} {/* Available From (Completion Window) */} @@ -1516,10 +1524,10 @@ const ChoreEdit = () => { } }} overlay - label='Set earliest completion time' + label={t('choreEdit.earliestCompletion')} /> - Task becomes available to complete X hours before the due date + {t('choreEdit.earliestCompletionHelp')} @@ -1531,7 +1539,9 @@ const ChoreEdit = () => { ml: 4, }} > - Hours: + + {t('choreEdit.hoursLabel')} + { max: 24 * 7, }, }} - placeholder='Hours' + placeholder={t('choreEdit.hoursPlaceholder')} onChange={e => { setCompletionWindow(parseInt(e.target.value)) }} @@ -1589,7 +1599,9 @@ const ChoreEdit = () => { size='sm' minValue={0} /> - after due date + + {t('choreEdit.afterDueDate')} + )} @@ -1597,9 +1609,9 @@ const ChoreEdit = () => { {!['once', 'no_repeat'].includes(frequencyType) && ( - Scheduling Preferences + {t('choreEdit.schedulingPrefs')} - How to reschedule the next due date? + {t('choreEdit.schedulingPrefsDesc')} div': { p: 1 } }}> @@ -1607,11 +1619,10 @@ const ChoreEdit = () => { overlay checked={!isRolling} onClick={() => setIsRolling(false)} - label='Reschedule from due date' + label={t('choreEdit.rescheduleFromDue')} /> - the next task will be scheduled from the original due date, - even if the previous task was completed late + {t('choreEdit.rescheduleFromDueHelp')} @@ -1622,11 +1633,10 @@ const ChoreEdit = () => { setIsRolling(true) setDeadlineOffset(-1) }} - label='Reschedule from completion date' + label={t('choreEdit.rescheduleFromCompletion')} /> - the next task will be scheduled from the actual completion - date of the previous task + {t('choreEdit.rescheduleFromCompletionHelp')} @@ -1635,11 +1645,10 @@ const ChoreEdit = () => { {/* Section 3.1: Notifications */} - Notifications + {t('choreEdit.notifications')} {!isPlusAccount(userProfile) && ( - Task notifications are not available in the Basic plan. Upgrade to - Plus to receive reminders when tasks are due or completed. + {t('choreEdit.notificationsPlanWarning')} )} @@ -1655,14 +1664,14 @@ const ChoreEdit = () => { checked={isNotificable} disabled={!isPlusAccount(userProfile)} overlay - label='Notify for this task' + label={t('choreEdit.notifyForTask')} /> - When should receive notifications for this task + {t('choreEdit.notifyForTaskHelp')} @@ -1677,7 +1686,7 @@ const ChoreEdit = () => { > - Notification Schedule + {t('choreEdit.notificationSchedule')} { - Who to Notify + {t('choreEdit.whoToNotify')} - Notify all assignees + + {t('choreEdit.allAssigneesHelp')} + @@ -1725,9 +1736,11 @@ const ChoreEdit = () => { ? notificationMetadata?.circleGroup : false } - label='Specific Group' + label={t('choreEdit.specificGroup')} /> - Notify a specific group + + {t('choreEdit.specificGroupHelp')} + {notificationMetadata?.circleGroup && ( @@ -1737,11 +1750,13 @@ const ChoreEdit = () => { ml: 4, }} > - Telegram Group ID: + + {t('choreEdit.telegramGroupIdLabel')} + { setNotificationMetadata({ ...notificationMetadata, @@ -1766,11 +1781,11 @@ const ChoreEdit = () => { pb: 1, }} > - Task Settings: + {t('choreEdit.taskSettings')} - Points System + {t('choreEdit.pointsSystem')} { @@ -1782,12 +1797,9 @@ const ChoreEdit = () => { }} checked={points > -1} overlay - label='Assign points for completion' + label={t('choreEdit.assignPoints')} /> - - Assign points to this task and user will earn points when they - completed it - + {t('choreEdit.assignPointsHelp')} {points != -1 && ( @@ -1797,7 +1809,9 @@ const ChoreEdit = () => { ml: 4, }} > - Points: + + {t('choreEdit.pointsLabel')} + { max: 1000, }, }} - placeholder='Points' + placeholder={t('choreEdit.pointsPlaceholder')} onChange={e => { setPoints(parseInt(e.target.value)) }} @@ -1819,7 +1833,9 @@ const ChoreEdit = () => { - Approval Requirement + + {t('choreEdit.approvalRequirement')} + { @@ -1827,18 +1843,17 @@ const ChoreEdit = () => { }} checked={requireApproval} overlay - label='Require admin approval' + label={t('choreEdit.requireApproval')} /> - This task will need approval from an admin before being marked as - complete + {t('choreEdit.requireApprovalHelp')} - Privacy Settings - Who can see this task? + {t('choreEdit.privacySettings')} + {t('choreEdit.privacyDesc')} { }} > - - Everyone in your circle + + {t('choreEdit.publicHelp')} - You and others that are assigned to the task + {t('choreEdit.limitedHelp')} {anyone || assignableTo.length === 0 - ? ' (No assignees selected, Limited option is disabled)' + ? t('choreEdit.limitedDisabledHint') : ''} @@ -1893,7 +1908,7 @@ const ChoreEdit = () => { setShowSavePrivacyDefault(false) }} > - Remember for Future Tasks + {t('choreEdit.rememberFuture')} )} @@ -1910,7 +1925,7 @@ const ChoreEdit = () => { }} > - Created by{' '} + {t('choreEdit.createdBy')}{' '} {membersData.res.find(f => f.userId === createdBy)?.displayName} {' '} @@ -1921,7 +1936,7 @@ const ChoreEdit = () => { - Updated by{' '} + {t('choreEdit.updatedBy')}{' '} { membersData.res.find(f => f.userId === updatedBy) @@ -1971,7 +1986,7 @@ const ChoreEdit = () => { : unarchiveChore.mutate(choreId) }} > - {isActive ? 'Archive' : 'Unarchive'} + {isActive ? t('choreEdit.archive') : t('choreEdit.unarchive')} { - Delete + {t('common:delete')} @@ -1999,13 +2014,13 @@ const ChoreEdit = () => { window.history.back() }} > - Cancel + {t('common:cancel')} {showKeyboardShortcuts && ( )} {userCircles.length > 0 && userCircles[0]?.userRole === 'member' && ( )} - Circle Members + + {t('circleSettings.circleMembers')} + {circleMembers.map(member => ( @@ -336,19 +339,27 @@ const Settings = () => { {member.displayName.charAt(0).toUpperCase() + member.displayName.slice(1)} - {member.userId === userProfile.id ? '(You)' : ''}{' '} + {member.userId === userProfile.id + ? t('circleSettings.you') + : ''}{' '} {' '} - {member.isActive ? member.role : 'Pending Approval'} + {member.isActive + ? member.role + : t('circleSettings.pendingApproval')} {member.isActive ? ( - Joined on {fmt.date(member.createdAt)} + {t('circleSettings.joinedOn', { + date: fmt.date(member.createdAt), + })} ) : ( - Request to join {fmt.date(member.updatedAt)} + {t('circleSettings.requestedToJoin', { + date: fmt.date(member.updatedAt), + })} )} @@ -378,28 +389,14 @@ const Settings = () => { } else { showNotification({ type: 'error', - message: 'Failed to update role', + message: t('circleSettings.roleUpdateFailed'), }) } }) }} > - {[ - { - value: 'member', - description: 'Just a regular member of the circle', - }, - { - value: 'manager', - description: - 'Can impersonate users and perform actions on their behalf', - }, - { - value: 'admin', - description: 'Full access to the circle', - }, - ].map((option, index) => ( - - {option.description} + {t(`circleSettings.roles.${option}Description`)} @@ -437,8 +433,10 @@ const Settings = () => { size='sm' onClick={() => { showConfirmation( - `Are you sure you want to remove ${member.displayName} from your circle?`, - 'Remove Member', + t('circleSettings.removeMemberMessage', { + name: member.displayName, + }), + t('circleSettings.removeMemberTitle'), () => { DeleteCircleMember( member.circleId, @@ -447,7 +445,7 @@ const Settings = () => { if (resp.ok) { showNotification({ type: 'success', - message: 'Removed member successfully', + message: t('circleSettings.memberRemoved'), }) // Invalidate and refetch circle-related queries queryClient.invalidateQueries(['circleMembers']) @@ -463,8 +461,8 @@ const Settings = () => { } }) }, - 'Remove', - 'Cancel', + t('common.remove'), + t('common.cancel'), 'danger', ) }} @@ -485,11 +483,15 @@ const Settings = () => { mb: 1, }} > - Circle Member Requests + + {t('circleSettings.circleMemberRequests')} + {lastRefresh && ( - Last updated: {fmt.dateTime(lastRefresh)} + {t('circleSettings.lastUpdated', { + time: fmt.dateTime(lastRefresh), + })} )} @@ -509,21 +513,24 @@ const Settings = () => { {circleMemberRequests.map(request => ( - {request.displayName} wants to join your circle. + {t('circleSettings.wantsToJoin', { name: request.displayName })} ))} - or + {t('circleSettings.or')} - if want to join someone else's Circle? Ask them for their unique - Circle code or join link. Enter the code below to join their Circle. + {t('circleSettings.joinOtherDescription')} - Enter Circle code: + {t('circleSettings.enterCircleCode')} setCircleInviteCode(e.target.value)} size='lg' @@ -575,20 +581,19 @@ const Settings = () => { if (resp.ok) { showNotification({ type: 'success', - message: - 'Joined circle successfully, wait for the circle owner to accept your request.', + message: t('circleSettings.joinedPending'), }) setTimeout(() => navigate('/'), 3000) } else { if (resp.status === 409) { showNotification({ type: 'error', - message: 'You are already a member of this circle', + message: t('circleSettings.alreadyMember'), }) } else { showNotification({ type: 'error', - message: 'Failed to join circle', + message: t('circleSettings.joinFailed'), }) } setTimeout(() => navigate('/'), 3000) @@ -596,24 +601,21 @@ const Settings = () => { }) }} > - Join Circle + {t('circleSettings.joinCircle')} {circleMembers.find(m => userProfile.id == m.userId)?.role === 'admin' && ( <> - Webhook + {t('advanced.webhookTitle')} - Webhooks allow you to send real-time notifications to other - services when events happen in your Circle. Configure a webhook - URL to receive real-time updates. + {t('advanced.webhookDescription')} {!isPlusAccount(userProfile) && ( - Webhook notifications are not available in the Basic plan. - Upgrade to Plus to receive real-time updates via webhooks. + {t('advanced.webhookPlusNotice')} )} @@ -627,7 +629,7 @@ const Settings = () => { } }} variant='soft' - label='Enable Webhook' + label={t('advanced.webhookToggle')} disabled={!isPlusAccount(userProfile)} overlay /> @@ -636,10 +638,10 @@ const Settings = () => { opacity: !isPlusAccount(userProfile) ? 0.5 : 1, }} > - Enable webhook notifications for tasks and things updates.{' '} + {t('advanced.webhookHelper')}{' '} {userProfile && !isPlusAccount(userProfile) && ( - Plus Feature + {t('common.plusFeature')} )} @@ -647,7 +649,9 @@ const Settings = () => { {webhookURL !== null && ( - Webhook URL + + {t('advanced.webhookURL')} + setWebhookURL(e.target.value)} @@ -670,19 +674,19 @@ const Settings = () => { if (resp.ok) { showNotification({ type: 'success', - message: 'Webhook URL updated successfully', + message: t('advanced.webhookUpdated'), }) } else { showNotification({ type: 'error', - message: 'Failed to update webhook URL', + message: t('advanced.webhookUpdateFailed'), }) } }) }} disabled={!isPlusAccount(userProfile)} > - Save + {t('common.save')} )} @@ -695,13 +699,13 @@ const Settings = () => {
- Account Settings + {t('accountSettings.title')} - Change your account settings, type or update your password + {t('accountSettings.description')} - Account Type : {getSubscriptionStatus()} + {t('accountSettings.accountType', { type: getSubscriptionStatus() })} {getSubscriptionDetails()} @@ -733,8 +737,7 @@ const Settings = () => { queryClient.refetchQueries(['userProfile']) showNotification({ type: 'success', - message: - 'Purchase successful! Please restart the app to access Plus features.', + message: t('accountSettings.purchase.success'), }) // invalidate user profile to get new subscription status: } @@ -749,57 +752,49 @@ const Settings = () => { // Store problem showNotification({ type: 'error', - message: - 'Store connection issue. Please check your network and try again.', + message: t('accountSettings.purchase.storeConnection'), }) } else if (error.code === '3') { // Purchase not allowed showNotification({ type: 'error', - message: - 'Purchases are not allowed on this device. Please check your device restrictions.', + message: t('accountSettings.purchase.notAllowed'), }) } else if (error.code === '4') { // Product not available showNotification({ type: 'error', - message: - 'This subscription is not available. Please try again later.', + message: t('accountSettings.purchase.unavailable'), }) } else if (error.code === '5') { // Receipt already in use showNotification({ type: 'error', - message: - 'This purchase has already been processed. If you believe this is an error, please contact support.', + message: t('accountSettings.purchase.alreadyProcessed'), }) } else if (error.code === '6') { // Missing receipt file showNotification({ type: 'error', - message: - 'Purchase receipt missing. Please try purchasing again.', + message: t('accountSettings.purchase.receiptMissing'), }) } else if (error.code === '7') { // Network error showNotification({ type: 'error', - message: - 'Network error. Please check your connection and try again.', + message: t('accountSettings.purchase.networkError'), }) } else if (error.code === '8') { // Invalid receipt showNotification({ type: 'error', - message: - 'Invalid purchase receipt. Please contact support if this persists.', + message: t('accountSettings.purchase.invalidReceipt'), }) } else if (error.code === '9') { // Payment pending showNotification({ type: 'warning', - message: - 'Payment is pending approval. You will receive access once approved.', + message: t('accountSettings.purchase.pending'), }) } else { // Generic error @@ -808,7 +803,11 @@ const Settings = () => { console.error('Error occurred in purchase flow') showNotification({ type: 'error', - message: `Purchase failed: ${error.message || 'Unknown error'}. Please try again or contact support.`, + message: t('accountSettings.purchase.failed', { + error: + error.message || + t('accountSettings.purchase.unknownError'), + }), }) } } @@ -817,7 +816,7 @@ const Settings = () => { } }} > - Upgrade + {t('accountSettings.upgrade')} {userProfile?.subscription === 'active' && ( @@ -833,14 +832,14 @@ const Settings = () => { setNativeCancelModal(true) }} > - Cancel + {t('accountSettings.cancel')} )} {import.meta.env.VITE_IS_SELF_HOSTED === 'true' && ( - Password : + {t('accountSettings.password')} {changePasswordModal ? ( { if (resp.ok) { showNotification({ type: 'success', - message: 'Password changed successfully', + message: t('accountSettings.passwordChanged'), }) } else { showNotification({ type: 'error', - message: 'Password change failed', + message: t('accountSettings.passwordChangeFailed'), }) } }) @@ -879,18 +878,17 @@ const Settings = () => { - Danger Zone + {t('accountSettings.dangerZone')} - Once you delete your account, there is no going back. Please be - certain. + {t('accountSettings.dangerZoneDescription')}
@@ -899,32 +897,26 @@ const Settings = () => {
- Sidepanel Customization + {t('sidepanel.title')} - Customize the layout and visibility of cards in the sidepanel. the - section only available on large screen devices such as tablets and - desktops.. + {t('sidepanel.detailedDescription')}
- Theme preferences + {t('theme.title')} - - Choose how the site looks to you. Select a single theme, or sync with - your system and automatically switch between day and night themes. - + {t('theme.description')}
- Localization + {t('localization.title')} - Customize language, date format, and regional preferences for your - account. These settings will apply throughout the application. + {t('localization.descriptionLong')}
diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index 8c23ce6..94e6a65 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -15,6 +15,7 @@ import * as chrono from 'chrono-node' import moment from 'moment' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { flushSync } from 'react-dom' +import { useTranslation } from 'react-i18next' import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint' import ModalActions from '../../components/common/ModalActions' @@ -186,6 +187,7 @@ const POINTS_SUGGESTIONS = { const PARSE_DEBOUNCE_MS = 150 const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { + const { t } = useTranslation('chores') const { ResponsiveModal } = useResponsiveModal() const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm')) const pickerEmptyDisplay = isMobile ? 'icon' : 'icon-text' @@ -229,13 +231,13 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { value: 'userId', display: 'displayName', options: [ - { userId: 'anyone', displayName: 'Anyone' }, + { userId: 'anyone', displayName: t('addTask.anyone') }, ...(circleMembers?.res || []), ], }, '*': POINTS_SUGGESTIONS, }), - [userLabels, circleMembers, handleCreateLabel], + [userLabels, circleMembers, handleCreateLabel, t], ) const [taskText, setTaskText] = useState('') @@ -1156,7 +1158,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { onClose={handleCloseModal} size='lg' fullWidth={true} - title='Create new task' + title={t('addTask.title')} footer={ {!showScan && !showVoice && projects?.length >= 1 && ( @@ -1177,7 +1179,9 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { fontWeight: 'normal', }} > - {selectedProject.name} + {selectedProject.id === 'default' + ? t('addTask.defaultProject') + : selectedProject.name} { sx={{ fontSize: 18, color: project.color }} /> - {project.name} + {project.id === 'default' + ? t('addTask.defaultProject') + : project.name} ) })} @@ -1211,7 +1217,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { color='neutral' onClick={handleCloseModal} > - Cancel + {t('common:cancel')} {showKeyboardShortcuts && ( { onClick={handleVoiceConfirm} > {creatingVoiceTasks - ? 'Creating…' + ? t('addTask.creating') : voiceState.segments.length > 1 - ? `Create ${voiceState.segments.length} Tasks` - : 'Use Task'} + ? t('addTask.createCount', { + count: voiceState.segments.length, + }) + : t('addTask.useTask')} )} {showScan && scanState.primaryAction && ( @@ -1249,7 +1257,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { )} {showScan && scanState.phase === 'processing' && ( )} {!showScan && !showVoice && ( @@ -1260,7 +1268,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { disabled={!taskTitle.trim() || isAttachingScan} onClick={submitChore} > - Create + {t('addTask.create')} {showKeyboardShortcuts && ( )} @@ -1349,7 +1357,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { onVoiceClick={ voiceAvailable ? () => setShowVoice(true) : undefined } - placeholder='Type your task...' + placeholder={t('addTask.taskPlaceholder')} onChange={text => { setTaskText(text) if (!text) setTaskTitle('') @@ -1484,7 +1492,9 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { }} > - Description + + {t('addTask.descriptionChip')} + )} {!hasSubTasks && ( @@ -1507,7 +1517,9 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => { }} > - Subtasks + + {t('addTask.subtasksChip')} + )}