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')}
{projects.map(project => (
@@ -947,10 +952,8 @@ const ChoreEdit = () => {
)}
- Labels
-
- Things to remember about this task or to tag it
-
+ {t('choreEdit.labels')}
+ {t('choreEdit.labelsDesc')}
- 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')}
- Assignment Strategy
+
+ {t('choreEdit.assignStrategy')}
+
- How to pick the next assignee for the following task?
+ {t('choreEdit.assignStrategyDesc')}
{
overlay
disableIcon
variant='soft'
- label={item
- .split('_')
- .map(x => x.charAt(0).toUpperCase() + x.slice(1))
- .join(' ')}
+ label={t(`choreEdit.strategy.${item}`)}
/>
))}
@@ -1412,11 +1420,13 @@ const ChoreEdit = () => {
- {REPEAT_ON_TYPE.includes(frequencyType) ? 'Start Date' : 'Due Date'}
+ {REPEAT_ON_TYPE.includes(frequencyType)
+ ? t('choreEdit.startDate')
+ : t('choreEdit.dueDate')}
{frequencyType === 'trigger' && !dueDate && (
- Due Date will be set when the trigger of the thing is met
+ {t('choreEdit.triggerDueHint')}
)}
@@ -1442,11 +1452,9 @@ const ChoreEdit = () => {
defaultChecked={dueDate !== null}
checked={dueDate !== null}
overlay
- label='Give this task a due date'
+ label={t('choreEdit.giveDueDate')}
/>
-
- Task needs to be completed by a specific time
-
+ {t('choreEdit.giveDueDateHelp')}
)}
{dueDate && (
@@ -1454,8 +1462,8 @@ const ChoreEdit = () => {
{REPEAT_ON_TYPE.includes(frequencyType)
- ? 'When does this task start?'
- : 'When is the next first time this task is due?'}
+ ? t('choreEdit.startWhen')
+ : t('choreEdit.dueWhen')}
{
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')}
{
@@ -1999,13 +2014,13 @@ const ChoreEdit = () => {
window.history.back()
}}
>
- Cancel
+ {t('common:cancel')}
{showKeyboardShortcuts && (
)}
)
-export const CaptureVignette = () => (
-
-
- {SOURCES.map((source, index) => (
-
- ))}
-
+export const CaptureVignette = () => {
+ const { t } = useTranslation('common')
+ return (
+
+
+ {SOURCES.map((source, index) => (
+
+ ))}
+
-
- }
- caption='Dates, labels and points from your words'
- card={
- }
- after={
-
- }
- />
- }
- />
- }
- caption='Read on your device'
- card={
- }
- after={
-
-
-
- }
- />
- }
- />
- }
- />
- }
- caption='#labels @people *points as you type'
- card={
- }
- after={
-
-
-
- }
- />
- }
- />
- }
- />
-
-
-)
+
+ }
+ caption={t('onboarding.capture.voiceCaption')}
+ card={
+ }
+ after={
+
+ }
+ />
+ }
+ />
+ }
+ caption={t('onboarding.capture.deviceCaption')}
+ card={
+ }
+ after={
+
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ caption={t('onboarding.capture.typeCaption')}
+ card={
+ }
+ after={
+
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+ )
+}
/* --------------------------------- slide 2: due date vs. completion date */
@@ -692,69 +701,93 @@ const Milestone = ({ date, delay, label, tone = 'neutral' }) => (
)
-const ScheduleTrack = ({ delay, mode, next }) => (
-
- {
+ const { t } = useTranslation('common')
+ return (
+
- {mode}
-
-
-
- {/* Connector sits behind the milestones and draws itself first. */}
-
-
-
-
-
+ {mode}
+
+
+
+ {/* Connector sits behind the milestones and draws itself first. */}
+
+
+
+
+
+
-
-)
+ )
+}
-export const ScheduleVignette = () => (
-
-
-
-
-)
+export const ScheduleVignette = () => {
+ const { t } = useTranslation('common')
+ return (
+
+
+
+
+ )
+}
/* ------------------------- slide 3: tap to finish, note, logged in history */
@@ -907,161 +940,164 @@ const ActionRow = ({ children }) => (
// Off the deck as of the five-slide cut — NFC now rides along in the "nobody
// has to be the nag" copy. Kept intact so it can be swapped back in as its own
// slide without rebuilding it.
-export const NfcVignette = () => (
-
-
+export const NfcVignette = () => {
+ const { t } = useTranslation('common')
+ return (
+
- {/* Two rings leaving the tag: the phone reading it. */}
- {[0, 1].map(ring => (
-
- ))}
-
-
-
-
-
- Tag on the washer
-
-
- Tap to open
-
-
-
-
- {/* The task the tag opens, with the action row the app actually shows. */}
-
-
- Swap the washer filter
-
-
-
-
-
- }
- label='Done'
- />
- }
- label='Skip'
- />
-
- }
- label='Start timer'
- />
-
-
-
- }>
- Amalie marked it done · waiting on you
-
-
- }
- label='Approve'
- />
- }
- label='Reject'
- />
-
-
-
-
-
- {/* A tighter radius than the task cards: at radius 20 the corner clips
- the status rule and it reads as a rendering slip. */}
-
- {/* grid-template-rows animates the new entry open, pushing the older
- one down the way the real list does — no height thrash. */}
-
- }
- performer='Mo'
- note='Used the delicate cycle — filter needs a clean next time.'
- meta='Just now · ★ 5 pts'
- />
+ {/* Two rings leaving the tag: the phone reading it. */}
+ {[0, 1].map(ring => (
+
+ ))}
+
+
+
+
+
+ {t('onboarding.nfc.tagOnWasher')}
+
+
+ {t('onboarding.nfc.tapToOpen')}
+
- }
- performer='Amalie'
- meta='Last week · ★ 5 pts'
- />
-
-
-)
+ {/* The task the tag opens, with the action row the app actually shows. */}
+
+
+ {t('onboarding.nfc.swapFilter')}
+
+
+
+
+
+ }
+ label={t('onboarding.nfc.done')}
+ />
+ }
+ label={t('onboarding.nfc.skip')}
+ />
+
+ }
+ label={t('onboarding.nfc.startTimer')}
+ />
+
+
+
+ }>
+ {t('onboarding.nfc.waitingApproval', { name: 'Amalie' })}
+
+
+ }
+ label={t('onboarding.nfc.approve')}
+ />
+ }
+ label={t('onboarding.nfc.reject')}
+ />
+
+
+
+
+
+ {/* A tighter radius than the task cards: at radius 20 the corner clips
+ the status rule and it reads as a rendering slip. */}
+
+ {/* grid-template-rows animates the new entry open, pushing the older
+ one down the way the real list does — no height thrash. */}
+
+
+ }
+ performer='Mo'
+ note={t('onboarding.nfc.note')}
+ meta={t('onboarding.nfc.metaJustNow')}
+ />
+
+
+
+ }
+ performer='Amalie'
+ meta={t('onboarding.nfc.metaLastWeek')}
+ />
+
+
+ )
+}
/* ----------------------------------------------- slide 4: share the load */
@@ -1075,90 +1111,93 @@ const AVATAR = 40
const AVATAR_GAP = 12
const STEP = AVATAR + AVATAR_GAP
-export const CircleVignette = () => (
-
-
- {MEMBERS.map(member => (
-
- ))}
-
- }
- />
+export const CircleVignette = () => {
+ const { t } = useTranslation('common')
+ return (
+
+
+ {MEMBERS.map(member => (
+
+ ))}
+
+ }
+ />
-
- {/* Selection ring hopping between members: the rotating assignee. */}
- {MEMBERS.map(member => (
+ >
+ {/* Selection ring hopping between members: the rotating assignee. */}
- {member.initial}
-
- ))}
-
+ />
+ {MEMBERS.map(member => (
+
+ {member.initial}
+
+ ))}
+
- } color='neutral'>
- Takes turns
-
-
-
-)
+ } color='neutral'>
+ {t('onboarding.circle.takesTurns')}
+
+
+
+ )
+}
// One member's turn on the task, and one at a time. Slower than the shared
// CYCLE_MS so each hand-off has room to read.
@@ -1208,52 +1247,55 @@ const historyStackKeyframes = {
* completion onto the top of the stack — pushing the older ones back and
* capping out at three before the oldest cycles away.
*/
-export const TakesTurnsVignette = () => (
-
-
- {MEMBERS.map(member => (
-
- ))}
-
- }
- />
+export const TakesTurnsVignette = () => {
+ const { t } = useTranslation('common')
+ return (
+
+
+ {MEMBERS.map(member => (
+
+ ))}
+
+ }
+ />
-
- {NAG_HISTORY_ORDER.map((member, index) => (
-
- }
- performer={member.name}
- meta='Just now · ★ 5 pts'
- divider={false}
- />
-
- ))}
-
-
-)
+
+ {NAG_HISTORY_ORDER.map((member, index) => (
+
+ }
+ performer={member.name}
+ meta={t('onboarding.turns.metaJustNow')}
+ divider={false}
+ />
+
+ ))}
+
+
+ )
+}
/* ------------------------------------- slide 5: widgets on the home screen */
@@ -1315,39 +1357,46 @@ const WidgetTask = ({ done, name, time }) => (
)
-const TodayWidget = () => (
-
-
-
- Today
-
- 3 left
-
-
+const TodayWidget = () => {
+ const { t } = useTranslation('common')
+ return (
+
+
+
+ {t('onboarding.widget.today')}
+
+ {t('onboarding.widget.left', { count: 3 })}
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-)
+ )
+}
/* ------------------- notification banners, stacked over the Today widget */
@@ -1409,24 +1458,27 @@ const NotificationBanner = ({
* and a circle update) arriving over the home-screen widget they'll be glancing
* at all day.
*/
-export const RemindersVignette = () => (
-
- }
- title='Take out the trash'
- body='Due in 30 minutes · Bin night'
- delay={160}
- />
- }
- title='Amalie finished Kitchen deep clean'
- body='Your turn is next Saturday'
- delay={520}
- />
-
-
-)
+export const RemindersVignette = () => {
+ const { t } = useTranslation('common')
+ return (
+
+ }
+ title={t('onboarding.reminders.trashTitle')}
+ body={t('onboarding.reminders.trashBody')}
+ delay={160}
+ />
+ }
+ title={t('onboarding.reminders.finishedTitle')}
+ body={t('onboarding.reminders.finishedBody')}
+ delay={520}
+ />
+
+
+ )
+}
/* -------------------------------------------- slide 1: the problem itself */
@@ -1440,131 +1492,134 @@ export const RemindersVignette = () => (
// instead of a full-width list row.
const LITTLE_THINGS = [
{
- label: 'Water bill',
+ key: 'waterBill',
priority: 1,
- frequency: 'Monthly',
- tag: 'Bills',
+ frequencyKey: 'monthly',
+ tagKey: 'tagBills',
tagColor: '#5c6bc0',
from: '-28px, 18px, -6deg',
},
{
- label: 'AC filter',
+ key: 'acFilter',
priority: 2,
- frequency: 'Every 3 months',
- tag: 'Home',
+ frequencyKey: 'everyThreeMonths',
+ tagKey: 'tagHome',
tagColor: '#26a69a',
from: '30px, 22px, 5deg',
},
{
- label: 'Trash day',
+ key: 'trashDay',
priority: 2,
- frequency: 'Every Monday',
- tag: 'Home',
+ frequencyKey: 'everyMonday',
+ tagKey: 'tagHome',
tagColor: '#26a69a',
from: '-34px, 26px, -4deg',
},
{
- label: "Dog's medicine",
+ key: 'dogMedicine',
priority: 1,
- frequency: 'Daily',
- tag: 'Pets',
+ frequencyKey: 'daily',
+ tagKey: 'tagPets',
tagColor: '#ec407a',
from: '26px, 30px, 6deg',
},
{
- label: 'Whose turn to cook',
+ key: 'whoseTurn',
priority: 3,
- frequency: 'Weekly',
- tag: 'Cooking',
+ frequencyKey: 'weekly',
+ tagKey: 'tagCooking',
tagColor: '#66bb6a',
from: '-22px, 34px, -5deg',
},
]
-export const ProblemVignette = () => (
-
-
- {LITTLE_THINGS.map((thing, index) => (
- {
+ const { t } = useTranslation('common')
+ return (
+
+
+ {LITTLE_THINGS.map((thing, index) => (
+
-
-
-
-
- {thing.label}
-
-
-
-
- {thing.frequency}
-
-
-
-
-
- {thing.tag}
+
+
+
+
+ {t(`onboarding.things.${thing.key}`)}
+
+
+
+
+ {t(`onboarding.things.${thing.frequencyKey}`)}
+
+
+
+
+
+ {t(`onboarding.things.${thing.tagKey}`)}
+
-
- ))}
-
-
-)
+ ))}
+
+
+ )
+}
diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx
index 7d5b0be..1325a58 100644
--- a/src/views/Settings/Settings.jsx
+++ b/src/views/Settings/Settings.jsx
@@ -20,6 +20,7 @@ import { Purchases } from '@revenuecat/purchases-capacitor'
import { useQueryClient } from '@tanstack/react-query'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import RealTimeSettings from '../../components/RealTimeSettings'
@@ -57,6 +58,7 @@ import StorageSettings from './StorageSettings'
import ThemeToggle from './ThemeToggle'
const Settings = () => {
+ const { t } = useTranslation('settings')
const { data: userProfile } = useUserProfile()
const queryClient = useQueryClient()
const { showNotification } = useNotification()
@@ -83,8 +85,8 @@ const Settings = () => {
message,
title,
onConfirm,
- confirmText = 'Confirm',
- cancelText = 'Cancel',
+ confirmText = t('common.confirm'),
+ cancelText = t('common.cancel'),
color = 'primary',
) => {
setConfirmModalConfig({
@@ -112,7 +114,7 @@ const Settings = () => {
} catch (error) {
showNotification({
type: 'error',
- message: 'Failed to refresh member requests',
+ message: t('circleSettings.refreshFailed'),
})
} finally {
setIsRefreshing(false)
@@ -195,27 +197,29 @@ const Settings = () => {
const getSubscriptionDetails = () => {
if (userProfile?.subscription === 'active') {
- return `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date(
- userProfile?.expiration,
- )}.`
+ return t('accountSettings.activeDescription', {
+ date: fmt.date(userProfile?.expiration),
+ })
} else if (userProfile?.subscription === 'cancelled') {
- return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${fmt.date(
- userProfile?.expiration,
- )}.`
+ return t('accountSettings.cancelledDescription', {
+ date: fmt.date(userProfile?.expiration),
+ })
} else {
- return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
+ return t('accountSettings.freeDescription')
}
}
const getSubscriptionStatus = () => {
if (userProfile?.subscription === 'active') {
- return `Plus`
+ return t('accountSettings.plus')
} else if (userProfile?.subscription === 'cancelled') {
if (moment().isBefore(userProfile?.expiration)) {
- return `Plus(until ${fmt.date(userProfile?.expiration)})`
+ return t('accountSettings.plusUntil', {
+ date: fmt.date(userProfile?.expiration),
+ })
}
- return `Free`
+ return t('accountSettings.free')
} else {
- return `Free`
+ return t('accountSettings.free')
}
}
@@ -241,18 +245,15 @@ const Settings = () => {
- Circle settings
+ {t('circleSettings.title')}
- Your account is automatically connected to a Circle when you create or
- join one. Easily invite friends by sharing the unique Circle code or
- link below. You'll receive a notification below when someone requests
- to join your Circle.
+ {t('circleSettings.description')}
{userCircles[0]?.userRole === 'member'
- ? `You part of ${userCircles[0]?.name} `
- : `You circle code is:`}
+ ? t('circleSettings.memberOf', { name: userCircles[0]?.name })
+ : t('circleSettings.yourCircleCode')}
{
navigator.clipboard.writeText(userCircles[0]?.invite_code)
showNotification({
type: 'success',
- message: 'Code copied to clipboard',
+ message: t('circleSettings.codeCopied'),
})
}}
>
- Copy Code
+ {t('circleSettings.copyCode')}
{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) => (
-
@@ -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')}