diff --git a/public/locales/en/chores.json b/public/locales/en/chores.json index d4d0001..6555089 100644 --- a/public/locales/en/chores.json +++ b/public/locales/en/chores.json @@ -72,12 +72,338 @@ "paused": "Timer Paused", "reset": "Reset Timer", "delete": "Delete Session" - } + }, + "unableUndo": "Failed to undo", + "offlineComplete": "You're offline — completion will sync when back online", + "unableComplete": "Unable to complete task", + "offlineSkip": "You're offline — skip will sync when back online", + "unableSkip": "Unable to skip task", + "offlineStart": "You're offline — start will sync when back online", + "unableStart": "Unable to start task", + "offlinePause": "You're offline — pause will sync when back online", + "unablePause": "Unable to pause task", + "offlineRestore": "You're offline — restore will sync when back online", + "restoreFailed": "Failed to restore", + "unableRestore": "Unable to restore task" }, "toolbar": { - "defaultProject": "Default Project" + "defaultProject": "Default Project", + "everyone": "Everyone", + "mine": "Mine", + "availableToMe": "Available to me", + "others": "Others", + "cards": "Cards", + "compact": "Compact", + "calendar": "Calendar", + "filters": "Filters", + "viewGroup": "View & Group", + "filterNamePlaceholder": "Filter name…", + "clearAll": "Clear all", + "saveFilter": "Save Filter", + "saveAsNew": "Save as New Filter", + "groupBy": "Group by", + "showTasksFor": "Show tasks for", + "type": { + "assignee": "Assignee" + }, + "display": "Display", + "displayOptions": "Display options" }, "sort": { - "assignedToMe": "Assigned to me" + "assignedToMe": "Assigned to me", + "smart": "Smart", + "createdDate": "Created Date", + "updatedDate": "Updated Date", + "sortAndGroup": "Sort and Group (Ctrl+G)", + "groupBy": "Group By", + "quickFilters": "Quick Filters", + "assignedTo": "Assigned to:", + "availableForMe": "Available for me", + "assignedToOthers": "Assigned to others", + "createFilter": "Create Filter", + "createFilterHint": "Build advanced filter rules" + }, + "thing": { + "triggerHint": "Trigger a task when a thing state changes to a desired state", + "selectThing": "Select a thing", + "enterText": "Enter the text to trigger the task" + }, + "activity": { + "showMore": "Show more", + "title": "Recent Activities", + "loading": "Loading activities...", + "status": { + "done": "Done" + } + }, + "group": { + "overdue": "Overdue", + "dueToday": "Due Today", + "pendingApproval": "Pending Approval", + "dueThisWeek": "Due This Week", + "noDueDate": "No Due Date", + "dueDate": "Due Date", + "dueLater": "Due Later" + }, + "insights": { + "highPriority": "High Priority" + }, + "labels": { + "label": "Labels" + }, + "actionMenu": { + "view": "View", + "archive": "Archive", + "writeNFC": "Write to NFC", + "completeWithNote": "Complete with note", + "completeInPast": "Complete in past", + "skipToNext": "Skip to next due date", + "sendNudge": "Send nudge", + "history": "History", + "clone": "Clone", + "moveToProject": "Move to project", + "removeDueDate": "Remove due date" + }, + "actions": { + "rescheduledTitle": "Task Rescheduled", + "rescheduledMessage": "The task due date has been updated successfully.", + "unplannedTitle": "Task Unplanned", + "unplannedMessage": "The task is now unplanned and has no due date.", + "archivedTitle": "Task Archived", + "startedTitle": "Task Started", + "startedMessage": "The task has been marked as started.", + "pausedTitle": "Task Paused", + "pausedMessage": "The task has been paused.", + "deletedMessage": "The task has been deleted.", + "undoable": { + "completed": "Task completed" + }, + "completionPending": "Task completion pending", + "failCompleteTitle": "Failed to complete", + "failStartTitle": "Failed to start", + "failPauseTitle": "Failed to pause", + "failApproveTitle": "Failed to approve", + "failRejectTitle": "Failed to reject", + "deletedMessageLong": "The task has been deleted successfully.", + "failDeleteTitle": "Failed to delete", + "failArchiveTitle": "Failed to archive", + "failSkipTitle": "Failed to skip", + "movedTitle": "Task Moved", + "failMoveTitle": "Failed to move task", + "failRescheduleTitle": "Failed to reschedule", + "nudgeSentTitle": "Nudge Sent!", + "nudgeFailed": "Failed to send nudge", + "failNudgeTitle": "Failed to Send Nudge", + "bulk": { + "completeTitle": "Complete Tasks", + "completedTitle": "✅ Tasks Completed", + "completeFailedTitle": "Bulk Complete Failed", + "archiveTitle": "Archive Tasks", + "archivedTitle": "📦 Tasks Archived", + "archiveFailedTitle": "Bulk Archive Failed", + "deleteTitle": "Delete Tasks", + "skipTitle": "Skip Tasks", + "skippedTitle": "⏭️ Tasks Skipped", + "skipFailedTitle": "Bulk Skip Failed" + } + }, + "archived": { + "restoredTitle": "Task Restored", + "restoredMsg": "The task has been restored and is now active.", + "deletedTitle": "Task Deleted", + "delete": "Delete", + "someFailedTitle": "Some Tasks Failed", + "unexpectedError": "An unexpected error occurred. Please try again.", + "deletedBulkTitle": "🗑️ Tasks Deleted", + "bulkDeleteFailTitle": "Bulk Delete Failed", + "archivedDate": "Archived Date", + "loadFailTitle": "Failed to load archived tasks", + "loadFailMsg": "Please try again later.", + "deletedMsg": "The archived task has been permanently deleted.", + "restoreTasksTitle": "Restore Tasks", + "restore": "Restore", + "restoreConfirm_one": "Restore {{count}} task to active list?", + "restoreConfirm_other": "Restore {{count}} tasks to active list?", + "restoredBulkTitle": "📤 Tasks Restored", + "restoredCount_one": "Restored {{count}} task{{offline}}.", + "restoredCount_other": "Restored {{count}} tasks{{offline}}.", + "restoreFailCount_one": "{{count}} task could not be restored.", + "restoreFailCount_other": "{{count}} tasks could not be restored.", + "bulkRestoreFailTitle": "Bulk Restore Failed", + "deleteTasksTitle": "Delete Archived Tasks", + "deleteConfirm_one": "Permanently delete {{count}} archived task?\n\nThis action cannot be undone.", + "deleteConfirm_other": "Permanently delete {{count}} archived tasks?\n\nThis action cannot be undone.", + "deletedCount_one": "Successfully deleted {{count}} task.", + "deletedCount_other": "Successfully deleted {{count}} tasks.", + "deleteFailCount_one": "{{count}} task could not be deleted.", + "deleteFailCount_other": "{{count}} tasks could not be deleted.", + "title": "Archived Tasks", + "subtitle": "View and manage tasks that have been archived or completed.", + "heading": "Archived Tasks", + "searchPlaceholder": "Search archived tasks", + "switchToCompact": "Switch to Compact View", + "switchToCard": "Switch to Card View", + "exitMultiSelect": "Exit Multi-select Mode (Ctrl+S)", + "enableMultiSelect": "Enable Multi-select Mode (Ctrl+S)", + "selected_one": "{{count}} task selected", + "selected_other": "{{count}} tasks selected", + "selectAllTitle": "Select all visible tasks (Ctrl+A)", + "all": "All", + "closeMultiSelect": "Close multi-select (Esc)", + "clearMultiSelect": "Clear multi-select (Esc)", + "close": "Close", + "clear": "Clear", + "restoreSelectedTitle": "Restore selected tasks (R)", + "deleteSelectedTitle": "Delete selected tasks (E)", + "clearSearch": "Clear search", + "clearFilters": "Clear filters", + "count_one": "{{count}} archived task", + "count_other": "{{count}} archived tasks", + "matching": " matching \"{{term}}\"" + }, + "edit": { + "deleteConfirm": "Are you sure you want to delete this chore?" + }, + "list": { + "complete": "Complete", + "filterSaved": "Filter Saved", + "filterSavedMsg": "\"{{name}}\" has been saved", + "nothingScheduled": "Nothing scheduled", + "createChore": "Create new chore (Cmd+C)", + "filterUpdated": "Filter Updated", + "filterUpdatedMsg": "\"{{name}}\" has been updated successfully", + "advancedFilterCreated": "Advanced Filter Created", + "advancedFilterCreatedMsg": "\"{{name}}\" has been created successfully" + }, + "multiToolbar": { + "skip": "Skip", + "completeTitle": "Complete selected tasks (Enter)", + "complete": "Complete", + "skipTitle": "Skip selected tasks (/)", + "archiveTitle": "Archive selected tasks (X)", + "archive": "Archive", + "deleteTitle": "Delete selected tasks (Shift+X)" + }, + "remind": { + "title": "Reminders" + }, + "assignee": { + "anyone": "Anyone" + }, + "filterChip": { + "cancelAll": "Cancel All Filters", + "unpin": "Unpin filter", + "pin": "Pin filter", + "edit": "Edit filter", + "delete": "Delete filter" + }, + "multiSelect": { + "showShortcuts": "Show keyboard shortcuts", + "title": "Multi-select Mode", + "selection": "Selection", + "selectAll": "Select all visible tasks", + "clearOrExit": "Clear selection or exit multi-select mode", + "actions": "Actions", + "markCompleted": "Mark selected tasks as completed", + "deleteSelected": "Delete selected tasks", + "interface": "Interface", + "quickAdd": "Quick add new task" + }, + "notifications": { + "needTitle": "Need Notification?", + "needBody": "You need to enable permission to receive notifications, do you want to enable it?", + "keepDisabled": "No, Keep it Disabled" + }, + "assigneeCard": { + "loading": "Loading tasks by assignee...", + "title": "Tasks by Assignee", + "scheduled": "Scheduled", + "pendingReview": "Pending Review" + }, + "filter": { + "status": { + "inProgress": "In Progress" + } + }, + "impersonate": { + "viewAs": "View tasks as", + "switchTitle": "Switch to user view", + "switchHint": "Tasks will be filtered to show only assignments for selected user", + "chooseUser": "Choose User", + "changeUser": "Change User" + }, + "modals": { + "changeDueDate": "Change due date", + "completePast": "Save Chore that you completed in the past", + "delegate": "Delegate to someone else", + "selectPerformer": "Select a performer", + "addNote": "Add note to attach to this completion:", + "complete": "Complete" + }, + "shortcuts": { + "allSelectedTitle": "✅ All Tasks Selected", + "someSelectedTitle": "🎯 Tasks Selected", + "allSelectedAltTitle": "🎯 All Tasks Selected" + }, + "overview": { + "title": "Chores Overviews", + "search": "Search", + "newChore": "New Chore", + "unassigned": "Unassigned", + "changeDueDate": "Change due date" + }, + "dataError": { + "attachmentsFailed": "Failed to fetch attachments" + }, + "nudge": { + "title": "Send Nudge", + "customMessage": "Custom Message (optional)", + "messagePlaceholder": "Add a personal message with your nudge...", + "notifyAll": "Notify All Assignees", + "notifyAllHint": "If enabled, all members who can see this task will be notified. Otherwise, only the assigned person will receive the nudge." + }, + "nfc": { + "errWrite": "Error writing to NFC tag. Please try again.", + "errUnsupported": "NFC is not supported by this browser. Copy the URL and write it to an NFC tag using a compatible device.", + "titleSuccess": "Tag written!", + "titleError": "Something went wrong", + "titleWaiting": "Hold near NFC tag", + "subSuccess": "Your NFC tag is ready to use.", + "subWaiting": "Keep your device near the tag until complete.", + "subIdle": "Encode this task link onto any NFC tag.", + "tagUrl": "Tag URL", + "copyUrl": "Copy URL", + "autoComplete": "Auto-complete on scan", + "autoCompleteHint": "Mark task done when tag is tapped" + }, + "duePicker": { + "today": "Today", + "tomorrow": "Tomorrow", + "weekend": "Weekend", + "nextWeek": "Next week" + }, + "photoTask": { + "title": "Scan photo to create task", + "cameraUnavailable": "Camera not available", + "uploadPhoto": "Upload Photo", + "scanDocument": "Scan Document", + "capture": "Capture", + "processNatively": "Process Natively", + "processImage": "Process Image", + "createTask": "Create Task", + "failedScan": "Failed scan" + }, + "descriptionPlaceholder": "Enter description...", + "notifTemplate": { + "onDueDate": "On due date", + "beforeDue": "{{count}} {{unit}} before due", + "afterDue": "{{count}} {{unit}} after due", + "errDuplicate": "This notification setting already exists. Please use a different timing.", + "errOneDue": "Only one \"Due Alert\" notification is allowed.", + "errAllConfigured": "All common {{type}} times are already configured.", + "reminder": "Reminder", + "dueAlert": "Due Alert", + "followUp": "Follow-up", + "rememberFuture": "Remember for Future Tasks" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 0cdc241..d95c1d1 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -140,7 +140,14 @@ "tooLargeTitle": "File Too Large", "tooLargeMessage": "The file you are trying to upload is too large.", "deniedTitle": "Permission Denied", - "deniedMessage": "You do not have permission to upload files." + "deniedMessage": "You do not have permission to upload files.", + "plusFeatureTitle": "Plus Feature", + "plusFeatureMessage": "Image uploads are not available in the Basic plan. Upgrade to Plus to add images to your content.", + "upgradeTitle": "Upgrade Required", + "upgradeMessage": "Image uploads are only available for Plus accounts.", + "failedTitle": "Upload Failed", + "failedMessage": "Failed to upload image.", + "processingMessage": "An error occurred while processing the image." }, "getStarted": "Get Started!", "imageLoadFailed": "Failed to load image.", @@ -163,5 +170,44 @@ "showDetails": "Show error details", "copyToClipboard": "Copy to clipboard", "copied": "Error details copied to clipboard" - } + }, + "userMenu": { + "actAsAnother": "Act as another user", + "stopImpersonating": "Stop Impersonating", + "returnToAccount": "Return to your account", + "accountPrefs": "Account & preferences", + "invitePeople": "Invite People", + "addMembers": "Add members to your circle", + "sidePanelSettings": "Side Panel Settings" + }, + "realtime": { + "newTaskTitle": "New Task Created", + "connected": "You are now receiving real-time as they happen.", + "errorTitle": "Real-time Error", + "parseTitle": "Message Error", + "parseMessage": "Failed to parse server message", + "disabledTitle": "Connection Temporarily Disabled", + "failedTitle": "Connection Failed", + "stateConnectionError": "Connection error occurred", + "stateAuthExpired": "Authentication expired - refreshing token...", + "stateTokenRefreshed": "Token refreshed - reconnecting...", + "stateRefreshInProgress": "Token refresh in progress - reconnecting soon...", + "stateSessionExpired": "Session expired - please log in again", + "stateAuthFailed": "Authentication failed - please log in again", + "stateAuthError": "Authentication error - please log in again", + "stateTimeout": "Connection timeout - reconnecting...", + "connectErrorTitle": "Connection Error", + "connectErrorMessage": "Failed to establish real-time connection. Please try again." + }, + "removeAll": "Remove all", + "apply": "Apply", + "cancelAll": "Cancel all", + "sync": { + "aria": "Open sync and network status", + "allSynced": "All changes synced", + "willSync": "Will sync when back online", + "cancelAll": "Cancel All" + }, + "attachments": "Attachments", + "fileNumbered": "File {{index}}" } diff --git a/public/locales/en/filters.json b/public/locales/en/filters.json index 6b10047..973cf6f 100644 --- a/public/locales/en/filters.json +++ b/public/locales/en/filters.json @@ -13,5 +13,11 @@ "nameExists": "A filter with this name already exists", "tasks_one": "{{count}} task", "tasks_other": "{{count}} tasks", - "overdue": "{{count}} overdue" + "overdue": "{{count}} overdue", + "delete": { + "title": "Delete Filter" + }, + "empty": { + "title": "No saved filters yet" + } } diff --git a/public/locales/en/history.json b/public/locales/en/history.json index 1452849..9d4b256 100644 --- a/public/locales/en/history.json +++ b/public/locales/en/history.json @@ -76,7 +76,9 @@ "hasNotes": "Has Notes", "hasPoints": "Has Points", "timePeriod": "Time Period", - "user": "User" + "user": "User", + "completedBy": "Completed By", + "completedAt": "Completed At" }, "period": { "days_one": "{{count}} Day", @@ -85,5 +87,26 @@ }, "activities": { "title": "Activities" + }, + "title": { + "summary": "Task Summary", + "activity": "Task Activity" + }, + "noResults": { + "clear": "Clear filters" + }, + "toast": { + "updateQueued": { + "title": "History Update Queued" + }, + "updated": { + "title": "History Updated" + }, + "deleteQueued": { + "title": "History Delete Queued" + }, + "deleted": { + "title": "History Deleted" + } } } diff --git a/public/locales/en/labels.json b/public/locales/en/labels.json index 47a296c..0d76f91 100644 --- a/public/locales/en/labels.json +++ b/public/locales/en/labels.json @@ -35,5 +35,13 @@ "notFoundDescription": "This label may have been deleted or is no longer shared with you.", "backToLabels": "Back to labels" }, - "blurb": "Manage your labels and organize your tasks effectively. Labels will be automatically shared with your circle if they are used on a shared task." + "blurb": "Manage your labels and organize your tasks effectively. Labels will be automatically shared with your circle if they are used on a shared task.", + "modal": { + "errorEmptyName": "Name cannot be empty", + "errorDuplicate": "Label with this name already exists", + "errorNoColor": "Please select a color", + "saveFailedTitle": "Failed to save label", + "saveFailedMessage": "Unable to save label. Please try again.", + "name": "Name" + } } diff --git a/public/locales/en/projects.json b/public/locales/en/projects.json index 19c9c9e..d9c34d8 100644 --- a/public/locales/en/projects.json +++ b/public/locales/en/projects.json @@ -28,5 +28,12 @@ "createNewDescription": "Add a custom project workspace", "manage": "Manage Projects", "manageDescription": "View, edit, and organize all projects" + }, + "modal": { + "errorNameRequired": "Project name is required", + "errorUpdate": "Failed to update project", + "errorCreate": "Failed to create project", + "namePlaceholder": "Enter project name...", + "descriptionPlaceholder": "Optional project description..." } } diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 44c90d3..ead40df 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -352,7 +352,8 @@ "backupCodesModal": { "title": "New Backup Codes", "warning": "Your previous backup codes are now invalid. Save these new codes in a safe place. Each code can only be used once." - } + }, + "generateCodes": "Generate New Codes" }, "apiTokens": { "title": "API Tokens", @@ -576,5 +577,8 @@ "selectOwner": "Select new owner", "confirmPrompt": "Please enter your password and type DELETE to confirm", "typeDelete": "DELETE" + }, + "realtime": { + "titleSse": "Real-time Updates (SSE)" } } diff --git a/public/locales/en/timer.json b/public/locales/en/timer.json index 848fd53..a448c49 100644 --- a/public/locales/en/timer.json +++ b/public/locales/en/timer.json @@ -9,7 +9,16 @@ "sessionDeletedMessage": "Timer session has been deleted successfully.", "sessionDeleteErrorTitle": "Error deleting session", "deleteConfirmTitle": "Delete Timer Session", - "deleteConfirmMessage": "Are you sure you want to delete this timer session?" + "deleteConfirmMessage": "Are you sure you want to delete this timer session?", + "startedTitle": "Timer Started", + "startedMessage": "Work session has been started successfully.", + "startQueuedTitle": "Start queued", + "startFailTitle": "Failed to start timer", + "pausedTitle": "Timer Paused", + "pausedMessage": "Work session has been paused.", + "pauseQueuedTitle": "Pause queued", + "pauseFailTitle": "Failed to pause timer", + "deleteSessionTitle": "Delete Session" }, "loading": "Loading timer data...", "noData": "No timer data found for this chore.", @@ -25,5 +34,6 @@ "startTime": "Start Time", "endTime": "End Time", "leaveEmpty": "Leave empty if session is ongoing", - "noSessionForChore": "No timer session found for this chore." + "noSessionForChore": "No timer session found for this chore.", + "saveChanges": "Save Changes" } diff --git a/src/components/NotificationTemplate.jsx b/src/components/NotificationTemplate.jsx index 73013b6..d942681 100644 --- a/src/components/NotificationTemplate.jsx +++ b/src/components/NotificationTemplate.jsx @@ -13,26 +13,29 @@ import Option from '@mui/joy/Option' import Select from '@mui/joy/Select' import Typography from '@mui/joy/Typography' import { useCallback, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors' import { TIME_UNITS } from '../utils/DurationUtils' const timeUnits = TIME_UNITS const timingOptions = [ - { label: 'Before', value: 'before' }, - { label: 'Due', value: 'ondue' }, - { label: 'After', value: 'after' }, + { value: 'before' }, + { value: 'ondue' }, + { value: 'after' }, ] -function getRelativeLabel(notification) { +function getRelativeLabel(notification, t) { const { value, unit } = notification const numericValue = Number(value) if (numericValue === 0) { - return 'On due date' + return t('notifTemplate.onDueDate') } - const unitName = unit === 'm' ? 'minutes' : unit === 'h' ? 'hours' : 'days' + const unitName = t(`notifTemplate.unitName.${unit}`) const absValue = Math.abs(numericValue) - return `${absValue} ${unitName} ${numericValue < 0 ? 'before' : 'after'} due` + return numericValue < 0 + ? t('notifTemplate.beforeDue', { count: absValue, unit: unitName }) + : t('notifTemplate.afterDue', { count: absValue, unit: unitName }) } // Helper functions to convert between internal value and UI representation @@ -71,6 +74,7 @@ const NotificationTemplate = ({ value, showTimeline = true, }) => { + const { t } = useTranslation('chores') const [notifications, setNotifications] = useState( value?.templates || JSON.parse(localStorage.getItem('defaultNotificationTemplate')) || @@ -218,9 +222,7 @@ const NotificationTemplate = ({ if (!currentNotification) return if (isDuplicate(currentNotification, idx, currentList)) { - setError( - 'This notification setting already exists. Please use a different timing.', - ) + setError(t('notifTemplate.errDuplicate')) return } } @@ -232,14 +234,14 @@ const NotificationTemplate = ({ if (type === 'due') { if (notificationsRef.current.some(n => Number(n.value) === 0)) { - setError('Only one "Due Alert" notification is allowed.') + setError(t('notifTemplate.errOneDue')) return } newNotification = { value: 0, unit: 'm' } } else { newNotification = getSmartSuggestion(type) if (!newNotification) { - setError(`All common ${type} times are already configured.`) + setError(t('notifTemplate.errAllConfigured', { type })) return } } @@ -364,7 +366,7 @@ const NotificationTemplate = ({ fontSize: '0.6rem', }} > - Due Date + {t('group.dueDate')} @@ -398,7 +400,7 @@ const NotificationTemplate = ({ zIndex: 10, }, }} - title={getRelativeLabel(n)} + title={getRelativeLabel(n, t)} > - {getRelativeLabel(n)} + {getRelativeLabel(n, t)} @@ -575,7 +577,7 @@ const NotificationTemplate = ({ value={opt.value} disabled={opt.value === 'ondue' && hasOnDueElsewhere} > - {opt.label} + {t(`notifTemplate.timing.${opt.value}`)} ))} @@ -638,7 +640,7 @@ const NotificationTemplate = ({ > {timeUnits.map(opt => ( ))} @@ -688,7 +690,7 @@ const NotificationTemplate = ({ }, }} > - Reminder + {t('notifTemplate.reminder')} {showSaveDefault && ( @@ -762,7 +764,7 @@ const NotificationTemplate = ({ setShowSaveDefault(false) }} > - Remember for Future Tasks + {t('notifTemplate.rememberFuture')} )} diff --git a/src/components/SSESettings.jsx b/src/components/SSESettings.jsx index d9623ad..3187bdc 100644 --- a/src/components/SSESettings.jsx +++ b/src/components/SSESettings.jsx @@ -13,8 +13,10 @@ import { useSSEContext } from '../hooks/useSSEContext' import { useUserProfile } from '../queries/UserQueries' import { isPlusAccount } from '../utils/Helpers' import SSEConnectionStatus from './SSEConnectionStatus' +import { useTranslation } from 'react-i18next' const SSESettings = () => { + const { t } = useTranslation('settings') const { data: userProfile } = useUserProfile() const { isConnected, @@ -75,7 +77,7 @@ const SSESettings = () => { )} - Real-time Updates (SSE) + {t('realtime.titleSse')} {!isPlusAccount(userProfile) && ( Plus Feature diff --git a/src/components/UserProfileAvatar.jsx b/src/components/UserProfileAvatar.jsx index e306c99..74b43e3 100644 --- a/src/components/UserProfileAvatar.jsx +++ b/src/components/UserProfileAvatar.jsx @@ -35,8 +35,10 @@ import { apiClient } from '../utils/ApiClient' import { isPlusAccount, resolvePhotoURL } from '../utils/Helpers' import UserModal from '../views/Modals/Inputs/UserModal' import SubscriptionModal from './SubscriptionModal' +import { useTranslation } from 'react-i18next' const UserProfileAvatar = () => { + const { t } = useTranslation('common') const navigate = useNavigate() const { mode, setMode } = useColorScheme() const { data: userProfile } = useUserProfile() @@ -297,7 +299,7 @@ const UserProfileAvatar = () => { level='body-xs' sx={{ color: 'var(--joy-palette-text-tertiary)' }} > - Act as another user + {t('userMenu.actAsAnother')} @@ -319,13 +321,13 @@ const UserProfileAvatar = () => { - Stop Impersonating + {t('userMenu.stopImpersonating')} - Return to your account + {t('userMenu.returnToAccount')} @@ -349,13 +351,13 @@ const UserProfileAvatar = () => { - Settings + {t('settings')} - Account & preferences + {t('userMenu.accountPrefs')} @@ -374,13 +376,13 @@ const UserProfileAvatar = () => { - Invite People + {t('userMenu.invitePeople')} - Add members to your circle + {t('userMenu.addMembers')} @@ -401,7 +403,7 @@ const UserProfileAvatar = () => { - Side Panel Settings + {t('userMenu.sidePanelSettings')} { + const { t } = useTranslation('common') const { isAuthenticated, token } = useAuth() // Only fetch user profile if authenticated - prevents unnecessary API calls on landing page const { data: userProfile } = useUserProfile() @@ -97,7 +99,7 @@ export const useSSE = () => { case 'chore.created': showNotification({ type: 'info', - title: 'New Task Created', + title: t('realtime.newTaskTitle'), message: `${eventData.data.user.displayName} created "${eventData.data.chore.name}"`, duration: 5000, }) @@ -242,14 +244,14 @@ export const useSSE = () => { showAlert({ type: 'success', color: 'success', - message: 'You are now receiving real-time as they happen.', + message: t('realtime.connected'), }) break case 'error': console.error('SSE error event:', eventData.data) showError({ - title: 'Real-time Error', + title: t('realtime.errorTitle'), message: eventData.data.message || 'An error occurred with real-time updates', @@ -262,8 +264,8 @@ export const useSSE = () => { } catch (err) { console.error('Failed to parse SSE message:', err) showError({ - title: 'Message Error', - message: 'Failed to parse server message', + title: t('realtime.parseTitle'), + message: t('realtime.parseMessage'), }) return // Stop processing if JSON parsing fails } @@ -315,7 +317,7 @@ export const useSSE = () => { if (isCircuitBreakerOpen) { console.log('SSE: Circuit breaker is open, preventing connection attempt') showError({ - title: 'Connection Temporarily Disabled', + title: t('realtime.disabledTitle'), message: 'Connection blocked due to repeated failures. Please try again later.', }) @@ -328,7 +330,7 @@ export const useSSE = () => { ) setIsCircuitBreakerOpen(true) showError({ - title: 'Connection Failed', + title: t('realtime.failedTitle'), message: 'Maximum connection attempts reached. SSE disabled for 10 minutes.', }) @@ -378,7 +380,7 @@ export const useSSE = () => { const ticket = await fetchSSETicket() if (!ticket) { console.error('SSE: Failed to obtain connection ticket') - setError('Connection error occurred') + setError(t('realtime.stateConnectionError')) setConnectionState(SSE_STATES.CLOSED) scheduleReconnect( RECONNECT_INTERVALS[ @@ -511,7 +513,7 @@ export const useSSE = () => { if (is401Error) { console.log('SSE 401 error detected, attempting token refresh...') - setError('Authentication expired - refreshing token...') + setError(t('realtime.stateAuthExpired')) try { const refreshResult = await apiClient.refreshToken() @@ -520,7 +522,7 @@ export const useSSE = () => { console.log( 'Token refreshed successfully, retrying SSE connection...', ) - setError('Token refreshed - reconnecting...') + setError(t('realtime.stateTokenRefreshed')) if (apiClient.failedQueue && apiClient.failedQueue.length > 0) { console.log( @@ -553,7 +555,7 @@ export const useSSE = () => { console.log( 'SSE: Token refresh in progress by another request, waiting...', ) - setError('Token refresh in progress - reconnecting soon...') + setError(t('realtime.stateRefreshInProgress')) reconnectAttemptsRef.current = 0 @@ -572,23 +574,23 @@ export const useSSE = () => { return } else if (refreshResult.error === 'Refresh token expired') { console.error('Refresh token expired, user must login again') - setError('Session expired - please log in again') + setError(t('realtime.stateSessionExpired')) return } else { console.error('Token refresh failed:', refreshResult.error) - setError('Authentication failed - please log in again') + setError(t('realtime.stateAuthFailed')) return } } catch (refreshError) { console.error('Token refresh error:', refreshError) - setError('Authentication error - please log in again') + setError(t('realtime.stateAuthError')) return } } else if (isTimeoutError) { console.log('SSE timeout detected, attempting reconnection...') - setError('Connection timeout - reconnecting...') + setError(t('realtime.stateTimeout')) } else { - setError('Connection error occurred') + setError(t('realtime.stateConnectionError')) } // Schedule reconnect for non-401 errors @@ -618,8 +620,8 @@ export const useSSE = () => { } catch (err) { console.error('Failed to create SSE connection:', err) showError({ - title: 'Connection Error', - message: 'Failed to establish real-time connection. Please try again.', + title: t('realtime.connectErrorTitle'), + message: t('realtime.connectErrorMessage'), }) setConnectionState(SSE_STATES.CLOSED) } diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx index 216055c..e8b49b7 100644 --- a/src/views/ChoreEdit/ChoreView.jsx +++ b/src/views/ChoreEdit/ChoreView.jsx @@ -293,7 +293,7 @@ const ChoreView = () => { message: t('choreView.taskCompletionUndone'), }) } else { - throw new Error('Failed to undo') + throw new Error(t('choreView.unableUndo')) } } catch (error) { showError({ @@ -331,7 +331,7 @@ const ChoreView = () => { }) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) showSuccess({ - message: "You're offline — completion will sync when back online", + message: t('choreView.offlineComplete'), undoAction: async () => { await commandQueue.cancel(cmdId) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) @@ -340,7 +340,7 @@ const ChoreView = () => { } else { showError({ title: t('choreView.undoFailed'), - message: error?.message || 'Unable to complete task', + message: error?.message || t('choreView.unableComplete'), }) } } @@ -369,7 +369,7 @@ const ChoreView = () => { message: t('choreView.taskSkipUndone'), }) } else { - throw new Error('Failed to undo') + throw new Error(t('choreView.unableUndo')) } } catch (error) { showError({ @@ -389,7 +389,7 @@ const ChoreView = () => { ) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) showSuccess({ - message: "You're offline — skip will sync when back online", + message: t('choreView.offlineSkip'), undoAction: async () => { await commandQueue.cancel(cmdId) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) @@ -398,7 +398,7 @@ const ChoreView = () => { } else { showError({ title: t('choreView.undoFailed'), - message: error?.message || 'Unable to skip task', + message: error?.message || t('choreView.unableSkip'), }) } } @@ -424,7 +424,7 @@ const ChoreView = () => { setChore(startedChore) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) showSuccess({ - message: "You're offline — start will sync when back online", + message: t('choreView.offlineStart'), undoAction: async () => { await commandQueue.cancel(cmdId) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) @@ -436,7 +436,7 @@ const ChoreView = () => { showError({ title: t('choreView.undoFailed'), - message: error?.message || 'Unable to start task', + message: error?.message || t('choreView.unableStart'), }) }, }) @@ -463,7 +463,7 @@ const ChoreView = () => { setChore(pausedChore) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) showSuccess({ - message: "You're offline — pause will sync when back online", + message: t('choreView.offlinePause'), undoAction: async () => { await commandQueue.cancel(cmdId) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) @@ -475,7 +475,7 @@ const ChoreView = () => { showError({ title: t('choreView.undoFailed'), - message: error?.message || 'Unable to pause task', + message: error?.message || t('choreView.unablePause'), }) }, }) @@ -579,7 +579,7 @@ const ChoreView = () => { setChore({ ...chore, isActive: true }) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) showSuccess({ - message: "You're offline — restore will sync when back online", + message: t('choreView.offlineRestore'), undoAction: async () => { await commandQueue.cancel(cmdId) await offlineDB.saveChores([{ ...chore, isActive: false }]) @@ -589,8 +589,8 @@ const ChoreView = () => { }) } else { showError({ - title: 'Failed to restore', - message: error.message || 'Unable to restore task', + title: t('choreView.restoreFailed'), + message: error.message || t('choreView.unableRestore'), }) } } diff --git a/src/views/ChoreEdit/ThingTriggerSection.jsx b/src/views/ChoreEdit/ThingTriggerSection.jsx index e4c4d24..3636372 100644 --- a/src/views/ChoreEdit/ThingTriggerSection.jsx +++ b/src/views/ChoreEdit/ThingTriggerSection.jsx @@ -14,6 +14,7 @@ import { } from '@mui/joy' import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' const isValidTrigger = (thing, condition, triggerState) => { const newErrors = {} if (!thing || !triggerState) { @@ -54,6 +55,7 @@ const ThingTriggerSection = ({ selected, isAttepmtingToSave, }) => { + const { t } = useTranslation('chores') const [selectedThing, setSelectedThing] = useState(null) const [condition, setCondition] = useState(null) const [triggerState, setTriggerState] = useState(null) @@ -85,7 +87,7 @@ const ThingTriggerSection = ({ return ( - Trigger a task when a thing state changes to a desired state + {t('thing.triggerHint')} {things?.length === 0 && ( @@ -135,7 +137,7 @@ const ThingTriggerSection = ({ )} renderInput={params => ( - + )} /> @@ -215,7 +217,7 @@ const ThingTriggerSection = ({ setTriggerState(e.target.value)} - label='Enter the text to trigger the task' + label={t('thing.enterText')} /> )} diff --git a/src/views/Chores/ActivitesCard.jsx b/src/views/Chores/ActivitesCard.jsx index 5108633..f3088b7 100644 --- a/src/views/Chores/ActivitesCard.jsx +++ b/src/views/Chores/ActivitesCard.jsx @@ -31,8 +31,10 @@ import { useChores, useChoresHistory } from '../../queries/ChoreQueries' import { useCircleMembers } from '../../queries/UserQueries' import { resolvePhotoURL } from '../../utils/Helpers' import NoteViewerModal from '../Modals/Inputs/NoteViewerModal' +import { useTranslation } from 'react-i18next' const ActivityItem = ({ activity, members, onViewNote }) => { + const { t } = useTranslation('chores') // Find the member who completed the activity const completedByMember = members?.find( member => member.userId === activity.completedBy, @@ -243,7 +245,7 @@ const ActivityItem = ({ activity, members, onViewNote }) => { display: 'inline-block', }} > - Show more + {t('activity.showMore')} )} @@ -272,7 +274,7 @@ const groupActivitiesByDate = activities => { return groups } -const ActivitiesCard = ({ title = 'Recent Activities' }) => { +const ActivitiesCard = ({ title = t('activity.title') }) => { const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false }) // Use hooks to fetch data @@ -332,7 +334,7 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => { }} > - Loading activities... + {t('activity.loading')} diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx index b4363e2..dfdefc2 100644 --- a/src/views/Chores/ArchivedTasks.jsx +++ b/src/views/Chores/ArchivedTasks.jsx @@ -29,6 +29,7 @@ import { import { useQueryClient } from '@tanstack/react-query' import Fuse from 'fuse.js' import { useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import EmptyState from '../../components/common/EmptyState' @@ -95,6 +96,7 @@ const applyPendingArchivedState = async chores => { } const ArchivedTasks = () => { + const { t } = useTranslation('chores') const { data: userProfile, isLoading: isUserProfileLoading } = useUserProfile() const { showError, showSuccess } = useNotification() @@ -135,7 +137,7 @@ const ArchivedTasks = () => { () => [ { id: 'assignee', - label: 'Assignee', + label: t('toolbar.type.assignee'), type: 'multi-select', icon: , options: performers.map(p => ({ @@ -147,7 +149,7 @@ const ArchivedTasks = () => { }, { id: 'priority', - label: 'Priority', + label: t('priority'), type: 'multi-select', icon: , options: Priorities.map(p => ({ @@ -162,7 +164,7 @@ const ArchivedTasks = () => { ? [ { id: 'label', - label: 'Labels', + label: t('labels.label'), type: 'multi-select', icon: