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 a6e4593..f2a0064 100644
--- a/src/i18n/config.js
+++ b/src/i18n/config.js
@@ -22,7 +22,19 @@ i18n
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
- ns: ['common', 'settings', 'chores', 'auth'],
+ ns: [
+ 'auth',
+ '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 */}
}
onClick={handleSave}
>
- Save Filter
+ {t('builder.save')}
@@ -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')}
{type === 'text' && (
- Value
+ {t('value')}
setState(e.target.value)}
sx={{ minWidth: 300 }}
@@ -113,9 +115,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
)}
{type === 'number' && (
- Value
+ {t('value')}
{
diff --git a/src/views/Modals/Inputs/EditThingState.jsx b/src/views/Modals/Inputs/EditThingState.jsx
index 1d77513..e9038a8 100644
--- a/src/views/Modals/Inputs/EditThingState.jsx
+++ b/src/views/Modals/Inputs/EditThingState.jsx
@@ -2,8 +2,10 @@ import { FormControl, FormHelperText, Input, Typography } from '@mui/joy'
import { useState } from 'react'
import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
+import { useTranslation } from 'react-i18next'
function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
+ const { t } = useTranslation('things')
const { ResponsiveModal } = useResponsiveModal()
const [state, setState] = useState(currentThing?.state || '')
@@ -13,7 +15,7 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
const newErrors = {}
if (state.trim() === '') {
- newErrors.state = 'State is required'
+ newErrors.state = t('errStateRequired')
}
setErrors(newErrors)
@@ -38,18 +40,18 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
open={isOpen}
onClose={onClose}
size='sm'
- title='Update state'
+ title={t('updateState')}
footer={
}
>
- Value
+ {t('value')}
setState(e.target.value)}
sx={{ minWidth: 300 }}
diff --git a/src/views/Modals/Inputs/IconPickerModal.jsx b/src/views/Modals/Inputs/IconPickerModal.jsx
index b1be623..491cb9f 100644
--- a/src/views/Modals/Inputs/IconPickerModal.jsx
+++ b/src/views/Modals/Inputs/IconPickerModal.jsx
@@ -3,6 +3,7 @@ import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { getTextColorFromBackgroundColor } from '../../../utils/Colors'
import PROJECT_ICONS from '../../../utils/ProjectIcons'
+import { useTranslation } from 'react-i18next'
const IconPickerModal = ({
isOpen,
@@ -11,6 +12,7 @@ const IconPickerModal = ({
currentIcon,
projectColor,
}) => {
+ const { t } = useTranslation('projects')
const { ResponsiveModal } = useResponsiveModal()
const handleIconClick = iconValue => {
@@ -25,13 +27,13 @@ const IconPickerModal = ({
size='lg'
fullWidth={true}
unmountDelay={250}
- title='Choose Project Icon'
+ title={t('iconPicker.chooseIcon')}
footer={
-
+
}
>
- Available Icons
+ {t('iconPicker.availableIcons')}
- {iconData.name}
+ {t(`icons.${iconData.key}`)}
diff --git a/src/views/Modals/Inputs/TimerEditModal.jsx b/src/views/Modals/Inputs/TimerEditModal.jsx
index f9b91e6..3ffed5b 100644
--- a/src/views/Modals/Inputs/TimerEditModal.jsx
+++ b/src/views/Modals/Inputs/TimerEditModal.jsx
@@ -12,6 +12,7 @@ import {
} from '@mui/joy'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useLocalization } from '../../../contexts/LocalizationContext'
import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
@@ -24,6 +25,7 @@ import {
import ConfirmationModal from './ConfirmationModal'
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
+ const { t } = useTranslation('timer')
const { ResponsiveModal } = useResponsiveModal()
const { fmt } = useLocalization()
@@ -181,8 +183,8 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
{
onSuccess: () => {
showSuccess({
- title: 'Session updated',
- message: 'Timer session has been updated successfully.',
+ title: t('toast.sessionUpdatedTitle'),
+ message: t('toast.sessionUpdatedMessage'),
})
refetchTimer()
cancelEditingSession(sessionId)
@@ -190,15 +192,15 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
},
onError: () => {
showError({
- title: 'Failed to update session',
- message: 'Please try again.',
+ title: t('toast.sessionUpdateFailTitle'),
+ message: t('toast.tryAgain'),
})
},
},
)
} catch (error) {
showError({
- title: 'Error updating session',
+ title: t('toast.sessionUpdateErrorTitle'),
message: error.message,
})
} finally {
@@ -213,15 +215,15 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
{
onSuccess: () => {
showSuccess({
- title: 'Session deleted',
- message: 'Timer session has been deleted successfully.',
+ title: t('toast.sessionDeletedTitle'),
+ message: t('toast.sessionDeletedMessage'),
})
refetchTimer()
onTimerUpdate?.()
},
onError: error => {
showError({
- title: 'Error deleting session',
+ title: t('toast.sessionDeleteErrorTitle'),
message: error.message,
})
},
@@ -235,10 +237,10 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
const confirmDeleteSession = sessionId => {
setConfirmDeleteConfig({
isOpen: true,
- title: 'Delete Timer Session',
- message: 'Are you sure you want to delete this timer session?',
- confirmText: 'Delete',
- cancelText: 'Cancel',
+ title: t('toast.deleteConfirmTitle'),
+ message: t('toast.deleteConfirmMessage'),
+ confirmText: t('common:delete'),
+ cancelText: t('common:cancel'),
color: 'danger',
onClose: isConfirmed => {
if (isConfirmed) {
@@ -310,23 +312,23 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
tertiary={
!loading && timerData && !editingSessions[timerData.id]
? {
- label: 'Delete',
+ label: t('common:delete'),
color: 'danger',
onClick: () => confirmDeleteSession(timerData.id),
}
: undefined
}
- secondary={{ label: 'Close', onClick: handleClose }}
+ secondary={{ label: t('common:close'), onClick: handleClose }}
primary={
!loading && timerData
? editingSessions[timerData.id]
? {
- label: 'Save',
+ label: t('common:save'),
onClick: () => saveSession(timerData.id),
loading,
}
: {
- label: 'Edit',
+ label: t('common:edit'),
startDecorator: ,
onClick: () => startEditingSession(),
}
@@ -337,13 +339,13 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
>
{loading && (
- Loading timer data...
+ {t('loading')}
)}
{!loading && !timerData && (
- No timer data found for this chore.
+ {t('noData')}
)}
@@ -403,7 +405,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
color: 'text.primary',
}}
>
- Active Work
+ {t('activeWork')}
@@ -457,7 +459,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
color: 'text.primary',
}}
>
- Break Time
+ {t('breakTime')}
@@ -511,7 +513,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
color: 'text.primary',
}}
>
- Work Sessions
+ {t('workSessions')}
@@ -565,7 +567,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
color: 'text.primary',
}}
>
- Total Time
+ {t('totalTime')}
@@ -597,12 +599,12 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
level='body-xs'
sx={{ color: 'text.secondary', fontWeight: 'medium' }}
>
- Work vs Break Distribution
+ {t('distribution')}
{calculateCurrentActiveDuration() > 0
? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}% active`
- : 'No active time yet'}
+ : t('noActiveTime')}
{
{/* Time Session */}
- Session Breakdown
+ {t('sessionBreakdown')}
@@ -644,7 +646,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
level='body-sm'
sx={{ fontWeight: 'bold', mb: 2 }}
>
- Work Sessions ({timerData.pauseLog.length})
+ {t('workSessions')} ({timerData.pauseLog.length})
{
level='body-sm'
sx={{ fontWeight: 'bold' }}
>
- Sessions
+ {t('sessions')}
@@ -855,7 +857,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
level='body-xs'
sx={{ fontWeight: 'bold' }}
>
- Start Time
+ {t('startTime')}
{
level='body-xs'
sx={{ fontWeight: 'bold' }}
>
- End Time
+ {t('endTime')}
{
}
/>
- Leave empty if session is ongoing
+ {t('leaveEmpty')}
@@ -941,7 +943,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
{!timerData && (
- No timer session found for this chore.
+ {t('noSessionForChore')}
)}
diff --git a/src/views/Modals/RedeemPointsModal.jsx b/src/views/Modals/RedeemPointsModal.jsx
index ffc43bc..22199a3 100644
--- a/src/views/Modals/RedeemPointsModal.jsx
+++ b/src/views/Modals/RedeemPointsModal.jsx
@@ -11,6 +11,7 @@ import {
Stack,
Typography,
} from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import { useEffect, useState } from 'react'
import ModalActions from '../../components/common/ModalActions.jsx'
@@ -18,6 +19,7 @@ import { useResponsiveModal } from '../../hooks/useResponsiveModal.js'
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
function RedeemPointsModal({ config }) {
+ const { t } = useTranslation('points')
const { ResponsiveModal } = useResponsiveModal()
const [points, setPoints] = useState(0)
@@ -56,12 +58,12 @@ function RedeemPointsModal({ config }) {
open={config?.isOpen}
onClose={config?.onClose}
size='md'
- title='Redeem Points'
+ title={t('redeem')}
footer={
,
disabled: !canRedeem,
onClick: () =>
@@ -103,7 +105,7 @@ function RedeemPointsModal({ config }) {
startDecorator={}
sx={{ mt: 0.5 }}
>
- {config?.available || 0} points available
+ {t('redeemModal.pointsAvailable', { count: config?.available || 0 })}
@@ -112,7 +114,7 @@ function RedeemPointsModal({ config }) {
{/* Points Input Section */}
- Points to Redeem
+ {t('redeemModal.pointsToRedeem')}
handlePointsChange(e.target.value)}
@@ -140,7 +142,7 @@ function RedeemPointsModal({ config }) {
/>
{points > config?.available && (
- Cannot exceed available points
+ {t('redeemModal.cannotExceed')}
)}
@@ -148,7 +150,7 @@ function RedeemPointsModal({ config }) {
{/* Quick Selection Buttons */}
- Quick Add:
+ {t('redeemModal.quickAdd')}
- You are about to redeem
+ {t('redeemModal.aboutToRedeem')}
- {points} points
+ {t('redeemModal.amount', { count: points })}
- Remaining: {(config?.available || 0) - points} points
+ {t('redeemModal.remaining', {
+ count: (config?.available || 0) - points,
+ })}
)}
diff --git a/src/views/Projects/ProjectView.jsx b/src/views/Projects/ProjectView.jsx
index be39ce7..ac48fdf 100644
--- a/src/views/Projects/ProjectView.jsx
+++ b/src/views/Projects/ProjectView.jsx
@@ -11,6 +11,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import ProjectModal from '../Modals/Inputs/ProjectModal'
@@ -40,6 +41,7 @@ const ProjectCardContent = ({
onCardClick,
onToggleActions,
}) => {
+ const { t } = useTranslation('projects')
// Check if current user owns this project
const isOwnedByCurrentUser = project.created_by === currentUserId
const isDefaultProject = project.id === 'default'
@@ -144,7 +146,7 @@ const ProjectCardContent = ({
fontWeight: 'md',
}}
>
- Default
+ {t('defaultChip')}
)}
@@ -178,7 +180,7 @@ const ProjectCardContent = ({
color: 'primary.500',
}}
>
- {taskCount} tasks
+ {t('tasks', { count: taskCount })}
{!isOwnedByCurrentUser && !isDefaultProject && (
@@ -193,7 +195,7 @@ const ProjectCardContent = ({
fontWeight: 'md',
}}
>
- Shared
+ {t('shared')}
)}
@@ -218,6 +220,7 @@ const ProjectCardContent = ({
}
const ProjectView = () => {
+ const { t } = useTranslation('projects')
const { data: projects, isProjectsLoading, isError } = useProjects()
const { data: userProfile } = useUserProfile()
const { data: chores = { res: [] } } = useChores(false) // false to exclude archived
@@ -250,11 +253,11 @@ const ProjectView = () => {
const project = userProjects.find(p => p.id === id)
setConfirmationModel({
isOpen: true,
- title: 'Delete Project',
- message: `Are you sure you want to delete "${project?.name}"? This will remove the project but keep all tasks (they'll move to the Default Project).`,
- confirmText: 'Delete',
+ title: t('delete.title'),
+ message: t('delete.message', { name: project?.name }),
+ confirmText: t('common:delete'),
color: 'danger',
- cancelText: 'Cancel',
+ cancelText: t('common:cancel'),
onClose: confirmed => {
if (confirmed === true) {
handleDeleteProject(id)
@@ -347,7 +350,7 @@ const ProjectView = () => {
if (isError) {
return (
- Failed to load projects. Please try again.
+ {t('loadError')}
)
}
@@ -360,11 +363,10 @@ const ProjectView = () => {
level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Projects
+ {t('common:navigation.projects')}
- Organize your tasks into projects. Create custom workspaces to keep
- your tasks organized and easily accessible.
+ {t('blurb')}
@@ -378,8 +380,8 @@ const ProjectView = () => {
{
onCardClick={() =>
handleCardClick({
id: 'default',
- name: 'Default Project',
+ name: t('chores:toolbar.defaultProject'),
icon: 'FolderOpen',
color: '#1976d2',
})
@@ -429,7 +431,7 @@ const ProjectView = () => {
>
- Edit
+ {t('common:edit')}
@@ -450,7 +452,7 @@ const ProjectView = () => {
>
- Delete
+ {t('common:delete')}
diff --git a/src/views/Things/ThingsHistory.jsx b/src/views/Things/ThingsHistory.jsx
index 0b53c8e..38b7379 100644
--- a/src/views/Things/ThingsHistory.jsx
+++ b/src/views/Things/ThingsHistory.jsx
@@ -10,6 +10,7 @@ import {
TrendingUp,
Update,
} from '@mui/icons-material'
+import { useTranslation } from 'react-i18next'
import {
Avatar,
Box,
@@ -41,6 +42,7 @@ import { useThingHistory } from '../../queries/ThingQueries'
import LoadingComponent from '../components/Loading'
const ThingsHistory = () => {
+ const { t } = useTranslation('things')
const { id } = useParams()
const theme = useTheme()
const { fmt } = useLocalization()
@@ -186,7 +188,7 @@ const ThingsHistory = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Things Overview
+ {t('history.overview')}
@@ -266,7 +268,7 @@ const ThingsHistory = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Data Visualization
+ {t('history.visualization')}
)}
@@ -328,7 +330,7 @@ const ThingsHistory = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Change History
+ {t('history.changeHistory')}
@@ -379,7 +381,7 @@ const ThingsHistory = () => {
display: { xs: 'none', sm: 'block' },
}}
>
- Updated
+ {t('history.updated')}
{
const aggregated = {}
for (let i = 0; i < history.length; i++) {
const item = history[i]
- const date = new Date(
- item.performedAt || item.updatedAt,
- ).toLocaleDateString()
+ // Key by a stable local ISO day (YYYY-MM-DD) so the render-time
+ // formatter (fmt.date) receives a parseable date instead of a
+ // locale-formatted string, which produced "Invalid date".
+ const d = new Date(item.performedAt || item.updatedAt || item.createdAt)
+ const date = isNaN(d.getTime())
+ ? 'unknown'
+ : `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
+ d.getDate(),
+ ).padStart(2, '0')}`
if (!aggregated[date]) {
aggregated[date] = []
}
@@ -83,6 +90,7 @@ const ChoreHistoryItem = ({
onViewNote,
onViewDetails,
}) => {
+ const { t } = useTranslation('history')
const cfg = statusConfig[status] ?? statusConfig[1]
return (
@@ -136,7 +144,7 @@ const ChoreHistoryItem = ({
{points && (
}>
- {`${points} points`}
+ {t('detail.points', { count: points })}
)}
{notes && (
@@ -151,7 +159,7 @@ const ChoreHistoryItem = ({
onViewNote?.(notes)
}}
>
- Note
+ {t('detail.note')}
)}
@@ -174,7 +182,7 @@ const ChoreHistoryTimeline = ({
{Object.entries(groupedHistory).map(([date, items]) => (
- {fmt.date(date)}
+ {date === 'unknown' ? '—' : fmt.date(date)}
@@ -197,7 +205,7 @@ const ChoreHistoryTimeline = ({
)
}
-const renderPieChart = (data, size, isPrimary, chartType = null) => {
+const renderPieChart = (t, data, size, isPrimary, chartType = null) => {
// Filter out items with zero or negative values
const validData = data.filter(item => item.value > 0)
@@ -216,7 +224,7 @@ const renderPieChart = (data, size, isPrimary, chartType = null) => {
}}
>
- No data available
+ {t('charts.noData')}
)
@@ -400,6 +408,7 @@ const USER_FILTER = (history, userId) => {
}
const UserActivites = () => {
+ const { t } = useTranslation('history')
const { data: userProfile } = useUserProfile()
const [tabValue, setTabValue] = React.useState(7)
@@ -448,16 +457,16 @@ const UserActivites = () => {
() => [
{
id: 'status',
- label: 'Status',
+ label: t('filter.status'),
type: 'multi-select',
icon: ,
options: [
- { value: 1, label: 'Completed', color: 'success', icon: },
- { value: 2, label: 'Skipped', color: 'warning', icon: },
- { value: 3, label: 'Pending', color: 'neutral', icon: },
- { value: 4, label: 'Rejected', color: 'danger', icon: },
- { value: 5, label: 'Missed', color: 'danger', icon: },
- { value: 6, label: 'Rescheduled', color: 'warning', icon: },
+ { value: 1, label: t('status.completed'), color: 'success', icon: },
+ { value: 2, label: t('status.skipped'), color: 'warning', icon: },
+ { value: 3, label: t('filter.pending'), color: 'neutral', icon: },
+ { value: 4, label: t('status.rejected'), color: 'danger', icon: },
+ { value: 5, label: t('status.missed'), color: 'danger', icon: },
+ { value: 6, label: t('status.rescheduled'), color: 'warning', icon: },
],
filterFn: (item, values) => values.includes(item.status),
},
@@ -465,7 +474,7 @@ const UserActivites = () => {
? [
{
id: 'label',
- label: 'Labels',
+ label: t('filter.labels'),
type: 'multi-select',
icon: ,
options: userLabels.map(l => ({
@@ -492,20 +501,20 @@ const UserActivites = () => {
: []),
{
id: 'hasNotes',
- label: 'Has Notes',
+ label: t('filter.hasNotes'),
type: 'boolean',
icon: ,
filterFn: item => !!item.notes,
},
{
id: 'hasPoints',
- label: 'Has Points',
+ label: t('filter.hasPoints'),
type: 'boolean',
icon: ,
filterFn: item => (item.points ?? 0) > 0,
},
],
- [userLabels],
+ [userLabels, t],
)
const {
@@ -520,20 +529,20 @@ const UserActivites = () => {
() => [
{
id: 'timePeriod',
- label: 'Time Period',
+ label: t('filter.timePeriod'),
type: 'single-select',
icon: ,
defaultValue: 7,
options: [
- { value: 7, label: '7 Days' },
- { value: 30, label: '30 Days' },
- { value: 90, label: '90 Days' },
- { value: 365, label: 'All Time' },
+ { value: 7, label: t('period.days', { count: 7 }) },
+ { value: 30, label: t('period.days', { count: 30 }) },
+ { value: 90, label: t('period.days', { count: 90 }) },
+ { value: 365, label: t('period.allTime') },
],
},
{
id: 'completedBy',
- label: 'User',
+ label: t('filter.user'),
type: 'single-select',
icon: ,
options: circleUsers.map(u => ({
@@ -544,7 +553,7 @@ const UserActivites = () => {
},
...clientFilterDefs,
],
- [circleUsers, clientFilterDefs],
+ [circleUsers, clientFilterDefs, t],
)
// Merge server-driven and client-driven active filter states for the bar
@@ -686,7 +695,7 @@ const UserActivites = () => {
// Add unlabeled tasks if there are any
if (unlabeledCount > 0) {
result.push({
- label: 'No Labels',
+ label: t('charts.noLabels'),
value: unlabeledCount,
color: TASK_COLOR.ANYTIME,
id: 'unlabeled',
@@ -709,7 +718,9 @@ const UserActivites = () => {
const assignee = circleUsers.find(
user => user.userId === chore.assignedTo,
)
- const assigneeName = assignee ? assignee.displayName : 'Unassigned'
+ const assigneeName = assignee
+ ? assignee.displayName
+ : t('charts.unassigned')
const assigneeId = chore.assignedTo || 'unassigned'
if (assigneeCounts[assigneeId]) {
@@ -802,7 +813,7 @@ const UserActivites = () => {
// Add unlabeled tasks duration if there is any
if (unlabeledDuration > 0) {
result.push({
- label: 'No Labels',
+ label: t('charts.noLabels'),
value: Math.round((unlabeledDuration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal
color: TASK_COLOR.ANYTIME,
id: 'unlabeled',
@@ -823,7 +834,7 @@ const UserActivites = () => {
// Iterate through ChoreHistory to get actual time spent per task
history.forEach(historyItem => {
const duration = historyItem.duration || 0 // duration in seconds from ChoreHistory
- const taskName = historyItem.choreName || 'Unknown Task'
+ const taskName = historyItem.choreName || t('charts.unknownTask')
if (taskDurations[taskName]) {
taskDurations[taskName].duration += duration
@@ -886,7 +897,7 @@ const UserActivites = () => {
if (totalCompleted > 0) {
result.push({
- label: `On time`,
+ label: t('badge.onTimeLabel'),
value: totalCompleted,
color: TASK_COLOR.COMPLETED,
id: 1,
@@ -895,7 +906,7 @@ const UserActivites = () => {
if (totalLate > 0) {
result.push({
- label: `Late`,
+ label: t('charts.late'),
value: totalLate,
color: TASK_COLOR.LATE,
id: 2,
@@ -904,7 +915,7 @@ const UserActivites = () => {
if (totalNoDueDate > 0) {
result.push({
- label: `Completed`,
+ label: t('status.completed'),
value: totalNoDueDate,
color: TASK_COLOR.ANYTIME,
id: 3,
@@ -919,43 +930,43 @@ const UserActivites = () => {
const chartData = {
history: {
data: historyPieChartData || [],
- title: 'Status',
- description: 'Completed tasks status',
+ title: t('charts.status.title'),
+ description: t('charts.status.description'),
},
due: {
data: choreDuePieChartData || [],
- title: 'Due Date',
- description: 'Current tasks due date',
+ title: t('charts.due.title'),
+ description: t('charts.due.description'),
},
// assigned: {
// data: choresAssignedChartData,
- // title: 'Assigned to me',
+ // title: t('chores:sort.assignedToMe'),
// description: 'Tasks assigned to you vs others',
// },
priority: {
data: choresPriorityChartData || [],
- title: 'Priority',
- description: 'Tasks by priority',
+ title: t('charts.priority.title'),
+ description: t('charts.priority.description'),
},
labels: {
data: choresLabelsChartData || [],
- title: 'Labels',
- description: 'Tasks by labels',
+ title: t('charts.labels.title'),
+ description: t('charts.labels.description'),
},
labelsDuration: {
data: choresLabelsDurationChartData || [],
- title: 'Labels (time)',
- description: 'Time spent by labels (hours)',
+ title: t('charts.labelsDuration.title'),
+ description: t('charts.labelsDuration.description'),
},
tasksTime: {
data: tasksTimeChartData || [],
- title: 'Tasks (time)',
- description: 'Time spent by individual tasks (hours)',
+ title: t('charts.tasksTime.title'),
+ description: t('charts.tasksTime.description'),
},
assigneeBreakdown: {
data: choresAssigneeBreakdownChartData || [],
- title: 'by Assignee',
- description: 'Tasks grouped by assignee',
+ title: t('charts.assigneeBreakdown.title'),
+ description: t('charts.assigneeBreakdown.description'),
},
}
if (!userProfile) {
@@ -976,7 +987,7 @@ const UserActivites = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Activities
+ {t('activities.title')}
@@ -1025,7 +1036,7 @@ const UserActivites = () => {
onViewNote={notes => {
setNoteViewerConfig({
isOpen: true,
- title: 'Note',
+ title: t('detail.note'),
content: notes,
onClose: () => setNoteViewerConfig({ isOpen: false }),
})
@@ -1153,6 +1164,7 @@ const UserActivites = () => {
}}
>
{renderPieChart(
+ t,
chartData[selectedChart].data,
300, // Increased size for better chart container
true,
@@ -1216,7 +1228,7 @@ const UserActivites = () => {
alignItems: 'center',
}}
>
- {renderPieChart(data, 70, false)}
+ {renderPieChart(t, data, 70, false)}
diff --git a/src/views/User/UserPoints.jsx b/src/views/User/UserPoints.jsx
index 7ab2dee..93c7979 100644
--- a/src/views/User/UserPoints.jsx
+++ b/src/views/User/UserPoints.jsx
@@ -40,6 +40,8 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import moment from 'moment'
+import { useTranslation } from 'react-i18next'
import LoadingComponent from '../components/Loading.jsx'
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
@@ -48,6 +50,7 @@ import { RedeemPoints } from '../../utils/Fetcher.jsx'
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
import RedeemPointsModal from '../Modals/RedeemPointsModal'
const UserPoints = () => {
+ const { t } = useTranslation('points')
const [tabValue, setTabValue] = useState(7)
const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false)
const [leaderboardMode, setLeaderboardMode] = useState('points') // 'points' or 'tasks'
@@ -104,15 +107,13 @@ const UserPoints = () => {
const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({
- label: currentDate.toLocaleString('en-US', { weekday: 'short' }),
+ label: moment(currentDate).format('ddd'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
- weekday: 'short',
- })
+ const dayName = moment(chore.performedAt).format('ddd')
const dayIndex = daysAggregated.findIndex(dayData => {
if (userId)
@@ -133,15 +134,13 @@ const UserPoints = () => {
const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({
- label: currentDate.toLocaleString('en-US', { day: 'numeric' }),
+ label: moment(currentDate).format('D'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
- day: 'numeric',
- })
+ const dayName = moment(chore.performedAt).format('D')
const dayIndex = daysAggregated.findIndex(dayData => {
if (userId)
@@ -164,15 +163,13 @@ const UserPoints = () => {
const currentMonth = new Date()
currentMonth.setMonth(currentMonth.getMonth() - i)
monthlyAggregated.push({
- label: currentMonth.toLocaleString('en-US', { month: 'short' }),
+ label: moment(currentMonth).format('MMM'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const monthName = new Date(chore.performedAt).toLocaleString('en-US', {
- month: 'short',
- })
+ const monthName = moment(chore.performedAt).format('MMM')
const monthIndex = monthlyAggregated.findIndex(monthData => {
if (userId)
@@ -195,15 +192,13 @@ const UserPoints = () => {
const currentYear = new Date()
currentYear.setFullYear(currentYear.getFullYear() - i)
yearlyAggregated.push({
- label: currentYear.toLocaleString('en-US', { year: 'numeric' }),
+ label: moment(currentYear).format('YYYY'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const yearName = new Date(chore.performedAt).toLocaleString('en-US', {
- year: 'numeric',
- })
+ const yearName = moment(chore.performedAt).format('YYYY')
const yearIndex = yearlyAggregated.findIndex(yearData => {
if (userId)
@@ -305,14 +300,14 @@ const UserPoints = () => {
level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- {leaderboardMode === 'points' ? 'Points' : 'Tasks'} Leaderboard
+ {leaderboardMode === 'points'
+ ? t('leaderboard.titlePoints')
+ : t('leaderboard.titleTasks')}
- Rankings based on{' '}
{leaderboardMode === 'points'
- ? 'points earned'
- : 'tasks completed'}{' '}
- during the selected time period
+ ? t('leaderboard.subtitlePoints')
+ : t('leaderboard.subtitleTasks')}
@@ -358,9 +353,9 @@ const UserPoints = () => {
}}
>
{[
- { label: '7D', value: 7 },
- { label: '6M', value: 6 * 30 },
- { label: 'All', value: 24 * 30 },
+ { label: t('tabs.short7d'), value: 7 },
+ { label: t('tabs.short6m'), value: 6 * 30 },
+ { label: t('tabs.shortAll'), value: 24 * 30 },
].map((tab, index) => (
{
sx={{ cursor: 'pointer' }}
onClick={() => setLeaderboardMode('points')}
>
- Points
+ {t('leaderboard.modePoints')}
{
sx={{ cursor: 'pointer' }}
onClick={() => setLeaderboardMode('tasks')}
>
- Tasks
+ {t('leaderboard.modeTasks')}
@@ -512,7 +507,7 @@ const UserPoints = () => {
color='primary'
sx={{ ml: 1 }}
>
- You
+ {t('leaderboard.you')}
)}
@@ -520,8 +515,10 @@ const UserPoints = () => {
level='body-xs'
sx={{ color: 'text.secondary' }}
>
- {user.periodTasks} tasks • {user.avgPointsPerTask} avg
- per task
+ {t('leaderboard.tasksAndAvg', {
+ tasks: user.periodTasks,
+ avg: user.avgPointsPerTask,
+ })}
@@ -552,8 +549,12 @@ const UserPoints = () => {
{leaderboardMode === 'points'
- ? `${user.availablePoints} available`
- : `${user.periodPoints} points`}
+ ? t('leaderboard.available', {
+ count: user.availablePoints,
+ })
+ : t('leaderboard.points', {
+ count: user.periodPoints,
+ })}
@@ -591,7 +592,7 @@ const UserPoints = () => {
- Filter & Analysis
+ {t('filter.analysisTitle')}
@@ -610,7 +611,7 @@ const UserPoints = () => {
>
- Filter Points
+ {t('filter.title')}
{
{/* User Filter */}
- Show points for:
+ {t('filter.showFor')}
diff --git a/src/views/components/ProjectSelector.jsx b/src/views/components/ProjectSelector.jsx
index 89c4754..385d3cd 100644
--- a/src/views/components/ProjectSelector.jsx
+++ b/src/views/components/ProjectSelector.jsx
@@ -10,6 +10,7 @@ import {
MenuItem,
Typography,
} from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
@@ -25,6 +26,7 @@ const ProjectSelector = ({
onProjectSelect,
showKeyboardShortcuts = false,
}) => {
+ const { t } = useTranslation('projects')
const { data: projects = [], isLoading } = useProjects()
const navigate = useNavigate()
@@ -265,7 +267,7 @@ const ProjectSelector = ({
- Projects
+ {t('selector.title')}
@@ -339,7 +341,7 @@ const ProjectSelector = ({
: 'var(--joy-palette-text-primary)',
}}
>
- Default Project
+ {t('chores:toolbar.defaultProject')}
{effectiveSelectedProject === 'Default Project' && (
- Create New Project
+ {t('selector.createNew')}
- Add a custom project workspace
+ {t('selector.createNewDescription')}
@@ -512,13 +514,13 @@ const ProjectSelector = ({
fontWeight: 500,
}}
>
- Manage Projects
+ {t('selector.manage')}
- View, edit, and organize all projects
+ {t('selector.manageDescription')}