diff --git a/public/locales/en/chores.json b/public/locales/en/chores.json index ec82037..50b151f 100644 --- a/public/locales/en/chores.json +++ b/public/locales/en/chores.json @@ -71,5 +71,11 @@ "reset": "Reset Timer", "delete": "Delete Session" } + }, + "toolbar": { + "defaultProject": "Default Project" + }, + "sort": { + "assignedToMe": "Assigned to me" } } diff --git a/public/locales/en/filters.json b/public/locales/en/filters.json new file mode 100644 index 0000000..6b10047 --- /dev/null +++ b/public/locales/en/filters.json @@ -0,0 +1,17 @@ +{ + "builder": { + "errorName": "Please enter a filter name", + "errorConditions": "Please configure at least one filter condition", + "editTitle": "Edit Filter", + "newTitle": "New Filter", + "addConditions": "Add conditions to preview", + "save": "Save Filter", + "name": "Filter Name", + "namePlaceholder": "e.g. Overdue tasks for Alice", + "color": "Color" + }, + "nameExists": "A filter with this name already exists", + "tasks_one": "{{count}} task", + "tasks_other": "{{count}} tasks", + "overdue": "{{count}} overdue" +} diff --git a/public/locales/en/history.json b/public/locales/en/history.json new file mode 100644 index 0000000..1452849 --- /dev/null +++ b/public/locales/en/history.json @@ -0,0 +1,89 @@ +{ + "edit": { + "title": "Edit History", + "completedDate": "Completed Date", + "note": "Note", + "additionalNotes": "Additional Notes", + "deleteTitle": "Delete History", + "deleteMessage": "Are you sure you want to delete this history?" + }, + "status": { + "inProgress": "In Progress", + "completed": "Completed", + "skipped": "Skipped", + "pendingApproval": "Pending Approval", + "rejected": "Rejected", + "missed": "Missed", + "rescheduled": "Rescheduled", + "scheduled": "Scheduled" + }, + "badge": { + "onTime": "On Time", + "onTimeLabel": "On time" + }, + "detail": { + "title": "Activity Detail", + "openTask": "Open Task", + "editEntry": "Edit Entry", + "performedBy": "Performed by", + "assignedTo": "Assigned to", + "lastUpdated": "Last updated", + "duration": "Duration", + "pointsEarned": "Points earned", + "points_one": "{{count}} pt", + "points_other": "{{count}} pts", + "note": "Note" + }, + "charts": { + "noData": "No data available", + "noLabels": "No Labels", + "unassigned": "Unassigned", + "unknownTask": "Unknown Task", + "late": "Late", + "status": { + "title": "Status", + "description": "Completed tasks status" + }, + "due": { + "title": "Due Date", + "description": "Current tasks due date" + }, + "priority": { + "title": "Priority", + "description": "Tasks by priority" + }, + "labels": { + "title": "Labels", + "description": "Tasks by labels" + }, + "labelsDuration": { + "title": "Labels (time)", + "description": "Time spent by labels (hours)" + }, + "tasksTime": { + "title": "Tasks (time)", + "description": "Time spent by individual tasks (hours)" + }, + "assigneeBreakdown": { + "title": "by Assignee", + "description": "Tasks grouped by assignee" + } + }, + "filter": { + "status": "Status", + "pending": "Pending", + "labels": "Labels", + "hasNotes": "Has Notes", + "hasPoints": "Has Points", + "timePeriod": "Time Period", + "user": "User" + }, + "period": { + "days_one": "{{count}} Day", + "days_other": "{{count}} Days", + "allTime": "All Time" + }, + "activities": { + "title": "Activities" + } +} diff --git a/public/locales/en/labels.json b/public/locales/en/labels.json new file mode 100644 index 0000000..93f96cb --- /dev/null +++ b/public/locales/en/labels.json @@ -0,0 +1,9 @@ +{ + "shared": "Shared", + "delete": { + "title": "Delete Label", + "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." +} diff --git a/public/locales/en/points.json b/public/locales/en/points.json new file mode 100644 index 0000000..a6ccd40 --- /dev/null +++ b/public/locales/en/points.json @@ -0,0 +1,74 @@ +{ + "redeem": "Redeem Points", + "redeemModal": { + "redeemButton": "Redeem", + "pointsAvailable": "{{count}} points available", + "pointsToRedeem": "Points to Redeem", + "placeholder": "Enter points...", + "cannotExceed": "Cannot exceed available points", + "quickAdd": "Quick Add:", + "aboutToRedeem": "You are about to redeem", + "amount": "{{count}} points", + "remaining": "Remaining: {{count}} points" + }, + "leaderboard": { + "titlePoints": "Points Leaderboard", + "titleTasks": "Tasks Leaderboard", + "subtitlePoints": "Rankings based on points earned during the selected time period", + "subtitleTasks": "Rankings based on tasks completed during the selected time period", + "modePoints": "Points", + "modeTasks": "Tasks", + "you": "You", + "tasksAndAvg": "{{tasks}} tasks • {{avg}} avg per task", + "available": "{{count}} available", + "points_one": "{{count}} point", + "points_other": "{{count}} points" + }, + "tabs": { + "short7d": "7D", + "short6m": "6M", + "shortAll": "All", + "days7": "7 Days", + "months6": "6 Months", + "allTime": "All Time" + }, + "filter": { + "analysisTitle": "Filter & Analysis", + "title": "Filter Points", + "showFor": "Show points for:", + "timePeriod": "Time period:" + }, + "cards": { + "available": { + "title": "Available", + "subtext": "Ready to redeem" + }, + "redeemed": { + "title": "Redeemed", + "subtext": "Previously used" + }, + "total": { + "title": "Total", + "subtext": "All time earned" + }, + "period": { + "title": "Period Points", + "allTime": "All time", + "last6Months": "Last 6 months", + "lastDays_one": "Last {{count}} day", + "lastDays_other": "Last {{count}} days" + } + }, + "summary": { + "showingFor": "Showing points for", + "unknownUser": "Unknown User", + "overThe": "over the", + "allTime": "All Time", + "last6Months": "Last 6 Months", + "lastDays_one": "Last {{count}} Day", + "lastDays_other": "Last {{count}} Days" + }, + "trend": { + "title": "Points Trend" + } +} diff --git a/public/locales/en/projects.json b/public/locales/en/projects.json new file mode 100644 index 0000000..0e7eed4 --- /dev/null +++ b/public/locales/en/projects.json @@ -0,0 +1,24 @@ +{ + "iconPicker": { + "chooseIcon": "Choose Project Icon", + "availableIcons": "Available Icons" + }, + "defaultChip": "Default", + "tasks_one": "{{count}} task", + "tasks_other": "{{count}} tasks", + "shared": "Shared", + "delete": { + "title": "Delete Project", + "message": "Are you sure you want to delete \"{{name}}\"? This will remove the project but keep all tasks (they'll move to the Default Project)." + }, + "loadError": "Failed to load projects. Please try again.", + "blurb": "Organize your tasks into projects. Create custom workspaces to keep your tasks organized and easily accessible.", + "defaultDescription": "All tasks without a specific project", + "selector": { + "title": "Projects", + "createNew": "Create New Project", + "createNewDescription": "Add a custom project workspace", + "manage": "Manage Projects", + "manageDescription": "View, edit, and organize all projects" + } +} diff --git a/public/locales/en/things.json b/public/locales/en/things.json new file mode 100644 index 0000000..1181eb5 --- /dev/null +++ b/public/locales/en/things.json @@ -0,0 +1,18 @@ +{ + "errName": "Name is required", + "errStateNumber": "State must be a number", + "errStateBool": "State must be true or false", + "errStateRequired": "State is required", + "name": "Name", + "namePlaceholder": "Thing name", + "value": "Value", + "valuePlaceholder": "Thing value", + "updateState": "Update state", + "update": "Update", + "history": { + "overview": "Things Overview", + "visualization": "Data Visualization", + "changeHistory": "Change History", + "updated": "Updated" + } +} diff --git a/public/locales/en/timer.json b/public/locales/en/timer.json new file mode 100644 index 0000000..848fd53 --- /dev/null +++ b/public/locales/en/timer.json @@ -0,0 +1,29 @@ +{ + "toast": { + "sessionUpdatedTitle": "Session updated", + "sessionUpdatedMessage": "Timer session has been updated successfully.", + "sessionUpdateFailTitle": "Failed to update session", + "tryAgain": "Please try again.", + "sessionUpdateErrorTitle": "Error updating session", + "sessionDeletedTitle": "Session deleted", + "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?" + }, + "loading": "Loading timer data...", + "noData": "No timer data found for this chore.", + "activeWork": "Active Work", + "breakTime": "Break Time", + "workSessions": "Work Sessions", + "totalTime": "Total Time", + "distribution": "Work vs Break Distribution", + "noActiveTime": "No active time yet", + "sessionBreakdown": "Session Breakdown", + "sessions": "Sessions", + "addSession": "Add Session", + "startTime": "Start Time", + "endTime": "End Time", + "leaveEmpty": "Leave empty if session is ongoing", + "noSessionForChore": "No timer session found for this chore." +} diff --git a/src/i18n/config.js b/src/i18n/config.js index 698fc83..5e30e52 100644 --- a/src/i18n/config.js +++ b/src/i18n/config.js @@ -22,7 +22,18 @@ i18n loadPath: '/locales/{{lng}}/{{ns}}.json', }, - ns: ['common', 'settings', 'chores'], + ns: [ + 'common', + 'settings', + 'chores', + 'labels', + 'history', + 'filters', + 'things', + 'projects', + 'timer', + 'points', + ], defaultNS: 'common', detection: { diff --git a/src/views/Labels/LabelView.jsx b/src/views/Labels/LabelView.jsx index 1f05020..841215e 100644 --- a/src/views/Labels/LabelView.jsx +++ b/src/views/Labels/LabelView.jsx @@ -11,6 +11,7 @@ import { Typography, } from '@mui/joy' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import LabelModal from '../Modals/Inputs/LabelModal' import { @@ -32,6 +33,7 @@ import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import { useLabels } from './LabelQueries' const LabelCardContent = ({ label, currentUserId, onToggleActions }) => { + const { t } = useTranslation('labels') // Check if current user owns this label const isOwnedByCurrentUser = label.created_by === currentUserId @@ -124,7 +126,7 @@ const LabelCardContent = ({ label, currentUserId, onToggleActions }) => { fontWeight: 'md', }} > - Shared + {t('shared')} )} @@ -149,6 +151,7 @@ const LabelCardContent = ({ label, currentUserId, onToggleActions }) => { } const LabelView = () => { + const { t } = useTranslation('labels') const { data: labels, isLabelsLoading, isError } = useLabels() const { data: userProfile } = useUserProfile() @@ -173,14 +176,11 @@ const LabelView = () => { const handleDeleteClicked = id => { setConfirmationModel({ isOpen: true, - title: 'Delete Label', - - message: - 'Are you sure you want to delete this label? This will remove the label from all tasks.', - - confirmText: 'Delete', + title: t('delete.title'), + message: t('delete.message'), + confirmText: t('common:delete'), color: 'danger', - cancelText: 'Cancel', + cancelText: t('common:cancel'), onClose: confirmed => { if (confirmed === true) { handleDeleteLabel(id) @@ -230,7 +230,7 @@ const LabelView = () => { if (isError) { return ( - Failed to load labels. Please try again. + {t('loadError')} ) } @@ -244,12 +244,10 @@ const LabelView = () => { level='h3' sx={{ fontWeight: 'lg', color: 'text.primary' }} > - Labels + {t('common:navigation.labels')} - Manage your labels and organize your tasks effectively. Labels will - be automatically shared with your circle if they are used on a - shared task. + {t('blurb')} @@ -300,7 +298,7 @@ const LabelView = () => { > - Edit + {t('common:edit')} @@ -319,7 +317,7 @@ const LabelView = () => { > - Delete + {t('common:delete')} diff --git a/src/views/Modals/EditHistoryModal.jsx b/src/views/Modals/EditHistoryModal.jsx index 6321ea6..381828c 100644 --- a/src/views/Modals/EditHistoryModal.jsx +++ b/src/views/Modals/EditHistoryModal.jsx @@ -1,12 +1,14 @@ import { FormLabel, Input } from '@mui/joy' import moment from 'moment' import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import ModalActions from '../../components/common/ModalActions' import { useResponsiveModal } from '../../hooks/useResponsiveModal' import ConfirmationModal from './Inputs/ConfirmationModal' function EditHistoryModal({ config, historyRecord }) { + const { t } = useTranslation('history') const { ResponsiveModal } = useResponsiveModal() const [completedDate, setCompletedDate] = useState('') @@ -40,12 +42,12 @@ function EditHistoryModal({ config, historyRecord }) { onClose={config?.onClose} size='lg' // fullWidth={true} - title='Edit History' + title={t('edit.title')} footer={ config.onSave({ id: historyRecord.id, @@ -57,7 +59,7 @@ function EditHistoryModal({ config, historyRecord }) { /> } > - Due Date + {t('chores:dueDate')} - Completed Date + {t('edit.completedDate')} - Note + {t('edit.note')} { if (e.target.value.trim() === '') { @@ -104,10 +106,10 @@ function EditHistoryModal({ config, historyRecord }) { } setIsDeleteModalOpen(false) }, - title: 'Delete History', - message: 'Are you sure you want to delete this history?', - confirmText: 'Delete', - cancelText: 'Cancel', + title: t('edit.deleteTitle'), + message: t('edit.deleteMessage'), + confirmText: t('common:delete'), + cancelText: t('common:cancel'), color: 'danger', }} /> diff --git a/src/views/Modals/HistoryDetailModal.jsx b/src/views/Modals/HistoryDetailModal.jsx index 23400c6..2744314 100644 --- a/src/views/Modals/HistoryDetailModal.jsx +++ b/src/views/Modals/HistoryDetailModal.jsx @@ -14,6 +14,7 @@ import { } from '@mui/icons-material' import { Avatar, Box, Button, Chip, Divider, Stack, Typography } from '@mui/joy' import moment from 'moment' +import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import ModalActions from '../../components/common/ModalActions' import { useLocalization } from '../../contexts/LocalizationContext' @@ -22,13 +23,13 @@ import { TASK_COLOR } from '../../utils/Colors.jsx' import RichTextEditor from '../components/RichTextEditor.jsx' const STATUS_CONFIG = { - 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: { label: t('status.inProgress'), color: 'primary', icon: }, + 1: { label: t('status.completed'), color: 'success', icon: }, + 2: { label: t('status.skipped'), color: 'warning', icon: }, + 3: { label: t('status.pendingApproval'), color: 'neutral', icon: }, + 4: { label: t('status.rejected'), color: 'danger', icon: }, + 5: { label: t('status.missed'), color: 'danger', icon: }, + 6: { label: t('status.rescheduled'), color: 'warning', icon: }, } const DetailRow = ({ icon, label, value, children }) => ( @@ -55,6 +56,8 @@ const DetailRow = ({ icon, label, value, children }) => ( ) const TimingBadge = ({ historyEntry }) => { + const { t } = useTranslation('history') + if (!historyEntry.dueDate || !historyEntry.performedAt) return null if ([0, 5, 6].includes(historyEntry.status)) return null @@ -71,7 +74,7 @@ const TimingBadge = ({ historyEntry }) => { sx={{ backgroundColor: TASK_COLOR.COMPLETED, color: 'white' }} startDecorator={} > - On Time + {t('badge.onTime')} ) } else if (performedAt.isBefore(dueDate)) { @@ -103,6 +106,7 @@ const TimingBadge = ({ historyEntry }) => { } function HistoryDetailModal({ config }) { + const { t } = useTranslation('history') const { ResponsiveModal } = useResponsiveModal() const { fmt } = useLocalization() const navigate = useNavigate() @@ -114,7 +118,9 @@ function HistoryDetailModal({ config }) { const statusCfg = STATUS_CONFIG[entry.status] ?? STATUS_CONFIG[1] const isFirstSchedule = entry.status === 6 && !entry.dueDate - const statusLabel = isFirstSchedule ? 'Scheduled' : statusCfg.label + const statusLabel = isFirstSchedule + ? t('status.scheduled') + : t(statusCfg.labelKey) const performer = performers.find(p => p.userId === entry.completedBy) const assignedTo = performers.find(p => p.userId === entry.assignedTo) const isDifferentAssignee = @@ -138,7 +144,7 @@ function HistoryDetailModal({ config }) { {entry.choreId && ( @@ -151,7 +157,7 @@ function HistoryDetailModal({ config }) { navigate(`/chores/${entry.choreId}`) }} > - Open Task + {t('detail.openTask')} )} {config?.onEdit && ( @@ -159,7 +165,7 @@ function HistoryDetailModal({ config }) { startDecorator={} onClick={() => config.onEdit(entry)} > - Edit Entry + {t('detail.editEntry')} )} @@ -196,7 +202,7 @@ function HistoryDetailModal({ config }) { {performer && ( } - label='Performed by' + label={t('detail.performedBy')} > } - label='Assigned to' + label={t('detail.assignedTo')} value={assignedTo.displayName} /> )} @@ -259,7 +265,7 @@ function HistoryDetailModal({ config }) { {showUpdatedAt && ( } - label='Last updated' + label={t('detail.lastUpdated')} value={fmt.dateTime(entry.updatedAt)} /> )} @@ -268,7 +274,7 @@ function HistoryDetailModal({ config }) { {entry.duration > 0 && ( } - label='Duration' + label={t('detail.duration')} value={formatDuration(entry.duration)} /> )} @@ -277,8 +283,8 @@ function HistoryDetailModal({ config }) { {entry.points > 0 && ( ★} - label='Points earned' - value={`${entry.points} pt${entry.points > 1 ? 's' : ''}`} + label={t('detail.pointsEarned')} + value={t('detail.points', { count: entry.points })} /> )} diff --git a/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx b/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx index 431ac71..8fa8e16 100644 --- a/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx +++ b/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx @@ -11,6 +11,7 @@ import FilterBuilderContent, { import { FILTER_COLORS } from '../../../utils/Colors' import { applyFilter } from '../../../utils/FilterEngine' import { useFilters } from '../../Filters/FilterQueries' +import { useTranslation } from 'react-i18next' const EMPTY_FILTERS = [] @@ -25,6 +26,7 @@ const AdvancedFilterBuilder = ({ userProfile = null, editingFilter = null, }) => { + const { t } = useTranslation('filters') const [filterName, setFilterName] = useState('') const [filterColor, setFilterColor] = useState(FILTER_COLORS[0].value) const [selections, setSelections] = useState(defaultSelections()) @@ -76,15 +78,15 @@ const AdvancedFilterBuilder = ({ const handleSave = () => { if (!filterName.trim()) { - setError('Please enter a filter name') + setError(t('builder.errorName')) return } if (filterNameExists(filterName.trim(), editingFilter?.id)) { - setError('A filter with this name already exists') + setError(t('nameExists')) return } if (conditions.length === 0) { - setError('Please configure at least one filter condition') + setError(t('builder.errorConditions')) return } onSave({ @@ -106,7 +108,7 @@ const AdvancedFilterBuilder = ({ maxHeight='92vh' title={ - {editingFilter ? 'Edit Filter' : 'New Filter'} + {editingFilter ? t('builder.editTitle') : t('builder.newTitle')} {activeConditionCount > 0 && ( {activeConditionCount} condition @@ -129,17 +131,17 @@ const AdvancedFilterBuilder = ({ {conditions.length > 0 ? ( <> - {previewCount} task{previewCount !== 1 ? 's' : ''} + {t('tasks', { count: previewCount })} {previewOverdueCount > 0 && ( - {previewOverdueCount} overdue + {t('overdue', { count: previewOverdueCount })} )} ) : ( - Add conditions to preview + {t('builder.addConditions')} )} @@ -147,7 +149,7 @@ const AdvancedFilterBuilder = ({ {/* Actions */} @@ -168,10 +170,10 @@ const AdvancedFilterBuilder = ({ level='body-xs' sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }} > - Filter Name + {t('builder.name')} { setFilterName(e.target.value) @@ -193,13 +195,13 @@ const AdvancedFilterBuilder = ({ level='body-xs' sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }} > - Color + {t('builder.color')} {FILTER_COLORS.map(c => ( setFilterColor(c.value)} sx={{ width: 26, diff --git a/src/views/Modals/Inputs/CreateThingModal.jsx b/src/views/Modals/Inputs/CreateThingModal.jsx index 9c2f367..ab2cb6f 100644 --- a/src/views/Modals/Inputs/CreateThingModal.jsx +++ b/src/views/Modals/Inputs/CreateThingModal.jsx @@ -10,8 +10,10 @@ import { import { useEffect, useState } from 'react' import ModalActions from '../../../components/common/ModalActions' import { useResponsiveModal } from '../../../hooks/useResponsiveModal' +import { useTranslation } from 'react-i18next' function CreateThingModal({ isOpen, onClose, onSave, currentThing }) { + const { t } = useTranslation('things') const { ResponsiveModal } = useResponsiveModal() const [name, setName] = useState(currentThing?.name || '') @@ -33,17 +35,17 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) { const isValid = () => { const newErrors = {} if (!name || name.trim() === '') { - newErrors.name = 'Name is required' + newErrors.name = t('errName') } if (type === 'number' && isNaN(state)) { - newErrors.state = 'State must be a number' + newErrors.state = t('errStateNumber') } if (type === 'boolean' && !['true', 'false'].includes(state)) { - newErrors.state = 'State must be true or false' + newErrors.state = t('errStateBool') } if ((type === 'text' && !state) || state.trim() === '') { - newErrors.state = 'State is required' + newErrors.state = t('errStateRequired') } setErrors(newErrors) @@ -67,7 +69,7 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) { footer={ - Name + {t('name')}