diff --git a/public/locales/en/chores.json b/public/locales/en/chores.json
index 50b151f..72c8dc6 100644
--- a/public/locales/en/chores.json
+++ b/public/locales/en/chores.json
@@ -70,12 +70,125 @@
"paused": "Timer Paused",
"reset": "Reset Timer",
"delete": "Delete Session"
- }
+ },
+ "unableUndo": "Failed to undo",
+ "restoreFailed": "Failed to restore"
},
"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 d8dff3b..fe1f4d4 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -147,5 +147,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 43698c4..5e9e6c8 100644
--- a/public/locales/en/settings.json
+++ b/public/locales/en/settings.json
@@ -563,5 +563,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')}
@@ -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 }) => {