feat: Add RTL drawer support, translate ChoreView, and apply date format preferences

Navigation:
- Fix drawer to slide from right in RTL languages (Arabic, Hebrew)
- Add anchor prop based on isRTL flag
- Drawer now properly positioned for RTL languages

ChoreView Translations:
- Add comprehensive Arabic and Spanish translations for ChoreView
- Translate info cards (Assignment, Schedule, Statistics, Details)
- Translate notification messages (Task Completed, Undo, etc.)
- Translate action buttons and confirmation modals
- Translate timer-related modals (Reset Timer, Clear All Time Records)

Date Format Application:
- Replace hardcoded moment date formats with formatDate()
- Apply user's selected date format preference in ChoreView
- Due date chip now respects user's date format setting
- Dates display consistently according to user preference

All UI text in ChoreView now translates to Arabic and Spanish.
Navigation drawer slides from correct side for RTL languages.
This commit is contained in:
Mo Tarbin
2026-03-08 16:51:18 +00:00
parent a82f6b16b7
commit 1214c17389
4 changed files with 121 additions and 32 deletions

View File

@@ -10,5 +10,46 @@
"assignedTo": "مُسند إلى",
"priority": "الأولوية",
"status": "الحالة",
"description": "الوصف"
"description": "الوصف",
"choreView": {
"assignment": "التعيين",
"assigned": "المعين",
"last": "الأخير",
"schedule": "الجدول",
"due": "الاستحقاق",
"statistics": "الإحصائيات",
"completed": "مكتمل",
"times": "مرات",
"details": "التفاصيل",
"createdBy": "أنشئ بواسطة",
"na": "غير متوفر",
"taskCompleted": "المهمة مكتملة",
"undoSuccessful": "التراجع ناجح",
"undoFailed": "فشل التراجع",
"resetTimer": "إعادة تعيين المؤقت",
"clearAllTimeRecords": "مسح جميع سجلات الوقت",
"descriptionTitle": "الوصف",
"previousNote": "الملاحظة السابقة",
"skipTask": "تخطي المهمة",
"markComplete": "وضع علامة مكتمل",
"edit": "تعديل",
"archive": "أرشفة",
"unarchive": "إلغاء الأرشفة",
"viewHistory": "عرض السجل",
"startTimer": "بدء المؤقت",
"pauseTimer": "إيقاف المؤقت مؤقتاً",
"approve": "موافقة",
"reject": "رفض",
"undo": "تراجع",
"skip": "تخطي",
"addNote": "أضف ملاحظة...",
"subtasks": "المهام الفرعية",
"noDescription": "لا يوجد وصف متاح",
"timer": {
"active": "المؤقت نشط",
"paused": "المؤقت متوقف مؤقتاً",
"reset": "إعادة تعيين المؤقت",
"delete": "حذف الجلسة"
}
}
}

View File

@@ -10,5 +10,46 @@
"assignedTo": "Asignado a",
"priority": "Prioridad",
"status": "Estado",
"description": "Descripción"
"description": "Descripción",
"choreView": {
"assignment": "Asignación",
"assigned": "Asignado",
"last": "Último",
"schedule": "Horario",
"due": "Vencimiento",
"statistics": "Estadísticas",
"completed": "Completado",
"times": "veces",
"details": "Detalles",
"createdBy": "Creado por",
"na": "N/D",
"taskCompleted": "Tarea Completada",
"undoSuccessful": "Deshacer Exitoso",
"undoFailed": "Deshacer Fallido",
"resetTimer": "Reiniciar Temporizador",
"clearAllTimeRecords": "Borrar Todos los Registros de Tiempo",
"descriptionTitle": "Descripción",
"previousNote": "Nota Anterior",
"skipTask": "Saltar Tarea",
"markComplete": "Marcar como Completada",
"edit": "Editar",
"archive": "Archivar",
"unarchive": "Desarchivar",
"viewHistory": "Ver Historial",
"startTimer": "Iniciar Temporizador",
"pauseTimer": "Pausar Temporizador",
"approve": "Aprobar",
"reject": "Rechazar",
"undo": "Deshacer",
"skip": "Saltar",
"addNote": "Agregar una nota...",
"subtasks": "Subtareas",
"noDescription": "No hay descripción disponible",
"timer": {
"active": "Temporizador Activo",
"paused": "Temporizador Pausado",
"reset": "Reiniciar Temporizador",
"delete": "Eliminar Sesión"
}
}
}

View File

@@ -39,9 +39,11 @@ import { Divider } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import moment from 'moment'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
import {
useChoreTimer,
@@ -75,6 +77,8 @@ import TimePassedCard from './TimePassedCard.jsx'
import TimerSplitButton from './TimerSplitButton.jsx'
const ChoreView = () => {
const { t } = useTranslation('chores')
const { formatDate } = useLocalization()
const [chore, setChore] = useState({})
const navigate = useNavigate()
const [performers, setPerformers] = useState([])
@@ -143,12 +147,12 @@ const ChoreView = () => {
{
size: 6,
icon: <PeopleAlt />,
title: 'Assignment',
text: `Assigned: ${
title: t('choreView.assignment'),
text: `${t('choreView.assigned')}: ${
performers.find(p => p.userId === chore.assignedTo)?.displayName ||
'N/A'
t('choreView.na')
}`,
subtext: ` Last: ${
subtext: ` ${t('choreView.last')}: ${
chore.lastCompletedDate
? performers.find(p => p.userId === chore.lastCompletedBy)
?.displayName
@@ -158,29 +162,29 @@ const ChoreView = () => {
{
size: 6,
icon: <CalendarMonth />,
title: 'Schedule',
text: `Due: ${
chore.nextDueDate ? moment(chore.nextDueDate).fromNow() : 'N/A'
title: t('choreView.schedule'),
text: `${t('choreView.due')}: ${
chore.nextDueDate ? moment(chore.nextDueDate).fromNow() : t('choreView.na')
}`,
subtext: `Last: ${
subtext: `${t('choreView.last')}: ${
chore.lastCompletedDate
? moment(chore.lastCompletedDate).fromNow()
: 'N/A'
: t('choreView.na')
}`,
},
{
size: 6,
icon: <Checklist />,
title: 'Statistics',
text: `Completed: ${chore.totalCompletedCount || 0} times`,
title: t('choreView.statistics'),
text: `${t('choreView.completed')}: ${chore.totalCompletedCount || 0} ${t('choreView.times')}`,
},
{
size: 6,
icon: <Person />,
title: 'Details',
subtext: `Created By: ${
title: t('choreView.details'),
subtext: `${t('choreView.createdBy')}: ${
performers.find(p => p.userId === chore.createdBy)?.displayName ||
'N/A'
t('choreView.na')
}`,
},
]
@@ -220,7 +224,7 @@ const ChoreView = () => {
.then(() => {
// Show undo notification
showSuccess({
title: 'Task Completed',
title: t('choreView.taskCompleted'),
message: 'Your task has been marked as complete',
undoAction: async () => {
try {
@@ -234,7 +238,7 @@ const ChoreView = () => {
queryClient.invalidateQueries(['chores'])
}
showUndo({
title: 'Undo Successful',
title: t('choreView.undoSuccessful'),
message: 'Task completion has been undone.',
})
} else {
@@ -242,7 +246,7 @@ const ChoreView = () => {
}
} catch (error) {
showError({
title: 'Undo Failed',
title: t('choreView.undoFailed'),
message: 'Unable to undo the action. Please try again.',
})
}
@@ -261,7 +265,7 @@ const ChoreView = () => {
// Show undo notification
showSuccess({
message: 'Task skipped',
message: t('choreView.skipTask'),
undoAction: async () => {
try {
const undoResponse = await UndoChoreAction(choreId)
@@ -274,7 +278,7 @@ const ChoreView = () => {
queryClient.invalidateQueries(['chores'])
}
showUndo({
title: 'Undo Successful',
title: t('choreView.undoSuccessful'),
message: 'Task skip has been undone.',
})
} else {
@@ -282,7 +286,7 @@ const ChoreView = () => {
}
} catch (error) {
showError({
title: 'Undo Failed',
title: t('choreView.undoFailed'),
message: 'Unable to undo the action. Please try again.',
})
}
@@ -319,11 +323,11 @@ const ChoreView = () => {
const handleResetTimer = () => {
setTimerActionConfig({
isOpen: true,
title: 'Reset Timer',
title: t('choreView.resetTimer'),
message:
'Are you sure you want to reset the timer? This will clear all time records since you started the task.',
confirmText: 'Reset Timer',
cancelText: 'Cancel',
confirmText: t('choreView.resetTimer'),
cancelText: t('common:cancel'),
onClose: confirmed => {
if (confirmed) {
resetChoreTimer.mutate(choreId, {
@@ -344,11 +348,11 @@ const ChoreView = () => {
const handleClearAllTime = () => {
setTimerActionConfig({
isOpen: true,
title: 'Clear All Time Records',
title: t('choreView.clearAllTimeRecords'),
message:
'This will permanently delete all timers for this task and set it back to "not started".',
confirmText: 'Clear All Time',
cancelText: 'Cancel',
confirmText: t('choreView.clearAllTimeRecords'),
cancelText: t('common:cancel'),
onClose: async confirmed => {
if (confirmed) {
if (choreTimer?.res?.id) {
@@ -468,13 +472,13 @@ const ChoreView = () => {
color='warning'
sx={{ mb: 1 }}
>
Archived
{t('choreView.archive')}
</Chip>
)}
<Chip startDecorator={<CalendarMonth />} size='md' sx={{ mb: 1 }}>
{chore.nextDueDate
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
: 'N/A'}
? `${t('choreView.due')} ${formatDate(chore.nextDueDate, true)}`
: t('choreView.na')}
</Chip>
<Box
sx={{

View File

@@ -29,6 +29,7 @@ import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { version } from '../../../package.json'
import UserProfileAvatar from '../../components/UserProfileAvatar'
import { useLocalization } from '../../contexts/LocalizationContext'
import NavBarLink from './NavBarLink'
import { SafeArea } from 'capacitor-plugin-safe-area'
@@ -39,6 +40,7 @@ import { apiClient } from '../../utils/ApiClient'
const publicPages = ['/landing', '/privacy', '/terms']
const NavBar = () => {
const { t } = useTranslation('common')
const { isRTL } = useLocalization()
const { data: resource } = useResource()
const navigate = useNavigate()
@@ -202,13 +204,14 @@ const NavBar = () => {
<Drawer
open={drawerOpen}
onClose={closeDrawer}
anchor={isRTL ? 'right' : 'left'}
size='sm'
onClick={closeDrawer}
sx={{
'& .MuiDrawer-content': {
position: 'fixed',
// pt: 'calc(var(--safe-area-inset-top, 0px))',
left: 0,
...(isRTL ? { right: 0 } : { left: 0 }),
// pb: 'calc(var(--safe-area-inset-bottom, 0px))',
// height:
// 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))',