diff --git a/public/locales/en/chores.json b/public/locales/en/chores.json index f80f09c..31db9aa 100644 --- a/public/locales/en/chores.json +++ b/public/locales/en/chores.json @@ -85,9 +85,120 @@ "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" }, "sort": { - "assignedToMe": "Assigned to me" + "assignedToMe": "Assigned to me", + "smart": "Smart" + }, + "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" + }, + "insights": { + "highPriority": "High Priority" + }, + "labels": { + "label": "Labels" + }, + "actionMenu": { + "view": "View", + "archive": "Archive" + }, + "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" + }, + "edit": { + "deleteConfirm": "Are you sure you want to delete this chore?" + }, + "list": { + "complete": "Complete" + }, + "multiToolbar": { + "skip": "Skip" + }, + "remind": { + "title": "Reminders" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index af1f291..6f55fb1 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -154,5 +154,42 @@ "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" } } 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 93f96cb..165851b 100644 --- a/public/locales/en/labels.json +++ b/public/locales/en/labels.json @@ -5,5 +5,13 @@ "message": "Are you sure you want to delete this label? This will remove the label from all tasks." }, "loadError": "Failed to load labels. Please try again.", - "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 0e7eed4..276d267 100644 --- a/public/locales/en/projects.json +++ b/public/locales/en/projects.json @@ -20,5 +20,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 cc7f123..ead40df 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -577,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/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/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/SmartInsightsCard.jsx b/src/views/Chores/SmartInsightsCard.jsx index be316d0..81e1eb6 100644 --- a/src/views/Chores/SmartInsightsCard.jsx +++ b/src/views/Chores/SmartInsightsCard.jsx @@ -9,6 +9,7 @@ import { import { Box, Button, Chip, Sheet, Typography } from '@mui/joy' import { useMemo } from 'react' import { TASK_COLOR } from '../../utils/Colors' +import { useTranslation } from 'react-i18next' // Static insight filter definitions – used for URL restoration export const INSIGHT_FILTER_DEFS = { @@ -62,6 +63,7 @@ const SmartInsightsCard = ({ clearTempFilter, tempFilter, }) => { + const { t } = useTranslation('chores') // Detect all possible insights from chores const insights = useMemo(() => { if (!chores || chores.length === 0) return [] @@ -84,7 +86,7 @@ const SmartInsightsCard = ({ id: 'overdue', priority: 1, count: overdueTasks.length, - title: 'Overdue', + title: t('group.overdue'), description: `${overdueTasks.length} ${overdueTasks.length === 1 ? 'task is' : 'tasks are'} overdue`, color: 'danger', bgColor: TASK_COLOR.OVERDUE, @@ -113,7 +115,7 @@ const SmartInsightsCard = ({ id: 'due-today', priority: 2, count: dueTodayTasks.length, - title: 'Due Today', + title: t('group.dueToday'), description: `${dueTodayTasks.length} ${dueTodayTasks.length === 1 ? 'task' : 'tasks'} due by end of day`, color: 'warning', bgColor: '#FFA500', @@ -138,7 +140,7 @@ const SmartInsightsCard = ({ id: 'pending-approval', priority: 3, count: pendingApprovalTasks.length, - title: 'Pending Approval', + title: t('group.pendingApproval'), description: `${pendingApprovalTasks.length} ${pendingApprovalTasks.length === 1 ? 'task awaits' : 'tasks await'} approval`, color: 'neutral', bgColor: TASK_COLOR.PENDING_REVIEW, @@ -167,7 +169,7 @@ const SmartInsightsCard = ({ id: 'due-this-week', priority: 4, count: dueThisWeekTasks.length, - title: 'Due This Week', + title: t('group.dueThisWeek'), description: `${dueThisWeekTasks.length} ${dueThisWeekTasks.length === 1 ? 'task' : 'tasks'} due in the next 7 days`, color: 'primary', bgColor: TASK_COLOR.IN_PROGRESS, @@ -194,7 +196,7 @@ const SmartInsightsCard = ({ id: 'high-priority', priority: 5, count: highPriorityTasks.length, - title: 'High Priority', + title: t('insights.highPriority'), description: `${highPriorityTasks.length} ${highPriorityTasks.length === 1 ? 'task requires' : 'tasks require'} immediate attention`, color: 'warning', bgColor: '#FF6B6B', @@ -221,7 +223,7 @@ const SmartInsightsCard = ({ id: 'no-due-date', priority: 6, count: noDueDateTasks.length, - title: 'No Due Date', + title: t('group.noDueDate'), description: `${noDueDateTasks.length} ${noDueDateTasks.length === 1 ? 'task needs' : 'tasks need'} a deadline`, color: 'neutral', bgColor: '#9E9E9E', diff --git a/src/views/Chores/components/ChoreToolbarPrototype.jsx b/src/views/Chores/components/ChoreToolbarPrototype.jsx index cd4e28d..c64aa65 100644 --- a/src/views/Chores/components/ChoreToolbarPrototype.jsx +++ b/src/views/Chores/components/ChoreToolbarPrototype.jsx @@ -63,6 +63,7 @@ import FilterBuilderContent, { import SearchBar from './SearchBar' import ProjectSelector from '../../components/ProjectSelector' import CustomFilterChips from './CustomFilterChips' +import { useTranslation } from 'react-i18next' // ─── sub-components for the Display sheet ──────────────────────────────────── @@ -225,6 +226,7 @@ const ChoreToolbar = ({ searchInputRef, showKeyboardShortcuts, }) => { + const { t } = useTranslation('chores') const [filterSheetOpen, setFilterSheetOpen] = useState(false) const [displaySheetOpen, setDisplaySheetOpen] = useState(false) const [localSelections, setLocalSelections] = useState(defaultSelections()) @@ -253,7 +255,7 @@ const ChoreToolbar = ({ createdBy: 'Created By', status: 'Status', priority: 'Priority', - label: 'Labels', + label: t('labels.label'), project: 'Project', dueDate: 'Due Date', points: 'Points', @@ -513,33 +515,33 @@ const ChoreToolbar = ({ const filterActive = activeFilterId != null || tempConditionCount > 0 const groupByOptions = [ - { value: 'default', label: 'Smart' }, - { value: 'due_date', label: 'Due Date' }, - { value: 'priority', label: 'Priority' }, - { value: 'labels', label: 'Labels' }, + { value: 'default', label: t('sort.smart') }, + { value: 'due_date', label: t('dueDate') }, + { value: 'priority', label: t('priority') }, + { value: 'labels', label: t('labels.label') }, ] const assigneeOptions = [ - { value: 'anyone', label: 'Everyone' }, - { value: 'assigned_to_me', label: 'Mine' }, - { value: 'available_for_me', label: 'Available to me' }, - { value: 'assigned_to_others', label: 'Others' }, + { value: 'anyone', label: t('toolbar.everyone') }, + { value: 'assigned_to_me', label: t('toolbar.mine') }, + { value: 'available_for_me', label: t('toolbar.availableToMe') }, + { value: 'assigned_to_others', label: t('toolbar.others') }, ] const viewOptions = [ { value: 'default', - label: 'Cards', + label: t('toolbar.cards'), icon: , }, { value: 'compact', - label: 'Compact', + label: t('toolbar.compact'), icon: , }, { value: 'calendar', - label: 'Calendar', + label: t('toolbar.calendar'), icon: , }, ] @@ -577,8 +579,8 @@ const ChoreToolbar = ({ size='sm' sx={{ height: 32, width: 32, borderRadius: '50%' }} onClick={openFilterSheet} - aria-label='Filters' - title='Filters' + aria-label={t('toolbar.filters')} + title={t('toolbar.filters')} > @@ -602,7 +604,7 @@ const ChoreToolbar = ({ sx={{ height: 32, width: 32, borderRadius: '50%' }} onClick={() => setDisplaySheetOpen(true)} aria-label='View and group options' - title='View & Group' + title={t('toolbar.viewGroup')} > {viewMode === 'calendar' ? ( @@ -708,7 +710,7 @@ const ChoreToolbar = ({ > setSaveFilterName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSaveFilter()} @@ -720,7 +722,7 @@ const ChoreToolbar = ({ onClick={handleSaveFilter} disabled={!saveFilterName.trim()} > - Save + {t('common:save')} ) : ( @@ -752,7 +754,7 @@ const ChoreToolbar = ({ setFilterSheetOpen(false) }} > - Clear all + {t('toolbar.clearAll')} {activeConditions.length > 0 ? ( @@ -784,7 +786,7 @@ const ChoreToolbar = ({ disabled={!editingSavedFilter} > - Save Filter + {t('toolbar.saveFilter')} { @@ -798,7 +800,7 @@ const ChoreToolbar = ({ }} > - Save as New Filter + {t('toolbar.saveAsNew')} @@ -812,7 +814,7 @@ const ChoreToolbar = ({ }} sx={{ minWidth: 140 }} > - Done + {t('activity.status.done')} )} @@ -893,13 +895,13 @@ const ChoreToolbar = ({ onClick={() => setDisplaySheetOpen(false)} sx={{ minWidth: 140 }} > - Done + {t('activity.status.done')} } > {/* View section */} - + {viewOptions.map(opt => ( } - label='Group by' + label={t('toolbar.groupBy')} badge={ selectedGroupBy !== 'default' ? groupByOptions.find(o => o.value === selectedGroupBy)?.label @@ -963,7 +965,7 @@ const ChoreToolbar = ({ } - label='Show tasks for' + label={t('toolbar.showTasksFor')} badge={ selectedAssigneeFilter !== 'anyone' ? assigneeOptions.find(o => o.value === selectedAssigneeFilter) diff --git a/src/views/Chores/hooks/useChoreActions.js b/src/views/Chores/hooks/useChoreActions.js index 55d9a2c..f4b27f0 100644 --- a/src/views/Chores/hooks/useChoreActions.js +++ b/src/views/Chores/hooks/useChoreActions.js @@ -20,6 +20,7 @@ import { } from '../../../utils/Fetcher' import { offlineDB } from '../../../utils/OfflineDB' import { isOfflineFeatureEnabled } from '../../../utils/OfflineFeatureToggle' +import { useTranslation } from 'react-i18next' // Effectively "can this action be queued offline?" β€” requires the offline // feature, otherwise there is no command queue to replay it later. @@ -47,6 +48,7 @@ export const useChoreActions = ({ getSelectedChoresData, clearSelection, }) => { + const { t } = useTranslation('chores') const queryClient = useQueryClient() const archiveChore = useArchiveChore() const unarchiveChore = useUnArchiveChore() @@ -102,16 +104,16 @@ export const useChoreActions = ({ skipped: 'Task skip has been undone.', } showUndo({ - title: 'Undo Successful', + title: t('choreView.undoSuccessful'), message: undoMessages[event], }) } else { - throw new Error('Failed to undo') + throw new Error(t('choreView.unableUndo')) } } catch (error) { showError({ - title: 'Undo Failed', - message: 'Unable to undo the action. Please try again.', + title: t('choreView.undoFailed'), + message: t('choreView.undoFailedMessage'), }) } }, @@ -122,39 +124,39 @@ export const useChoreActions = ({ const notifications = { rescheduled: { type: 'success', - title: 'Task Rescheduled', - message: 'The task due date has been updated successfully.', + title: t('actions.rescheduledTitle'), + message: t('actions.rescheduledMessage'), }, 'due-date-removed': { type: 'success', - title: 'Task Unplanned', - message: 'The task is now unplanned and has no due date.', + title: t('actions.unplannedTitle'), + message: t('actions.unplannedMessage'), }, unarchive: { type: 'success', - title: 'Task Restored', - message: 'The task has been restored and is now active.', + title: t('archived.restoredTitle'), + message: t('archived.restoredMsg'), }, archive: { type: 'success', - title: 'Task Archived', + title: t('actions.archivedTitle'), message: 'The task has been archived and hidden from the active list.', }, started: { type: 'success', - title: 'Task Started', - message: 'The task has been marked as started.', + title: t('actions.startedTitle'), + message: t('actions.startedMessage'), }, paused: { type: 'warning', - title: 'Task Paused', - message: 'The task has been paused.', + title: t('actions.pausedTitle'), + message: t('actions.pausedMessage'), }, deleted: { type: 'success', - title: 'Task Deleted', - message: 'The task has been deleted.', + title: t('archived.deletedTitle'), + message: t('actions.deletedMessage'), }, } @@ -203,21 +205,21 @@ export const useChoreActions = ({ } }) showSuccess({ - message: 'Task completed', + message: t('actions.undoable.completed'), undoAction: async () => { try { const undoResponse = await UndoChoreAction(chore.id) if (undoResponse.ok) { queryClient.invalidateQueries(['chores']) showUndo({ - title: 'Undo Successful', - message: 'Task completion has been undone.', + title: t('choreView.undoSuccessful'), + message: t('choreView.taskCompletionUndone'), }) - } else throw new Error('Failed to undo') + } else throw new Error(t('choreView.unableUndo')) } catch { showError({ - title: 'Undo Failed', - message: 'Unable to undo the action. Please try again.', + title: t('choreView.undoFailed'), + message: t('choreView.undoFailedMessage'), }) } }, @@ -254,7 +256,7 @@ export const useChoreActions = ({ }) queryClient.invalidateQueries({ queryKey: ['pendingCommands'] }) showSuccess({ - title: 'Task completion pending', + title: t('actions.completionPending'), message: "You're offline β€” completion will sync when back online", undoAction: async () => { @@ -266,7 +268,7 @@ export const useChoreActions = ({ }) } else { showError({ - title: 'Failed to complete', + title: t('actions.failCompleteTitle'), message: error?.message || 'Unable to complete chore', }) } @@ -318,7 +320,7 @@ export const useChoreActions = ({ }) } else { showError({ - title: 'Failed to start', + title: t('actions.failStartTitle'), message: error?.message || 'Unable to start chore', }) } @@ -371,7 +373,7 @@ export const useChoreActions = ({ }) } else { showError({ - title: 'Failed to pause', + title: t('actions.failPauseTitle'), message: error?.message || 'Unable to pause chore', }) } @@ -388,7 +390,7 @@ export const useChoreActions = ({ } } catch (error) { showError({ - title: 'Failed to approve', + title: t('actions.failApproveTitle'), message: error.message || 'Unable to approve chore', }) } @@ -403,7 +405,7 @@ export const useChoreActions = ({ } } catch (error) { showError({ - title: 'Failed to reject', + title: t('actions.failRejectTitle'), message: error.message || 'Unable to reject chore', }) } @@ -412,10 +414,10 @@ export const useChoreActions = ({ case 'delete': setConfirmModelConfig({ isOpen: true, - title: 'Delete Chore', - confirmText: 'Delete', - cancelText: 'Cancel', - message: 'Are you sure you want to delete this chore?', + title: t('deleteChore'), + confirmText: t('archived.delete'), + cancelText: t('choreView.cancel'), + message: t('edit.deleteConfirm'), onClose: async isConfirmed => { if (isConfirmed === true) { try { @@ -429,8 +431,8 @@ export const useChoreActions = ({ setFilteredChores(newFilteredChores) queryClient.invalidateQueries(['chores']) showSuccess({ - title: 'Task Deleted', - message: 'The task has been deleted successfully.', + title: t('archived.deletedTitle'), + message: t('actions.deletedMessageLong'), }) } } catch (error) { @@ -461,7 +463,7 @@ export const useChoreActions = ({ }) } else { showError({ - title: 'Failed to delete', + title: t('actions.failDeleteTitle'), message: error?.message || 'Unable to delete chore', }) } @@ -515,7 +517,7 @@ export const useChoreActions = ({ resolve() } else { showError({ - title: 'Failed to archive', + title: t('actions.failArchiveTitle'), message: error.message || 'Unable to archive chore', }) reject(error) @@ -563,7 +565,7 @@ export const useChoreActions = ({ resolve() } else { showError({ - title: 'Failed to restore', + title: t('choreView.restoreFailed'), message: error.message || 'Unable to restore chore', }) reject(error) @@ -604,7 +606,7 @@ export const useChoreActions = ({ }) } else { showError({ - title: 'Failed to skip', + title: t('actions.failSkipTitle'), message: error?.message || 'Unable to skip chore', }) } @@ -679,13 +681,13 @@ export const useChoreActions = ({ if (response.ok) { updateChoreInState(updatedChore, 'moved-to-project') showSuccess({ - title: 'Task Moved', + title: t('actions.movedTitle'), message: `Task moved to ${project?.name || 'Default Project'}.`, }) } } catch (error) { showError({ - title: 'Failed to move task', + title: t('actions.failMoveTitle'), message: error?.message || 'Unable to move task to project', }) } @@ -763,7 +765,7 @@ export const useChoreActions = ({ }) } else { showError({ - title: 'Failed to reschedule', + title: t('actions.failRescheduleTitle'), message: error.message || 'Unable to update due date', }) } @@ -849,15 +851,15 @@ export const useChoreActions = ({ if (response.ok) { const data = await response.json() showSuccess({ - title: 'Nudge Sent!', + title: t('actions.nudgeSentTitle'), message: data.message || 'Nudge sent successfully', }) } else { - throw new Error('Failed to send nudge') + throw new Error(t('actions.nudgeFailed')) } } catch (error) { showError({ - title: 'Failed to Send Nudge', + title: t('actions.failNudgeTitle'), message: error.message || 'Unable to send nudge at this time', }) } finally { @@ -873,9 +875,9 @@ export const useChoreActions = ({ setConfirmModelConfig({ isOpen: true, - title: 'Complete Tasks', - confirmText: 'Complete', - cancelText: 'Cancel', + title: t('actions.bulk.completeTitle'), + confirmText: t('list.complete'), + cancelText: t('choreView.cancel'), message: `Mark ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} as completed?`, onClose: async isConfirmed => { if (isConfirmed === true) { @@ -901,14 +903,14 @@ export const useChoreActions = ({ if (completedTasks.length > 0) { showSuccess({ - title: 'βœ… Tasks Completed', + title: t('actions.bulk.completedTitle'), message: `Successfully completed ${completedTasks.length} task${completedTasks.length > 1 ? 's' : ''}.`, }) } if (failedTasks.length > 0) { showError({ - title: 'Some Tasks Failed', + title: t('archived.someFailedTitle'), message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be completed.`, }) } @@ -917,8 +919,8 @@ export const useChoreActions = ({ clearSelection() } catch (error) { showError({ - title: 'Bulk Complete Failed', - message: 'An unexpected error occurred. Please try again.', + title: t('actions.bulk.completeFailedTitle'), + message: t('archived.unexpectedError'), }) } } @@ -941,9 +943,9 @@ export const useChoreActions = ({ setConfirmModelConfig({ isOpen: true, - title: 'Archive Tasks', - confirmText: 'Archive', - cancelText: 'Cancel', + title: t('actions.bulk.archiveTitle'), + confirmText: t('actionMenu.archive'), + cancelText: t('choreView.cancel'), message: `Archive ${selectedData.length} task${selectedData.length > 1 ? 's' : ''}?`, onClose: async isConfirmed => { if (isConfirmed === true) { @@ -972,13 +974,13 @@ export const useChoreActions = ({ } if (archivedTasks.length > 0) { showSuccess({ - title: 'πŸ“¦ Tasks Archived', + title: t('actions.bulk.archivedTitle'), message: `Successfully archived ${archivedTasks.length} task${archivedTasks.length > 1 ? 's' : ''}.`, }) } if (failedTasks.length > 0) { showError({ - title: 'Some Tasks Failed', + title: t('archived.someFailedTitle'), message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be archived.`, }) } @@ -986,8 +988,8 @@ export const useChoreActions = ({ clearSelection() } catch (error) { showError({ - title: 'Bulk Archive Failed', - message: 'An unexpected error occurred. Please try again.', + title: t('actions.bulk.archiveFailedTitle'), + message: t('archived.unexpectedError'), }) } } @@ -1012,9 +1014,9 @@ export const useChoreActions = ({ setConfirmModelConfig({ isOpen: true, - title: 'Delete Tasks', - confirmText: 'Delete', - cancelText: 'Cancel', + title: t('actions.bulk.deleteTitle'), + confirmText: t('archived.delete'), + cancelText: t('choreView.cancel'), message: `Delete ${selectedData.length} task${selectedData.length > 1 ? 's' : ''}?\n\nThis action cannot be undone.`, onClose: async isConfirmed => { if (isConfirmed === true) { @@ -1033,7 +1035,7 @@ export const useChoreActions = ({ if (deletedTasks.length > 0) { showSuccess({ - title: 'πŸ—‘οΈ Tasks Deleted', + title: t('archived.deletedBulkTitle'), message: `Successfully deleted ${deletedTasks.length} task${deletedTasks.length > 1 ? 's' : ''}.`, }) @@ -1048,7 +1050,7 @@ export const useChoreActions = ({ if (failedTasks.length > 0) { showError({ - title: 'Some Tasks Failed', + title: t('archived.someFailedTitle'), message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be deleted.`, }) } @@ -1056,8 +1058,8 @@ export const useChoreActions = ({ clearSelection() } catch (error) { showError({ - title: 'Bulk Delete Failed', - message: 'An unexpected error occurred. Please try again.', + title: t('archived.bulkDeleteFailTitle'), + message: t('archived.unexpectedError'), }) } } @@ -1083,9 +1085,9 @@ export const useChoreActions = ({ setConfirmModelConfig({ isOpen: true, - title: 'Skip Tasks', - confirmText: 'Skip', - cancelText: 'Cancel', + title: t('actions.bulk.skipTitle'), + confirmText: t('multiToolbar.skip'), + cancelText: t('choreView.cancel'), message: `Skip ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} to next due date?`, onClose: async isConfirmed => { if (isConfirmed === true) { @@ -1104,7 +1106,7 @@ export const useChoreActions = ({ if (skippedTasks.length > 0) { showSuccess({ - title: '⏭️ Tasks Skipped', + title: t('actions.bulk.skippedTitle'), message: `Successfully skipped ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`, undoAction: async () => { try { @@ -1113,13 +1115,13 @@ export const useChoreActions = ({ } queryClient.invalidateQueries(['chores']) showUndo({ - title: 'Undo Successful', + title: t('choreView.undoSuccessful'), message: `Undo skip for ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`, }) } catch (error) { showError({ - title: 'Undo Failed', - message: 'Unable to undo the action. Please try again.', + title: t('choreView.undoFailed'), + message: t('choreView.undoFailedMessage'), }) } }, @@ -1128,7 +1130,7 @@ export const useChoreActions = ({ if (failedTasks.length > 0) { showError({ - title: 'Some Tasks Failed', + title: t('archived.someFailedTitle'), message: `${failedTasks.length > 1 ? 's' : ''} could not be skipped.`, }) } @@ -1137,8 +1139,8 @@ export const useChoreActions = ({ clearSelection() } catch (error) { showError({ - title: 'Bulk Skip Failed', - message: 'An unexpected error occurred. Please try again.', + title: t('actions.bulk.skipFailedTitle'), + message: t('archived.unexpectedError'), }) } } @@ -1190,7 +1192,7 @@ export const useChoreActions = ({ } if (failedTasks.length > 0) { showError({ - title: 'Some Tasks Failed', + title: t('archived.someFailedTitle'), message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be moved.`, }) } diff --git a/src/views/Filters/FilterView.jsx b/src/views/Filters/FilterView.jsx index be37bd8..fa6618a 100644 --- a/src/views/Filters/FilterView.jsx +++ b/src/views/Filters/FilterView.jsx @@ -46,6 +46,7 @@ import { useToggleFilterPin, useUpdateFilter, } from './FilterQueries' +import { useTranslation } from 'react-i18next' const FilterCardContent = ({ filter, @@ -245,6 +246,7 @@ const FilterCardContent = ({ } const FilterView = () => { + const { t } = useTranslation('filters') const navigate = useNavigate() const { data: userProfile } = useUserProfile() const { data: chores = { res: [] } } = useChores(false) @@ -330,11 +332,11 @@ const FilterView = () => { const filter = savedFilters.find(f => f.id === id) setConfirmationModel({ isOpen: true, - title: 'Delete Filter', + title: t('delete.title'), message: `Are you sure you want to delete "${filter?.name}"? This cannot be undone.`, - confirmText: 'Delete', + confirmText: t('common:delete'), color: 'danger', - cancelText: 'Cancel', + cancelText: t('common:cancel'), onClose: confirmed => { if (confirmed === true) { handleDeleteFilter(id) @@ -421,7 +423,7 @@ const FilterView = () => { } - title='No saved filters yet' + title={t('empty.title')} description='Save a set of conditions once, like "overdue and assigned to me", and jump straight back to it from anywhere.' primaryAction={{ label: 'Create a filter', @@ -490,7 +492,7 @@ const FilterView = () => { > - Edit + {t('common:edit')} @@ -515,7 +517,7 @@ const FilterView = () => { sx={{ mt: 0.5 }} color='danger' > - Delete + {t('common:delete')} diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx index cf5906c..d1983ba 100644 --- a/src/views/History/ChoreHistory.jsx +++ b/src/views/History/ChoreHistory.jsx @@ -52,8 +52,10 @@ import HistoryDetailModal from '../Modals/HistoryDetailModal' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import NoteViewerModal from '../Modals/Inputs/NoteViewerModal' import HistoryCard from './HistoryCard' +import { useTranslation } from 'react-i18next' const ChoreHistory = () => { + const { t } = useTranslation('history') const [userHistory, setUserHistory] = useState([]) const [historyInfo, setHistoryInfo] = useState([]) const { choreId } = useParams() @@ -96,43 +98,43 @@ const ChoreHistory = () => { () => [ { id: 'status', - label: 'Status', + label: t('charts.status.title'), type: 'multi-select', icon: , options: [ { value: ChoreHistoryStatus.COMPLETED, - label: 'Completed', + label: t('status.completed'), color: 'success', icon: , }, { value: ChoreHistoryStatus.SKIPPED, - label: 'Skipped', + label: t('status.skipped'), color: 'warning', icon: , }, { value: ChoreHistoryStatus.PENDING_APPROVAL, - label: 'Pending', + label: t('filter.pending'), color: 'neutral', icon: , }, { value: ChoreHistoryStatus.REJECTED, - label: 'Rejected', + label: t('status.rejected'), color: 'danger', icon: , }, { value: 5, - label: 'Missed', + label: t('status.missed'), color: 'danger', icon: , }, { value: 6, - label: 'Rescheduled', + label: t('status.rescheduled'), color: 'warning', icon: , }, @@ -141,14 +143,14 @@ const ChoreHistory = () => { }, { id: 'hasNotes', - label: 'Has Notes', + label: t('filter.hasNotes'), type: 'boolean', icon: , filterFn: item => !!item.notes, }, { id: 'completedBy', - label: 'Completed By', + label: t('filter.completedBy'), type: 'multi-select', icon: , options: performers.map(p => ({ @@ -160,7 +162,7 @@ const ChoreHistory = () => { }, { id: 'dateRange', - label: 'Completed At', + label: t('filter.completedAt'), type: 'date-range', icon: , filterFn: (item, value) => { @@ -328,7 +330,7 @@ const ChoreHistory = () => { level='title-md' sx={{ fontWeight: 'lg', color: 'text.primary' }} > - Task Summary + {t('title.summary')} @@ -405,7 +407,7 @@ const ChoreHistory = () => { level='title-md' sx={{ fontWeight: 'lg', color: 'text.primary' }} > - Task Activity + {t('title.activity')} @@ -425,7 +427,7 @@ const ChoreHistory = () => { icon={} title='No history matches these filters' description='There is history here, but none of it fits the filters that are currently on.' - primaryAction={{ label: 'Clear filters', onClick: clearAll }} + primaryAction={{ label: t('noResults.clear'), onClick: clearAll }} /> )} @@ -467,7 +469,7 @@ const ChoreHistory = () => { > - Edit + {t('common:edit')} @@ -486,7 +488,7 @@ const ChoreHistory = () => { > - Delete + {t('common:delete')} @@ -560,13 +562,13 @@ const ChoreHistory = () => { setEditHistory(null) if (data?.queued) { showSuccess({ - title: 'History Update Queued', + title: t('toast.updateQueued.title'), message: 'You are offline. The history update will sync when connection is restored.', }) } else { showSuccess({ - title: 'History Updated', + title: t('toast.updated.title'), message: `The history record has been updated successfully.`, }) } @@ -590,13 +592,13 @@ const ChoreHistory = () => { setEditHistory(null) if (data?.queued) { showSuccess({ - title: 'History Delete Queued', + title: t('toast.deleteQueued.title'), message: 'You are offline. The history delete will sync when connection is restored.', }) } else { showSuccess({ - title: 'History Deleted', + title: t('toast.deleted.title'), message: `The history record has been deleted successfully.`, }) } diff --git a/src/views/History/HistoryCard.jsx b/src/views/History/HistoryCard.jsx index 555bdbb..0204f0b 100644 --- a/src/views/History/HistoryCard.jsx +++ b/src/views/History/HistoryCard.jsx @@ -10,6 +10,7 @@ import { } from '@mui/icons-material' import { Avatar, Box, Card, Chip, IconButton, Typography } from '@mui/joy' import moment from 'moment' +import { useTranslation } from 'react-i18next' import { useLocalization } from '../../contexts/LocalizationContext' import { TASK_COLOR } from '../../utils/Colors.jsx' import PendingBadge from '../components/PendingBadge' @@ -33,13 +34,13 @@ const stripHtmlTags = html => { } const statusConfig = { - 0: { label: 'In Progress', color: 'primary', icon: }, - 1: { label: 'Completed', color: 'success', icon: }, - 2: { label: 'Skipped', color: 'warning', icon: }, - 3: { label: 'Pending Approval', color: 'neutral', icon: }, - 4: { label: 'Rejected', color: 'danger', icon: }, - 5: { label: 'Missed', color: 'danger', icon: }, - 6: { label: 'Rescheduled', color: 'warning', icon: }, + 0: { labelKey: 'status.inProgress', color: 'primary', icon: }, + 1: { labelKey: 'status.completed', color: 'success', icon: }, + 2: { labelKey: 'status.skipped', color: 'warning', icon: }, + 3: { labelKey: 'status.pendingApproval', color: 'neutral', icon: }, + 4: { labelKey: 'status.rejected', color: 'danger', icon: }, + 5: { labelKey: 'status.missed', color: 'danger', icon: }, + 6: { labelKey: 'status.rescheduled', color: 'warning', icon: }, } const HistoryCard = ({ @@ -52,45 +53,54 @@ const HistoryCard = ({ onViewNote, onViewDetails, }) => { + const { t } = useTranslation('history') const { fmt } = useLocalization() const performer = performers.find(p => p.userId === historyEntry.completedBy) const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo) const config = statusConfig[historyEntry.status] ?? statusConfig[1] const displayLabel = - historyEntry.status === 6 && !historyEntry.dueDate ? 'Scheduled' : config.label + historyEntry.status === 6 && !historyEntry.dueDate + ? t('status.scheduled') + : t(config.labelKey) const actionDate = historyEntry.performedAt || historyEntry.updatedAt const getTimingLine = () => { const { status, performedAt, dueDate } = historyEntry if (!dueDate) return null - if (status === 6) { - return `Was due ${moment(dueDate).format('MMM D')}` - } - if (status === 5) { - return `Was due ${moment(dueDate).format('MMM D')}` + if (status === 6 || status === 5) { + return t('card.wasDue', { date: fmt.date(dueDate) }) } if ((status === 1 || status === 2 || status === 0) && performedAt) { const diffHours = moment(performedAt).diff(dueDate, 'hours') const abs = Math.abs(diffHours) if (abs <= 6) return null // chip already says "On Time" - if (diffHours < 0) return abs >= 48 ? `${Math.floor(abs / 24)}d before due date` : `${abs}h before due date` - return abs >= 48 ? `${Math.floor(abs / 24)}d after due date` : `${abs}h after due date` + const early = diffHours < 0 + if (abs >= 48) { + const days = Math.floor(abs / 24) + return early + ? t('card.daysBeforeDue', { count: days }) + : t('card.daysAfterDue', { count: days }) + } + return early + ? t('card.hoursBeforeDue', { count: abs }) + : t('card.hoursAfterDue', { count: abs }) } return null } const timingLine = getTimingLine() - const noteLabel = historyEntry.status === 2 || historyEntry.status === 4 ? 'Reason' : 'Note' const plainTextNotes = historyEntry.notes ? stripHtmlTags(historyEntry.notes) : '' const metaTextParts = [ fmt.dateTime(actionDate), historyEntry.completedBy !== historyEntry.assignedTo && assignedTo - ? `Assigned to ${assignedTo.displayName}` + ? t('card.assignedTo', { name: assignedTo.displayName }) : null, historyEntry?.duration > 0 ? `⏱ ${formatTime(historyEntry.duration)}` : null, - historyEntry?.points > 0 ? `β˜… ${historyEntry.points} pt${historyEntry.points > 1 ? 's' : ''}` : null, + historyEntry?.points > 0 + ? t('card.points', { count: historyEntry.points }) + : null, ].filter(Boolean) return ( diff --git a/src/views/Modals/Inputs/LabelModal.jsx b/src/views/Modals/Inputs/LabelModal.jsx index 7b771f0..41e47cf 100644 --- a/src/views/Modals/Inputs/LabelModal.jsx +++ b/src/views/Modals/Inputs/LabelModal.jsx @@ -8,8 +8,10 @@ import { useNotification } from '../../../service/NotificationProvider.jsx' import LABEL_COLORS from '../../../utils/Colors.jsx' import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher' import { useLabels } from '../../Labels/LabelQueries' +import { useTranslation } from 'react-i18next' function LabelModal({ isOpen, onClose, label }) { + const { t } = useTranslation('labels') const { ResponsiveModal } = useResponsiveModal() const [labelName, setLabelName] = useState('') @@ -34,7 +36,7 @@ function LabelModal({ isOpen, onClose, label }) { // Validation logic const validateLabel = () => { if (!labelName.trim()) { - setError('Name cannot be empty') + setError(t('modal.errorEmptyName')) return false } if ( @@ -42,11 +44,11 @@ function LabelModal({ isOpen, onClose, label }) { userLabel => userLabel.name === labelName && userLabel.id !== label?.id, ) ) { - setError('Label with this name already exists') + setError(t('modal.errorDuplicate')) return false } if (!color) { - setError('Please select a color') + setError(t('modal.errorNoColor')) return false } return true @@ -71,13 +73,13 @@ function LabelModal({ isOpen, onClose, label }) { .catch(err => { if (err.queued) { showError({ - title: 'Failed to save label', - message: 'Unable to save label. Please try again.', + title: t('modal.saveFailedTitle'), + message: t('modal.saveFailedMessage'), }) } else { showError({ - title: 'Failed to save label', - message: 'Unable to save label. Please try again.', + title: t('modal.saveFailedTitle'), + message: t('modal.saveFailedMessage'), }) } }) @@ -92,7 +94,7 @@ function LabelModal({ isOpen, onClose, label }) { title={label ? 'Edit Label' : 'Add Label'} footer={ - Name + {t('modal.name')} { + const { t } = useTranslation('projects') const { ResponsiveModal } = useResponsiveModal() const [projectName, setProjectName] = useState('') const [projectDescription, setProjectDescription] = useState('') @@ -58,7 +60,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => { e.preventDefault() if (!projectName.trim()) { - setError('Project name is required') + setError(t('modal.errorNameRequired')) return } @@ -82,7 +84,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => { }, onError: error => { console.error('Error updating project:', error) - setError('Failed to update project') + setError(t('modal.errorUpdate')) }, }, ) @@ -95,7 +97,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => { }, onError: error => { console.error('Error creating project:', error) - setError('Failed to create project') + setError(t('modal.errorCreate')) }, }) } @@ -130,7 +132,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => { footer={ { setProjectName(e.target.value)} - placeholder='Enter project name...' + placeholder={t('modal.namePlaceholder')} autoFocus disabled={isSubmitting} /> @@ -164,7 +166,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {