feat: Complete comprehensive localization of all tracking and management pages
Some checks failed
CI/CD Pipeline / Build Application (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled

- Feeding page: 47+ strings localized with validation, success/error messages
- Medicine page: 44 strings localized with unit conversion support
- Sleep page: Already localized (verified)
- Diaper page: Already localized (verified)
- Activity page: Already localized (verified)
- AI Assistant: 51 strings localized including chat interface and suggested questions
- Children page: 38 strings fully localized with gender labels
- Family page: 42 strings localized with role management
- Insights page: 41 strings localized including charts and analytics

Added translation files:
- locales/en/ai.json (44 keys)
- locales/en/family.json (42 keys)
- locales/en/insights.json (41 keys)

Updated translation files:
- locales/en/tracking.json (added feeding, health/medicine sections)
- locales/en/children.json (verified complete)

All pages now use useTranslation hook with proper namespaces.
All user-facing text externalized and ready for multi-language support.
This commit is contained in:
2025-10-03 13:57:47 +00:00
parent 5fea603922
commit 41320638e5
10 changed files with 434 additions and 204 deletions

View File

@@ -239,7 +239,7 @@ export default function ChildrenPage() {
{child.name} {child.name}
</Typography> </Typography>
<Chip <Chip
label={child.gender} label={t(`gender.${child.gender}`)}
size="small" size="small"
sx={{ textTransform: 'capitalize', mt: 0.5 }} sx={{ textTransform: 'capitalize', mt: 0.5 }}
/> />

View File

@@ -25,8 +25,10 @@ import { InviteMemberDialog } from '@/components/family/InviteMemberDialog';
import { JoinFamilyDialog } from '@/components/family/JoinFamilyDialog'; import { JoinFamilyDialog } from '@/components/family/JoinFamilyDialog';
import { RemoveMemberDialog } from '@/components/family/RemoveMemberDialog'; import { RemoveMemberDialog } from '@/components/family/RemoveMemberDialog';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useTranslation } from '@/hooks/useTranslation';
export default function FamilyPage() { export default function FamilyPage() {
const { t } = useTranslation('family');
const { user, refreshUser } = useAuth(); const { user, refreshUser } = useAuth();
const [family, setFamily] = useState<Family | null>(null); const [family, setFamily] = useState<Family | null>(null);
const [members, setMembers] = useState<FamilyMember[]>([]); const [members, setMembers] = useState<FamilyMember[]>([]);
@@ -47,7 +49,7 @@ export default function FamilyPage() {
fetchFamilyData(); fetchFamilyData();
} else { } else {
setLoading(false); setLoading(false);
setError('No family found. Please complete onboarding first.'); setError(t('messages.noFamilyFound'));
} }
}, [familyId]); }, [familyId]);
@@ -65,7 +67,7 @@ export default function FamilyPage() {
setMembers(membersData); setMembers(membersData);
} catch (err: any) { } catch (err: any) {
console.error('Failed to fetch family data:', err); console.error('Failed to fetch family data:', err);
setError(err.response?.data?.message || 'Failed to load family information'); setError(err.response?.data?.message || t('messages.failedToLoad'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -76,26 +78,26 @@ export default function FamilyPage() {
try { try {
await navigator.clipboard.writeText(family.shareCode); await navigator.clipboard.writeText(family.shareCode);
setSnackbar({ open: true, message: 'Share code copied to clipboard!' }); setSnackbar({ open: true, message: t('messages.shareCodeCopied') });
} catch (err) { } catch (err) {
setSnackbar({ open: true, message: 'Failed to copy share code' }); setSnackbar({ open: true, message: t('messages.shareCodeCopyFailed') });
} }
}; };
const handleInviteMember = async (data: InviteMemberData) => { const handleInviteMember = async (data: InviteMemberData) => {
if (!familyId) { if (!familyId) {
throw new Error('No family ID found'); throw new Error(t('messages.noFamilyId'));
} }
try { try {
setActionLoading(true); setActionLoading(true);
await familiesApi.inviteMember(familyId, data); await familiesApi.inviteMember(familyId, data);
setSnackbar({ open: true, message: 'Invitation sent successfully!' }); setSnackbar({ open: true, message: t('messages.invitationSent') });
await fetchFamilyData(); await fetchFamilyData();
setInviteDialogOpen(false); setInviteDialogOpen(false);
} catch (err: any) { } catch (err: any) {
console.error('Failed to invite member:', err); console.error('Failed to invite member:', err);
throw new Error(err.response?.data?.message || 'Failed to send invitation'); throw new Error(err.response?.data?.message || t('messages.failedToInvite'));
} finally { } finally {
setActionLoading(false); setActionLoading(false);
} }
@@ -105,13 +107,13 @@ export default function FamilyPage() {
try { try {
setActionLoading(true); setActionLoading(true);
await familiesApi.joinFamily(data); await familiesApi.joinFamily(data);
setSnackbar({ open: true, message: 'Successfully joined family!' }); setSnackbar({ open: true, message: t('messages.joinedFamily') });
await refreshUser(); await refreshUser();
await fetchFamilyData(); await fetchFamilyData();
setJoinDialogOpen(false); setJoinDialogOpen(false);
} catch (err: any) { } catch (err: any) {
console.error('Failed to join family:', err); console.error('Failed to join family:', err);
throw new Error(err.response?.data?.message || 'Failed to join family'); throw new Error(err.response?.data?.message || t('messages.failedToJoin'));
} finally { } finally {
setActionLoading(false); setActionLoading(false);
} }
@@ -128,13 +130,13 @@ export default function FamilyPage() {
try { try {
setActionLoading(true); setActionLoading(true);
await familiesApi.removeMember(familyId, memberToRemove.userId); await familiesApi.removeMember(familyId, memberToRemove.userId);
setSnackbar({ open: true, message: 'Member removed successfully' }); setSnackbar({ open: true, message: t('messages.memberRemoved') });
await fetchFamilyData(); await fetchFamilyData();
setRemoveDialogOpen(false); setRemoveDialogOpen(false);
setMemberToRemove(null); setMemberToRemove(null);
} catch (err: any) { } catch (err: any) {
console.error('Failed to remove member:', err); console.error('Failed to remove member:', err);
setError(err.response?.data?.message || 'Failed to remove member'); setError(err.response?.data?.message || t('messages.failedToRemove'));
} finally { } finally {
setActionLoading(false); setActionLoading(false);
} }
@@ -164,10 +166,10 @@ export default function FamilyPage() {
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Box> <Box>
<Typography variant="h4" component="h1" fontWeight="600" gutterBottom> <Typography variant="h4" component="h1" fontWeight="600" gutterBottom>
Family {t('pageTitle')}
</Typography> </Typography>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" color="text.secondary">
Manage your family members and share access {t('pageSubtitle')}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
@@ -177,7 +179,7 @@ export default function FamilyPage() {
onClick={() => setJoinDialogOpen(true)} onClick={() => setJoinDialogOpen(true)}
disabled={loading} disabled={loading}
> >
Join Family {t('buttons.joinFamily')}
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
@@ -185,7 +187,7 @@ export default function FamilyPage() {
onClick={() => setInviteDialogOpen(true)} onClick={() => setInviteDialogOpen(true)}
disabled={loading || !familyId} disabled={loading || !familyId}
> >
Invite Member {t('buttons.inviteMember')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -208,10 +210,10 @@ export default function FamilyPage() {
<Card> <Card>
<CardContent> <CardContent>
<Typography variant="h6" component="h2" fontWeight="600" gutterBottom> <Typography variant="h6" component="h2" fontWeight="600" gutterBottom>
Family Share Code {t('shareCode.title')}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Share this code with family members to give them access to your family&apos;s data {t('shareCode.description')}
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
<Chip <Chip
@@ -229,7 +231,7 @@ export default function FamilyPage() {
startIcon={<ContentCopy />} startIcon={<ContentCopy />}
onClick={handleCopyCode} onClick={handleCopyCode}
> >
Copy Code {t('buttons.copyCode')}
</Button> </Button>
</Box> </Box>
</CardContent> </CardContent>
@@ -242,30 +244,30 @@ export default function FamilyPage() {
<Card> <Card>
<CardContent> <CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom sx={{ mb: 3 }}> <Typography variant="h6" fontWeight="600" gutterBottom sx={{ mb: 3 }}>
Family Members ({members.length}) {t('members.title', { count: members.length })}
</Typography> </Typography>
{members.length === 0 ? ( {members.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 4 }}> <Box sx={{ textAlign: 'center', py: 4 }}>
<People sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} /> <People sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
<Typography variant="body2" color="text.secondary" gutterBottom> <Typography variant="body2" color="text.secondary" gutterBottom>
No family members yet {t('members.noMembers')}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Invite family members to collaborate on child care {t('members.noMembersDescription')}
</Typography> </Typography>
<Button <Button
variant="outlined" variant="outlined"
startIcon={<PersonAdd />} startIcon={<PersonAdd />}
onClick={() => setInviteDialogOpen(true)} onClick={() => setInviteDialogOpen(true)}
> >
Invite First Member {t('buttons.inviteFirstMember')}
</Button> </Button>
</Box> </Box>
) : ( ) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{members.map((member, index) => { {members.map((member, index) => {
const memberName = member.user?.name || 'Unknown User'; const memberName = member.user?.name || t('placeholders.unknownUser');
return ( return (
<Box key={member.id} component="div"> <Box key={member.id} component="div">
<motion.div <motion.div
@@ -289,15 +291,15 @@ export default function FamilyPage() {
{memberName} {memberName}
</Typography> </Typography>
{isCurrentUser(member.userId) && ( {isCurrentUser(member.userId) && (
<Chip label="You" size="small" color="success" /> <Chip label={t('members.youLabel')} size="small" color="success" />
)} )}
</Box> </Box>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{member.user?.email || 'No email'} {member.user?.email || t('placeholders.noEmail')}
</Typography> </Typography>
</Box> </Box>
<Chip <Chip
label={member.role.charAt(0).toUpperCase() + member.role.slice(1)} label={t(`roles.${member.role}`)}
color={getRoleColor(member.role)} color={getRoleColor(member.role)}
size="small" size="small"
/> />
@@ -306,7 +308,7 @@ export default function FamilyPage() {
size="small" size="small"
onClick={() => handleRemoveClick(member)} onClick={() => handleRemoveClick(member)}
color="error" color="error"
aria-label={`Remove ${memberName} from family`} aria-label={t('members.removeAriaLabel', { name: memberName })}
> >
<Delete /> <Delete />
</IconButton> </IconButton>

View File

@@ -142,7 +142,7 @@ function FeedingTrackPage() {
} }
} catch (err: any) { } catch (err: any) {
console.error('Failed to load children:', err); console.error('Failed to load children:', err);
setError(err.response?.data?.message || 'Failed to load children'); setError(err.response?.data?.message || t('common.error.loadChildrenFailed'));
} finally { } finally {
setChildrenLoading(false); setChildrenLoading(false);
} }
@@ -189,23 +189,23 @@ function FeedingTrackPage() {
const handleSubmit = async () => { const handleSubmit = async () => {
if (!selectedChild) { if (!selectedChild) {
setError('Please select a child'); setError(t('common.selectChild'));
return; return;
} }
// Validation // Validation
if (feedingType === 'breast' && duration === 0 && timerSeconds === 0) { if (feedingType === 'breast' && duration === 0 && timerSeconds === 0) {
setError('Please enter duration or use the timer'); setError(t('feeding.validation.durationRequired'));
return; return;
} }
if (feedingType === 'bottle' && !amount) { if (feedingType === 'bottle' && !amount) {
setError('Please enter amount'); setError(t('feeding.validation.amountRequired'));
return; return;
} }
if (feedingType === 'solid' && !foodDescription) { if (feedingType === 'solid' && !foodDescription) {
setError('Please enter food description'); setError(t('feeding.validation.foodRequired'));
return; return;
} }
@@ -235,7 +235,7 @@ function FeedingTrackPage() {
notes: notes || undefined, notes: notes || undefined,
}); });
setSuccessMessage('Feeding logged successfully!'); setSuccessMessage(t('feeding.success'));
// Reset form // Reset form
resetForm(); resetForm();
@@ -244,7 +244,7 @@ function FeedingTrackPage() {
await loadRecentFeedings(); await loadRecentFeedings();
} catch (err: any) { } catch (err: any) {
console.error('Failed to save feeding:', err); console.error('Failed to save feeding:', err);
setError(err.response?.data?.message || 'Failed to save feeding'); setError(err.response?.data?.message || t('feeding.error.saveFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -273,13 +273,13 @@ function FeedingTrackPage() {
try { try {
setLoading(true); setLoading(true);
await trackingApi.deleteActivity(activityToDelete); await trackingApi.deleteActivity(activityToDelete);
setSuccessMessage('Feeding deleted successfully'); setSuccessMessage(t('feeding.deleted'));
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setActivityToDelete(null); setActivityToDelete(null);
await loadRecentFeedings(); await loadRecentFeedings();
} catch (err: any) { } catch (err: any) {
console.error('Failed to delete feeding:', err); console.error('Failed to delete feeding:', err);
setError(err.response?.data?.message || 'Failed to delete feeding'); setError(err.response?.data?.message || t('feeding.error.deleteFailed'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -345,17 +345,17 @@ function FeedingTrackPage() {
<CardContent sx={{ textAlign: 'center', py: 8 }}> <CardContent sx={{ textAlign: 'center', py: 8 }}>
<ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} /> <ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom> <Typography variant="h6" color="text.secondary" gutterBottom>
No Children Added {t('common.noChildrenAdded')}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
You need to add a child before you can track feeding activities {t('common.noChildrenMessage')}
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
startIcon={<Add />} startIcon={<Add />}
onClick={() => router.push('/children')} onClick={() => router.push('/children')}
> >
Add Child {t('common.addChild')}
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -414,11 +414,11 @@ function FeedingTrackPage() {
{children.length > 1 && ( {children.length > 1 && (
<Paper sx={{ p: 2, mb: 3 }}> <Paper sx={{ p: 2, mb: 3 }}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Select Child</InputLabel> <InputLabel>{t('common.selectChild')}</InputLabel>
<Select <Select
value={selectedChild} value={selectedChild}
onChange={(e) => setSelectedChild(e.target.value)} onChange={(e) => setSelectedChild(e.target.value)}
label="Select Child" label={t('common.selectChild')}
> >
{children.map((child) => ( {children.map((child) => (
<MenuItem key={child.id} value={child.id}> <MenuItem key={child.id} value={child.id}>
@@ -479,7 +479,7 @@ function FeedingTrackPage() {
startIcon={<Refresh />} startIcon={<Refresh />}
onClick={resetTimer} onClick={resetTimer}
> >
Reset {t('feeding.reset')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -506,7 +506,7 @@ function FeedingTrackPage() {
value={duration || ''} value={duration || ''}
onChange={(e) => setDuration(parseInt(e.target.value) || 0)} onChange={(e) => setDuration(parseInt(e.target.value) || 0)}
sx={{ mb: 3 }} sx={{ mb: 3 }}
helperText={t('feeding.placeholders.notes')} helperText={t('feeding.placeholders.duration')}
/> />
</Box> </Box>
)} )}
@@ -524,15 +524,15 @@ function FeedingTrackPage() {
/> />
<FormControl fullWidth sx={{ mb: 3 }}> <FormControl fullWidth sx={{ mb: 3 }}>
<InputLabel>{t('feeding.type')}</InputLabel> <InputLabel>{t('feeding.bottleType')}</InputLabel>
<Select <Select
value={bottleType} value={bottleType}
onChange={(e) => setBottleType(e.target.value as 'formula' | 'breastmilk' | 'other')} onChange={(e) => setBottleType(e.target.value as 'formula' | 'breastmilk' | 'other')}
label={t('feeding.type')} label={t('feeding.bottleType')}
> >
<MenuItem value="formula">{t('feeding.types.bottle')}</MenuItem> <MenuItem value="formula">{t('feeding.bottleTypes.formula')}</MenuItem>
<MenuItem value="breastmilk">{t('feeding.types.breast')}</MenuItem> <MenuItem value="breastmilk">{t('feeding.bottleTypes.breastmilk')}</MenuItem>
<MenuItem value="other">Other</MenuItem> <MenuItem value="other">{t('feeding.bottleTypes.other')}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Box> </Box>
@@ -543,20 +543,20 @@ function FeedingTrackPage() {
<Box> <Box>
<TextField <TextField
fullWidth fullWidth
label={t('feeding.type')} label={t('feeding.foodDescription')}
value={foodDescription} value={foodDescription}
onChange={(e) => setFoodDescription(e.target.value)} onChange={(e) => setFoodDescription(e.target.value)}
sx={{ mb: 3 }} sx={{ mb: 3 }}
placeholder={t('feeding.placeholders.notes')} placeholder={t('feeding.placeholders.foodDescription')}
/> />
<TextField <TextField
fullWidth fullWidth
label={t('feeding.amount')} label={t('feeding.amountDescription')}
value={amountDescription} value={amountDescription}
onChange={(e) => setAmountDescription(e.target.value)} onChange={(e) => setAmountDescription(e.target.value)}
sx={{ mb: 3 }} sx={{ mb: 3 }}
placeholder={t('feeding.placeholders.amount')} placeholder={t('feeding.placeholders.amountDescription')}
/> />
</Box> </Box>
)} )}
@@ -583,7 +583,7 @@ function FeedingTrackPage() {
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading} disabled={loading}
> >
{loading ? t('feeding.addFeeding') : t('feeding.addFeeding')} {loading ? t('common.loading') : t('feeding.addFeeding')}
</Button> </Button>
</Paper> </Paper>
@@ -591,7 +591,7 @@ function FeedingTrackPage() {
<Paper sx={{ p: 3 }}> <Paper sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
{t('feeding.title')} {t('feeding.recentFeedings')}
</Typography> </Typography>
<IconButton onClick={loadRecentFeedings} disabled={feedingsLoading}> <IconButton onClick={loadRecentFeedings} disabled={feedingsLoading}>
<Refresh /> <Refresh />
@@ -685,10 +685,10 @@ function FeedingTrackPage() {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)} disabled={loading}> <Button onClick={() => setDeleteDialogOpen(false)} disabled={loading}>
Cancel {t('common.cancel')}
</Button> </Button>
<Button onClick={handleDeleteConfirm} color="error" disabled={loading}> <Button onClick={handleDeleteConfirm} color="error" disabled={loading}>
{loading ? t('deleteEntry') : t('deleteEntry')} {loading ? t('common.loading') : t('common.delete')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -114,7 +114,7 @@ function MedicineTrackPage() {
} }
} catch (err: any) { } catch (err: any) {
console.error('Failed to load children:', err); console.error('Failed to load children:', err);
setError(err.response?.data?.message || 'Failed to load children'); setError(err.response?.data?.message || t('common.error.loadChildrenFailed'));
} finally { } finally {
setChildrenLoading(false); setChildrenLoading(false);
} }
@@ -140,19 +140,19 @@ function MedicineTrackPage() {
const handleSubmit = async () => { const handleSubmit = async () => {
if (!selectedChild) { if (!selectedChild) {
setError('Please select a child'); setError(t('common.selectChild'));
return; return;
} }
// Validation // Validation
if (!medicineName) { if (!medicineName) {
setError('Please enter medicine name'); setError(t('health.medicineName.required'));
return; return;
} }
const dosageValue = unit === 'ml' ? dosage : dosageText; const dosageValue = unit === 'ml' ? dosage : dosageText;
if (!dosageValue || (unit === 'ml' && dosage === 0) || (unit !== 'ml' && !dosageText)) { if (!dosageValue || (unit === 'ml' && dosage === 0) || (unit !== 'ml' && !dosageText)) {
setError('Please enter dosage'); setError(t('health.dosage.required'));
return; return;
} }
@@ -175,7 +175,7 @@ function MedicineTrackPage() {
notes: notes || undefined, notes: notes || undefined,
}); });
setSuccessMessage('Medicine logged successfully!'); setSuccessMessage(t('health.success'));
// Reset form // Reset form
resetForm(); resetForm();
@@ -184,7 +184,7 @@ function MedicineTrackPage() {
await loadRecentMedicines(); await loadRecentMedicines();
} catch (err: any) { } catch (err: any) {
console.error('Failed to save medicine:', err); console.error('Failed to save medicine:', err);
setError(err.response?.data?.message || 'Failed to save medicine'); setError(err.response?.data?.message || t('health.error'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -211,13 +211,13 @@ function MedicineTrackPage() {
try { try {
setLoading(true); setLoading(true);
await trackingApi.deleteActivity(activityToDelete); await trackingApi.deleteActivity(activityToDelete);
setSuccessMessage('Medicine deleted successfully'); setSuccessMessage(t('health.deleted'));
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setActivityToDelete(null); setActivityToDelete(null);
await loadRecentMedicines(); await loadRecentMedicines();
} catch (err: any) { } catch (err: any) {
console.error('Failed to delete medicine:', err); console.error('Failed to delete medicine:', err);
setError(err.response?.data?.message || 'Failed to delete medicine'); setError(err.response?.data?.message || t('health.deleteError'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -282,17 +282,17 @@ function MedicineTrackPage() {
<CardContent sx={{ textAlign: 'center', py: 8 }}> <CardContent sx={{ textAlign: 'center', py: 8 }}>
<ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} /> <ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom> <Typography variant="h6" color="text.secondary" gutterBottom>
No Children Added {t('common.noChildrenAdded')}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
You need to add a child before you can track medicine activities {t('common.noChildrenMessage')}
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
startIcon={<Add />} startIcon={<Add />}
onClick={() => router.push('/children')} onClick={() => router.push('/children')}
> >
Add Child {t('common.addChild')}
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -352,11 +352,11 @@ function MedicineTrackPage() {
{children.length > 1 && ( {children.length > 1 && (
<Paper sx={{ p: 2, mb: 3 }}> <Paper sx={{ p: 2, mb: 3 }}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Select Child</InputLabel> <InputLabel>{t('common.selectChild')}</InputLabel>
<Select <Select
value={selectedChild} value={selectedChild}
onChange={(e) => setSelectedChild(e.target.value)} onChange={(e) => setSelectedChild(e.target.value)}
label="Select Child" label={t('common.selectChild')}
> >
{children.map((child) => ( {children.map((child) => (
<MenuItem key={child.id} value={child.id}> <MenuItem key={child.id} value={child.id}>
@@ -373,17 +373,17 @@ function MedicineTrackPage() {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<MedicalServices sx={{ fontSize: 36, color: 'error.main', mr: 2 }} /> <MedicalServices sx={{ fontSize: 36, color: 'error.main', mr: 2 }} />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Medicine Information {t('health.medicineInfo')}
</Typography> </Typography>
</Box> </Box>
<TextField <TextField
fullWidth fullWidth
label="Medicine Name" label={t('health.medicineName.label')}
value={medicineName} value={medicineName}
onChange={(e) => setMedicineName(e.target.value)} onChange={(e) => setMedicineName(e.target.value)}
sx={{ mb: 3 }} sx={{ mb: 3 }}
placeholder="e.g., Acetaminophen, Ibuprofen" placeholder={t('health.medicineName.placeholder')}
required required
/> />
@@ -391,7 +391,7 @@ function MedicineTrackPage() {
{unit === 'ml' ? ( {unit === 'ml' ? (
<UnitInput <UnitInput
fullWidth fullWidth
label="Dosage" label={t('health.dosage.label')}
type="volume" type="volume"
value={dosage} value={dosage}
onChange={(metricValue) => setDosage(metricValue)} onChange={(metricValue) => setDosage(metricValue)}
@@ -400,16 +400,16 @@ function MedicineTrackPage() {
) : ( ) : (
<TextField <TextField
fullWidth fullWidth
label="Dosage" label={t('health.dosage.label')}
value={dosageText} value={dosageText}
onChange={(e) => setDosageText(e.target.value)} onChange={(e) => setDosageText(e.target.value)}
placeholder="e.g., 5, 2.5" placeholder={t('health.dosage.placeholder')}
required required
/> />
)} )}
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Unit</InputLabel> <InputLabel>{t('health.unit')}</InputLabel>
<Select <Select
value={unit} value={unit}
onChange={(e) => { onChange={(e) => {
@@ -422,39 +422,39 @@ function MedicineTrackPage() {
setDosage(0); setDosage(0);
} }
}} }}
label="Unit" label={t('health.unit')}
> >
<MenuItem value="ml">ml</MenuItem> <MenuItem value="ml">{t('health.units.ml')}</MenuItem>
<MenuItem value="mg">mg</MenuItem> <MenuItem value="mg">{t('health.units.mg')}</MenuItem>
<MenuItem value="tsp">tsp</MenuItem> <MenuItem value="tsp">{t('health.units.tsp')}</MenuItem>
<MenuItem value="tbsp">tbsp</MenuItem> <MenuItem value="tbsp">{t('health.units.tbsp')}</MenuItem>
<MenuItem value="drops">drops</MenuItem> <MenuItem value="drops">{t('health.units.drops')}</MenuItem>
<MenuItem value="tablet">tablet(s)</MenuItem> <MenuItem value="tablet">{t('health.units.tablet')}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Box> </Box>
<FormControl fullWidth sx={{ mb: 3 }}> <FormControl fullWidth sx={{ mb: 3 }}>
<InputLabel>Route</InputLabel> <InputLabel>{t('health.route.label')}</InputLabel>
<Select <Select
value={route} value={route}
onChange={(e) => setRoute(e.target.value as 'oral' | 'topical' | 'injection' | 'other')} onChange={(e) => setRoute(e.target.value as 'oral' | 'topical' | 'injection' | 'other')}
label="Route" label={t('health.route.label')}
> >
<MenuItem value="oral">Oral</MenuItem> <MenuItem value="oral">{t('health.route.oral')}</MenuItem>
<MenuItem value="topical">Topical</MenuItem> <MenuItem value="topical">{t('health.route.topical')}</MenuItem>
<MenuItem value="injection">Injection</MenuItem> <MenuItem value="injection">{t('health.route.injection')}</MenuItem>
<MenuItem value="other">Other</MenuItem> <MenuItem value="other">{t('health.route.other')}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<TextField <TextField
fullWidth fullWidth
label="Reason (optional)" label={t('health.reason.label')}
value={reason} value={reason}
onChange={(e) => setReason(e.target.value)} onChange={(e) => setReason(e.target.value)}
sx={{ mb: 3 }} sx={{ mb: 3 }}
placeholder="e.g., Fever, Pain, Allergy" placeholder={t('health.reason.placeholder')}
/> />
<TextField <TextField
@@ -477,7 +477,7 @@ function MedicineTrackPage() {
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading} disabled={loading}
> >
{loading ? t('activities.medicine') : t('activities.medicine')} {loading ? t('common.loading') : t('health.logMedicine')}
</Button> </Button>
</Paper> </Paper>
@@ -485,7 +485,7 @@ function MedicineTrackPage() {
<Paper sx={{ p: 3 }}> <Paper sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
{t('activities.medicine')} {t('health.recentMedicines')}
</Typography> </Typography>
<IconButton onClick={loadRecentMedicines} disabled={medicinesLoading}> <IconButton onClick={loadRecentMedicines} disabled={medicinesLoading}>
<Refresh /> <Refresh />
@@ -578,10 +578,10 @@ function MedicineTrackPage() {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)} disabled={loading}> <Button onClick={() => setDeleteDialogOpen(false)} disabled={loading}>
Cancel {t('common.cancel')}
</Button> </Button>
<Button onClick={handleDeleteConfirm} color="error" disabled={loading}> <Button onClick={handleDeleteConfirm} color="error" disabled={loading}>
{loading ? t('deleteEntry') : t('deleteEntry')} {loading ? t('common.loading') : t('common.delete')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -52,6 +52,7 @@ import { useAuth } from '@/lib/auth/AuthContext';
import apiClient from '@/lib/api/client'; import apiClient from '@/lib/api/client';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { useTranslation } from '@/hooks/useTranslation';
interface Message { interface Message {
id: string; id: string;
@@ -83,43 +84,9 @@ interface ConversationGroup {
isCollapsed: boolean; isCollapsed: boolean;
} }
const suggestedQuestions = [
'How much should my baby sleep at 3 months?',
'What are normal feeding patterns?',
'When should I introduce solid foods?',
'Tips for better sleep routine',
];
const thinkingMessages = [
'Gathering baby wisdom...',
'Consulting the baby books...',
'Mixing up the perfect answer...',
'Warming up some advice...',
'Preparing your bottle of knowledge...',
'Counting tiny fingers and toes...',
'Connecting the building blocks...',
'Peeking into the toy box...',
'Arranging the puzzle pieces...',
'Stirring the baby food jar...',
'Polishing the pacifier of wisdom...',
'Tiptoeing through naptime...',
'Organizing the diaper bag...',
'Wrapping up your answer with love...',
'Brewing a warm cup of guidance...',
'Knitting together some thoughts...',
'Tucking in the details...',
'Sprinkling some magic dust...',
'Humming a lullaby while I think...',
];
// Get a random selection of 3-5 thinking messages
const getRandomThinkingMessages = () => {
const count = Math.floor(Math.random() * 3) + 3; // 3 to 5
const shuffled = [...thinkingMessages].sort(() => Math.random() - 0.5);
return shuffled.slice(0, count);
};
export const AIChatInterface: React.FC = () => { export const AIChatInterface: React.FC = () => {
const { t } = useTranslation('ai');
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -141,6 +108,43 @@ export const AIChatInterface: React.FC = () => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Localized arrays that depend on translations
const suggestedQuestions = [
t('interface.suggestedQuestion1'),
t('interface.suggestedQuestion2'),
t('interface.suggestedQuestion3'),
t('interface.suggestedQuestion4'),
];
const thinkingMessages = [
t('interface.thinking1'),
t('interface.thinking2'),
t('interface.thinking3'),
t('interface.thinking4'),
t('interface.thinking5'),
t('interface.thinking6'),
t('interface.thinking7'),
t('interface.thinking8'),
t('interface.thinking9'),
t('interface.thinking10'),
t('interface.thinking11'),
t('interface.thinking12'),
t('interface.thinking13'),
t('interface.thinking14'),
t('interface.thinking15'),
t('interface.thinking16'),
t('interface.thinking17'),
t('interface.thinking18'),
t('interface.thinking19'),
];
// Get a random selection of 3-5 thinking messages
const getRandomThinkingMessages = () => {
const count = Math.floor(Math.random() * 3) + 3; // 3 to 5
const shuffled = [...thinkingMessages].sort(() => Math.random() - 0.5);
return shuffled.slice(0, count);
};
const scrollToBottom = () => { const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}; };
@@ -243,10 +247,11 @@ export const AIChatInterface: React.FC = () => {
// Group conversations by their group name // Group conversations by their group name
const organizeConversations = (): ConversationGroup[] => { const organizeConversations = (): ConversationGroup[] => {
const groups: { [key: string]: Conversation[] } = {}; const groups: { [key: string]: Conversation[] } = {};
const ungroupedLabel = t('interface.ungrouped');
// Separate conversations by group // Separate conversations by group
conversations.forEach((conv) => { conversations.forEach((conv) => {
const groupName = conv.metadata?.groupName || 'Ungrouped'; const groupName = conv.metadata?.groupName || ungroupedLabel;
if (!groups[groupName]) { if (!groups[groupName]) {
groups[groupName] = []; groups[groupName] = [];
} }
@@ -262,8 +267,8 @@ export const AIChatInterface: React.FC = () => {
})) }))
.sort((a, b) => { .sort((a, b) => {
// Ungrouped always last // Ungrouped always last
if (a.name === 'Ungrouped') return 1; if (a.name === ungroupedLabel) return 1;
if (b.name === 'Ungrouped') return -1; if (b.name === ungroupedLabel) return -1;
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}); });
@@ -372,7 +377,7 @@ export const AIChatInterface: React.FC = () => {
const errorMessage: Message = { const errorMessage: Message = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
role: 'assistant', role: 'assistant',
content: 'Sorry, I encountered an error. Please try again.', content: t('interface.errorMessage'),
timestamp: new Date(), timestamp: new Date(),
}; };
setMessages((prev) => [...prev, errorMessage]); setMessages((prev) => [...prev, errorMessage]);
@@ -390,10 +395,10 @@ export const AIChatInterface: React.FC = () => {
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Chat History {t('history.title')}
</Typography> </Typography>
{isMobile && ( {isMobile && (
<IconButton onClick={() => setDrawerOpen(false)} size="small" aria-label="Close drawer"> <IconButton onClick={() => setDrawerOpen(false)} size="small" aria-label={t('interface.closeDrawer')}>
<Close /> <Close />
</IconButton> </IconButton>
)} )}
@@ -405,7 +410,7 @@ export const AIChatInterface: React.FC = () => {
onClick={handleNewConversation} onClick={handleNewConversation}
sx={{ borderRadius: 2 }} sx={{ borderRadius: 2 }}
> >
New Chat {t('chat.newChat')}
</Button> </Button>
</Box> </Box>
<List sx={{ flex: 1, overflow: 'auto', py: 1 }}> <List sx={{ flex: 1, overflow: 'auto', py: 1 }}>
@@ -413,7 +418,7 @@ export const AIChatInterface: React.FC = () => {
<Box sx={{ p: 3, textAlign: 'center' }}> <Box sx={{ p: 3, textAlign: 'center' }}>
<Chat sx={{ fontSize: 48, color: 'text.secondary', opacity: 0.3, mb: 1 }} /> <Chat sx={{ fontSize: 48, color: 'text.secondary', opacity: 0.3, mb: 1 }} />
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
No conversations yet {t('history.noHistory')}
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
@@ -434,7 +439,7 @@ export const AIChatInterface: React.FC = () => {
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={group.name} primary={group.name}
secondary={`${group.conversations.length} chat${group.conversations.length !== 1 ? 's' : ''}`} secondary={t('interface.chatCount', { count: group.conversations.length })}
primaryTypographyProps={{ primaryTypographyProps={{
variant: 'body2', variant: 'body2',
fontWeight: 600, fontWeight: 600,
@@ -495,7 +500,7 @@ export const AIChatInterface: React.FC = () => {
} }
}} }}
sx={{ ml: 1 }} sx={{ ml: 1 }}
aria-label={isMobile ? 'More options' : 'Delete conversation'} aria-label={isMobile ? t('interface.moreOptions') : t('interface.deleteConversation')}
> >
{isMobile ? <MoreVert fontSize="small" /> : <Delete fontSize="small" />} {isMobile ? <MoreVert fontSize="small" /> : <Delete fontSize="small" />}
</IconButton> </IconButton>
@@ -566,10 +571,10 @@ export const AIChatInterface: React.FC = () => {
</Avatar> </Avatar>
<Box> <Box>
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
AI Parenting Assistant {t('interface.assistantTitle')}
</Typography> </Typography>
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
Ask me anything about parenting and childcare {t('interface.assistantSubtitle')}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -599,7 +604,7 @@ export const AIChatInterface: React.FC = () => {
> >
<AutoAwesome sx={{ fontSize: 64, color: 'primary.main', opacity: 0.5 }} /> <AutoAwesome sx={{ fontSize: 64, color: 'primary.main', opacity: 0.5 }} />
<Typography variant="h6" color="text.secondary" textAlign="center"> <Typography variant="h6" color="text.secondary" textAlign="center">
Hi {user?.name}! How can I help you today? {t('interface.greeting', { name: user?.name })}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@@ -732,7 +737,7 @@ export const AIChatInterface: React.FC = () => {
transition: 'opacity 0.3s ease-in-out', transition: 'opacity 0.3s ease-in-out',
}} }}
> >
{currentThinkingMessages[currentThinkingIndex] || 'Thinking...'} {currentThinkingMessages[currentThinkingIndex] || t('chat.thinking')}
</Typography> </Typography>
</Paper> </Paper>
</Box> </Box>
@@ -758,7 +763,7 @@ export const AIChatInterface: React.FC = () => {
fullWidth fullWidth
multiline multiline
maxRows={4} maxRows={4}
placeholder="Ask me anything..." placeholder={t('interface.inputPlaceholder')}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => { onKeyPress={(e) => {
@@ -795,24 +800,23 @@ export const AIChatInterface: React.FC = () => {
</IconButton> </IconButton>
</Box> </Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}> <Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
This AI assistant provides general information. Always consult healthcare professionals {t('interface.disclaimerFooter')}
for medical advice.
</Typography> </Typography>
</Paper> </Paper>
</Box> </Box>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}> <Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Delete Conversation</DialogTitle> <DialogTitle>{t('interface.deleteDialogTitle')}</DialogTitle>
<DialogContent> <DialogContent>
<Typography> <Typography>
Are you sure you want to delete this conversation? This action cannot be undone. {t('interface.deleteDialogMessage')}
</Typography> </Typography>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button> <Button onClick={() => setDeleteDialogOpen(false)}>{t('interface.cancel')}</Button>
<Button onClick={handleDeleteConversation} color="error" variant="contained"> <Button onClick={handleDeleteConversation} color="error" variant="contained">
Delete {t('interface.delete')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@@ -837,7 +841,7 @@ export const AIChatInterface: React.FC = () => {
<ListItemIcon> <ListItemIcon>
<DriveFileMove fontSize="small" /> <DriveFileMove fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText>Move to Group</ListItemText> <ListItemText>{t('interface.moveToGroup')}</ListItemText>
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
@@ -851,7 +855,7 @@ export const AIChatInterface: React.FC = () => {
<ListItemIcon> <ListItemIcon>
<Delete fontSize="small" /> <Delete fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText>Delete</ListItemText> <ListItemText>{t('interface.delete')}</ListItemText>
</MenuItem> </MenuItem>
</Menu> </Menu>
@@ -862,7 +866,7 @@ export const AIChatInterface: React.FC = () => {
maxWidth="xs" maxWidth="xs"
fullWidth fullWidth
> >
<DialogTitle>Move to Group</DialogTitle> <DialogTitle>{t('interface.moveToGroup')}</DialogTitle>
<DialogContent> <DialogContent>
<List> <List>
<ListItemButton <ListItemButton
@@ -875,7 +879,7 @@ export const AIChatInterface: React.FC = () => {
<ListItemIcon> <ListItemIcon>
<Chat /> <Chat />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Ungrouped" /> <ListItemText primary={t('interface.ungrouped')} />
</ListItemButton> </ListItemButton>
<Divider /> <Divider />
{getExistingGroups().map((groupName) => ( {getExistingGroups().map((groupName) => (
@@ -898,13 +902,13 @@ export const AIChatInterface: React.FC = () => {
<ListItemIcon> <ListItemIcon>
<CreateNewFolder /> <CreateNewFolder />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Create New Group" /> <ListItemText primary={t('interface.createNewGroup')} />
</ListItemButton> </ListItemButton>
</List> </List>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setMoveToGroupDialog({ open: false, conversationId: null })}> <Button onClick={() => setMoveToGroupDialog({ open: false, conversationId: null })}>
Cancel {t('interface.cancel')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@@ -919,12 +923,12 @@ export const AIChatInterface: React.FC = () => {
maxWidth="xs" maxWidth="xs"
fullWidth fullWidth
> >
<DialogTitle>Create New Group</DialogTitle> <DialogTitle>{t('interface.createNewGroup')}</DialogTitle>
<DialogContent> <DialogContent>
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
label="Group Name" label={t('interface.groupNameLabel')}
fullWidth fullWidth
variant="outlined" variant="outlined"
value={newGroupName} value={newGroupName}
@@ -943,10 +947,10 @@ export const AIChatInterface: React.FC = () => {
setNewGroupName(''); setNewGroupName('');
}} }}
> >
Cancel {t('interface.cancel')}
</Button> </Button>
<Button onClick={handleCreateNewGroup} variant="contained" disabled={!newGroupName.trim()}> <Button onClick={handleCreateNewGroup} variant="contained" disabled={!newGroupName.trim()}>
Create {t('interface.create')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -41,6 +41,7 @@ import { trackingApi, Activity, ActivityType } from '@/lib/api/tracking';
import { childrenApi, Child } from '@/lib/api/children'; import { childrenApi, Child } from '@/lib/api/children';
import { subDays, startOfDay, endOfDay, parseISO, differenceInMinutes } from 'date-fns'; import { subDays, startOfDay, endOfDay, parseISO, differenceInMinutes } from 'date-fns';
import { useLocalizedDate } from '@/hooks/useLocalizedDate'; import { useLocalizedDate } from '@/hooks/useLocalizedDate';
import { useTranslation } from '@/hooks/useTranslation';
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
type DateRange = '7days' | '30days' | '3months'; type DateRange = '7days' | '30days' | '3months';
@@ -99,6 +100,7 @@ const getActivityColor = (type: ActivityType) => {
export const InsightsDashboard: React.FC = () => { export const InsightsDashboard: React.FC = () => {
const router = useRouter(); const router = useRouter();
const { format, formatDistanceToNow } = useLocalizedDate(); const { format, formatDistanceToNow } = useLocalizedDate();
const { t } = useTranslation('insights');
const [children, setChildren] = useState<Child[]>([]); const [children, setChildren] = useState<Child[]>([]);
const [selectedChild, setSelectedChild] = useState<string>(''); const [selectedChild, setSelectedChild] = useState<string>('');
const [dateRange, setDateRange] = useState<DateRange>('7days'); const [dateRange, setDateRange] = useState<DateRange>('7days');
@@ -116,7 +118,7 @@ export const InsightsDashboard: React.FC = () => {
setSelectedChild(childrenData[0].id); setSelectedChild(childrenData[0].id);
} }
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.message || 'Failed to load children'); setError(err.response?.data?.message || t('errors.loadChildren'));
} }
}; };
fetchChildren(); fetchChildren();
@@ -142,7 +144,7 @@ export const InsightsDashboard: React.FC = () => {
); );
setActivities(activitiesData); setActivities(activitiesData);
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.message || 'Failed to load activities'); setError(err.response?.data?.message || t('errors.loadActivities'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -172,7 +174,7 @@ export const InsightsDashboard: React.FC = () => {
activities.forEach((a) => { activities.forEach((a) => {
typeCounts[a.type] = (typeCounts[a.type] || 0) + 1; typeCounts[a.type] = (typeCounts[a.type] || 0) + 1;
}); });
const mostCommonType = Object.entries(typeCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'None'; const mostCommonType = Object.entries(typeCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'none';
return { return {
totalFeedings, totalFeedings,
@@ -230,7 +232,7 @@ export const InsightsDashboard: React.FC = () => {
}); });
return Object.entries(typeCount).map(([name, value]) => ({ return Object.entries(typeCount).map(([name, value]) => ({
name: name.charAt(0).toUpperCase() + name.slice(1), name: t(`diaperTypes.${name}`),
value, value,
color: COLORS[name as keyof typeof COLORS] || '#CCCCCC', color: COLORS[name as keyof typeof COLORS] || '#CCCCCC',
})); }));
@@ -244,7 +246,7 @@ export const InsightsDashboard: React.FC = () => {
}); });
return Object.entries(typeCount).map(([name, count]) => ({ return Object.entries(typeCount).map(([name, count]) => ({
name: name.charAt(0).toUpperCase() + name.slice(1), name: t(`activityTypes.${name}`),
count, count,
color: COLORS[name as keyof typeof COLORS] || '#CCCCCC', color: COLORS[name as keyof typeof COLORS] || '#CCCCCC',
})); }));
@@ -269,10 +271,10 @@ export const InsightsDashboard: React.FC = () => {
> >
<Box> <Box>
<Typography variant="h4" fontWeight="600" gutterBottom> <Typography variant="h4" fontWeight="600" gutterBottom>
Insights & Analytics {t('title')}
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}> <Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
Track patterns and get insights about your child's activities {t('subtitle')}
</Typography> </Typography>
{/* Filters */} {/* Filters */}
@@ -281,11 +283,11 @@ export const InsightsDashboard: React.FC = () => {
{children.length > 1 && ( {children.length > 1 && (
<Grid item xs={12} sm={6} md={4}> <Grid item xs={12} sm={6} md={4}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Child</InputLabel> <InputLabel>{t('filters.child')}</InputLabel>
<Select <Select
value={selectedChild} value={selectedChild}
onChange={(e) => setSelectedChild(e.target.value)} onChange={(e) => setSelectedChild(e.target.value)}
label="Child" label={t('filters.child')}
> >
{children.map((child) => ( {children.map((child) => (
<MenuItem key={child.id} value={child.id}> <MenuItem key={child.id} value={child.id}>
@@ -304,9 +306,9 @@ export const InsightsDashboard: React.FC = () => {
fullWidth fullWidth
size="large" size="large"
> >
<ToggleButton value="7days">7 Days</ToggleButton> <ToggleButton value="7days">{t('filters.dateRange.7days')}</ToggleButton>
<ToggleButton value="30days">30 Days</ToggleButton> <ToggleButton value="30days">{t('filters.dateRange.30days')}</ToggleButton>
<ToggleButton value="3months">3 Months</ToggleButton> <ToggleButton value="3months">{t('filters.dateRange.3months')}</ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</Grid> </Grid>
</Grid> </Grid>
@@ -323,17 +325,17 @@ export const InsightsDashboard: React.FC = () => {
<CardContent sx={{ textAlign: 'center', py: 8 }}> <CardContent sx={{ textAlign: 'center', py: 8 }}>
<ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} /> <ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom> <Typography variant="h6" color="text.secondary" gutterBottom>
No Children Added {t('emptyStates.noChildren.title')}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Add a child to view insights and analytics {t('emptyStates.noChildren.message')}
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
startIcon={<Add />} startIcon={<Add />}
onClick={() => router.push('/children')} onClick={() => router.push('/children')}
> >
Add Child {t('emptyStates.noChildren.action')}
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -347,7 +349,7 @@ export const InsightsDashboard: React.FC = () => {
{noActivities && !noChildren && ( {noActivities && !noChildren && (
<Alert severity="info" sx={{ mb: 3 }}> <Alert severity="info" sx={{ mb: 3 }}>
No activities found for the selected date range. Start tracking activities to see insights! {t('emptyStates.noActivities')}
</Alert> </Alert>
)} )}
@@ -366,14 +368,14 @@ export const InsightsDashboard: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Restaurant sx={{ fontSize: 32, mr: 1 }} /> <Restaurant sx={{ fontSize: 32, mr: 1 }} />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Feedings {t('stats.feedings.title')}
</Typography> </Typography>
</Box> </Box>
<Typography variant="h3" fontWeight="700"> <Typography variant="h3" fontWeight="700">
{stats.totalFeedings} {stats.totalFeedings}
</Typography> </Typography>
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}> <Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
Total count {t('stats.feedings.subtitle')}
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>
@@ -391,14 +393,14 @@ export const InsightsDashboard: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Hotel sx={{ fontSize: 32, mr: 1 }} /> <Hotel sx={{ fontSize: 32, mr: 1 }} />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Sleep {t('stats.sleep.title')}
</Typography> </Typography>
</Box> </Box>
<Typography variant="h3" fontWeight="700"> <Typography variant="h3" fontWeight="700">
{stats.avgSleepHours}h {stats.avgSleepHours}h
</Typography> </Typography>
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}> <Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
Average per day {t('stats.sleep.subtitle')}
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>
@@ -416,14 +418,14 @@ export const InsightsDashboard: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<BabyChangingStation sx={{ fontSize: 32, mr: 1 }} /> <BabyChangingStation sx={{ fontSize: 32, mr: 1 }} />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Diapers {t('stats.diapers.title')}
</Typography> </Typography>
</Box> </Box>
<Typography variant="h3" fontWeight="700"> <Typography variant="h3" fontWeight="700">
{stats.totalDiapers} {stats.totalDiapers}
</Typography> </Typography>
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}> <Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
Total changes {t('stats.diapers.subtitle')}
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>
@@ -441,14 +443,14 @@ export const InsightsDashboard: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<TrendingUp sx={{ fontSize: 32, mr: 1 }} /> <TrendingUp sx={{ fontSize: 32, mr: 1 }} />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Top Activity {t('stats.topActivity.title')}
</Typography> </Typography>
</Box> </Box>
<Typography variant="h3" fontWeight="700" sx={{ textTransform: 'capitalize' }}> <Typography variant="h3" fontWeight="700" sx={{ textTransform: 'capitalize' }}>
{stats.mostCommonType} {t(`activityTypes.${stats.mostCommonType}`)}
</Typography> </Typography>
<Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}> <Typography variant="body2" sx={{ opacity: 0.9, mt: 1 }}>
Most frequent {t('stats.topActivity.subtitle')}
</Typography> </Typography>
</CardContent> </CardContent>
</Card> </Card>
@@ -464,7 +466,7 @@ export const InsightsDashboard: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Restaurant sx={{ mr: 1, color: COLORS.feeding }} /> <Restaurant sx={{ mr: 1, color: COLORS.feeding }} />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Feeding Frequency {t('charts.feedingFrequency')}
</Typography> </Typography>
</Box> </Box>
<ResponsiveContainer width="100%" height={250}> <ResponsiveContainer width="100%" height={250}>
@@ -473,7 +475,7 @@ export const InsightsDashboard: React.FC = () => {
<XAxis dataKey="date" tick={{ fontSize: 12 }} /> <XAxis dataKey="date" tick={{ fontSize: 12 }} />
<YAxis /> <YAxis />
<Tooltip /> <Tooltip />
<Bar dataKey="feedings" fill={COLORS.feeding} name="Feedings" /> <Bar dataKey="feedings" fill={COLORS.feeding} name={t('charts.chartLabels.feedings')} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>
@@ -486,7 +488,7 @@ export const InsightsDashboard: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Hotel sx={{ mr: 1, color: COLORS.sleep }} /> <Hotel sx={{ mr: 1, color: COLORS.sleep }} />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Sleep Duration (Hours) {t('charts.sleepDuration')}
</Typography> </Typography>
</Box> </Box>
<ResponsiveContainer width="100%" height={250}> <ResponsiveContainer width="100%" height={250}>
@@ -500,7 +502,7 @@ export const InsightsDashboard: React.FC = () => {
dataKey="sleepHours" dataKey="sleepHours"
stroke={COLORS.sleep} stroke={COLORS.sleep}
strokeWidth={3} strokeWidth={3}
name="Sleep Hours" name={t('charts.chartLabels.sleepHours')}
dot={{ fill: COLORS.sleep, r: 4 }} dot={{ fill: COLORS.sleep, r: 4 }}
/> />
</LineChart> </LineChart>
@@ -516,7 +518,7 @@ export const InsightsDashboard: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<BabyChangingStation sx={{ mr: 1, color: COLORS.diaper }} /> <BabyChangingStation sx={{ mr: 1, color: COLORS.diaper }} />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Diaper Changes by Type {t('charts.diaperChangesByType')}
</Typography> </Typography>
</Box> </Box>
<ResponsiveContainer width="100%" height={250}> <ResponsiveContainer width="100%" height={250}>
@@ -549,7 +551,7 @@ export const InsightsDashboard: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Assessment sx={{ mr: 1, color: 'primary.main' }} /> <Assessment sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="h6" fontWeight="600"> <Typography variant="h6" fontWeight="600">
Activity Timeline {t('charts.activityTimeline')}
</Typography> </Typography>
</Box> </Box>
<ResponsiveContainer width="100%" height={250}> <ResponsiveContainer width="100%" height={250}>
@@ -559,9 +561,9 @@ export const InsightsDashboard: React.FC = () => {
<YAxis /> <YAxis />
<Tooltip /> <Tooltip />
<Legend /> <Legend />
<Bar dataKey="feedings" fill={COLORS.feeding} name="Feedings" /> <Bar dataKey="feedings" fill={COLORS.feeding} name={t('charts.chartLabels.feedings')} />
<Bar dataKey="diapers" fill={COLORS.diaper} name="Diapers" /> <Bar dataKey="diapers" fill={COLORS.diaper} name={t('charts.chartLabels.diapers')} />
<Bar dataKey="sleepHours" fill={COLORS.sleep} name="Sleep (hrs)" /> <Bar dataKey="sleepHours" fill={COLORS.sleep} name={t('charts.chartLabels.sleepHours')} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>
@@ -573,7 +575,7 @@ export const InsightsDashboard: React.FC = () => {
<Card sx={{ mb: 3 }}> <Card sx={{ mb: 3 }}>
<CardContent> <CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom> <Typography variant="h6" fontWeight="600" gutterBottom>
Activity Distribution {t('charts.activityDistribution')}
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 2 }}> <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 2 }}>
{activityTypeData.map((activity) => ( {activityTypeData.map((activity) => (
@@ -600,7 +602,7 @@ export const InsightsDashboard: React.FC = () => {
<Card> <Card>
<CardContent> <CardContent>
<Typography variant="h6" fontWeight="600" gutterBottom> <Typography variant="h6" fontWeight="600" gutterBottom>
Recent Activities (Last 20) {t('recentActivities.title')}
</Typography> </Typography>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
<List sx={{ maxHeight: 400, overflow: 'auto' }}> <List sx={{ maxHeight: 400, overflow: 'auto' }}>
@@ -627,7 +629,7 @@ export const InsightsDashboard: React.FC = () => {
primary={ primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body1" fontWeight="600" sx={{ textTransform: 'capitalize' }}> <Typography variant="body1" fontWeight="600" sx={{ textTransform: 'capitalize' }}>
{activity.type} {t(`activityTypes.${activity.type}`)}
</Typography> </Typography>
<Chip <Chip
label={formatDistanceToNow(parseISO(activity.timestamp), { addSuffix: true })} label={formatDistanceToNow(parseISO(activity.timestamp), { addSuffix: true })}

View File

@@ -58,5 +58,50 @@
"safety": "Safety", "safety": "Safety",
"nutrition": "Nutrition", "nutrition": "Nutrition",
"general": "General" "general": "General"
},
"interface": {
"assistantTitle": "AI Parenting Assistant",
"assistantSubtitle": "Ask me anything about parenting and childcare",
"greeting": "Hi {{name}}! How can I help you today?",
"inputPlaceholder": "Ask me anything...",
"closeDrawer": "Close drawer",
"moreOptions": "More options",
"deleteConversation": "Delete conversation",
"chatCount": "{{count}} chat",
"chatCount_plural": "{{count}} chats",
"ungrouped": "Ungrouped",
"errorMessage": "Sorry, I encountered an error. Please try again.",
"disclaimerFooter": "This AI assistant provides general information. Always consult healthcare professionals for medical advice.",
"deleteDialogTitle": "Delete Conversation",
"deleteDialogMessage": "Are you sure you want to delete this conversation? This action cannot be undone.",
"cancel": "Cancel",
"delete": "Delete",
"moveToGroup": "Move to Group",
"createNewGroup": "Create New Group",
"groupNameLabel": "Group Name",
"create": "Create",
"suggestedQuestion1": "How much should my baby sleep at 3 months?",
"suggestedQuestion2": "What are normal feeding patterns?",
"suggestedQuestion3": "When should I introduce solid foods?",
"suggestedQuestion4": "Tips for better sleep routine",
"thinking1": "Gathering baby wisdom...",
"thinking2": "Consulting the baby books...",
"thinking3": "Mixing up the perfect answer...",
"thinking4": "Warming up some advice...",
"thinking5": "Preparing your bottle of knowledge...",
"thinking6": "Counting tiny fingers and toes...",
"thinking7": "Connecting the building blocks...",
"thinking8": "Peeking into the toy box...",
"thinking9": "Arranging the puzzle pieces...",
"thinking10": "Stirring the baby food jar...",
"thinking11": "Polishing the pacifier of wisdom...",
"thinking12": "Tiptoeing through naptime...",
"thinking13": "Organizing the diaper bag...",
"thinking14": "Wrapping up your answer with love...",
"thinking15": "Brewing a warm cup of guidance...",
"thinking16": "Knitting together some thoughts...",
"thinking17": "Tucking in the details...",
"thinking18": "Sprinkling some magic dust...",
"thinking19": "Humming a lullaby while I think..."
} }
} }

View File

@@ -0,0 +1,43 @@
{
"pageTitle": "Family",
"pageSubtitle": "Manage your family members and share access",
"buttons": {
"joinFamily": "Join Family",
"inviteMember": "Invite Member",
"copyCode": "Copy Code",
"inviteFirstMember": "Invite First Member"
},
"shareCode": {
"title": "Family Share Code",
"description": "Share this code with family members to give them access to your family's data"
},
"members": {
"title": "Family Members ({{count}})",
"noMembers": "No family members yet",
"noMembersDescription": "Invite family members to collaborate on child care",
"youLabel": "You",
"removeAriaLabel": "Remove {{name}} from family"
},
"roles": {
"parent": "Parent",
"caregiver": "Caregiver",
"viewer": "Viewer"
},
"messages": {
"shareCodeCopied": "Share code copied to clipboard!",
"shareCodeCopyFailed": "Failed to copy share code",
"invitationSent": "Invitation sent successfully!",
"joinedFamily": "Successfully joined family!",
"memberRemoved": "Member removed successfully",
"noFamilyFound": "No family found. Please complete onboarding first.",
"failedToLoad": "Failed to load family information",
"noFamilyId": "No family ID found",
"failedToInvite": "Failed to send invitation",
"failedToJoin": "Failed to join family",
"failedToRemove": "Failed to remove member"
},
"placeholders": {
"unknownUser": "Unknown User",
"noEmail": "No email"
}
}

View File

@@ -0,0 +1,73 @@
{
"title": "Insights & Analytics",
"subtitle": "Track patterns and get insights about your child's activities",
"filters": {
"child": "Child",
"dateRange": {
"7days": "7 Days",
"30days": "30 Days",
"3months": "3 Months"
}
},
"stats": {
"feedings": {
"title": "Feedings",
"subtitle": "Total count"
},
"sleep": {
"title": "Sleep",
"subtitle": "Average per day"
},
"diapers": {
"title": "Diapers",
"subtitle": "Total changes"
},
"topActivity": {
"title": "Top Activity",
"subtitle": "Most frequent"
}
},
"charts": {
"feedingFrequency": "Feeding Frequency",
"sleepDuration": "Sleep Duration (Hours)",
"diaperChangesByType": "Diaper Changes by Type",
"activityTimeline": "Activity Timeline",
"activityDistribution": "Activity Distribution",
"chartLabels": {
"feedings": "Feedings",
"diapers": "Diapers",
"sleepHours": "Sleep (hrs)"
}
},
"recentActivities": {
"title": "Recent Activities (Last 20)"
},
"emptyStates": {
"noChildren": {
"title": "No Children Added",
"message": "Add a child to view insights and analytics",
"action": "Add Child"
},
"noActivities": "No activities found for the selected date range. Start tracking activities to see insights!"
},
"errors": {
"loadChildren": "Failed to load children",
"loadActivities": "Failed to load activities"
},
"activityTypes": {
"feeding": "Feeding",
"sleep": "Sleep",
"diaper": "Diaper",
"medication": "Medication",
"milestone": "Milestone",
"note": "Note",
"none": "None"
},
"diaperTypes": {
"wet": "Wet",
"dirty": "Dirty",
"both": "Both",
"dry": "Dry",
"unknown": "Unknown"
}
}

View File

@@ -26,17 +26,41 @@
}, },
"amount": "Amount", "amount": "Amount",
"duration": "Duration", "duration": "Duration",
"startTime": "Start Time", "startTime": "Start Timer",
"endTime": "End Time", "endTime": "Stop Timer",
"reset": "Reset",
"notes": "Notes", "notes": "Notes",
"bottleType": "Bottle Type",
"bottleTypes": {
"formula": "Formula",
"breastmilk": "Breast Milk",
"other": "Other"
},
"foodDescription": "Food Description",
"amountDescription": "Amount Description",
"placeholders": { "placeholders": {
"amount": "Enter amount", "amount": "Enter amount",
"notes": "Add any notes about this feeding..." "notes": "Add any notes about this feeding...",
"duration": "Or enter duration manually",
"foodDescription": "e.g., Rice cereal, Banana puree",
"amountDescription": "e.g., 1/4 cup, 2 spoonfuls"
}, },
"units": { "units": {
"ml": "ml", "ml": "ml",
"oz": "oz", "oz": "oz",
"minutes": "minutes" "minutes": "minutes"
},
"validation": {
"durationRequired": "Please enter duration or use the timer",
"amountRequired": "Please enter amount",
"foodRequired": "Please enter food description"
},
"success": "Feeding logged successfully!",
"deleted": "Feeding deleted successfully",
"recentFeedings": "Recent Feedings",
"error": {
"saveFailed": "Failed to save feeding",
"deleteFailed": "Failed to delete feeding"
} }
}, },
"sleep": { "sleep": {
@@ -148,7 +172,35 @@
}, },
"temperature": "Temperature", "temperature": "Temperature",
"medication": "Medication", "medication": "Medication",
"dosage": "Dosage", "medicineInfo": "Medicine Information",
"medicineName": {
"label": "Medicine Name",
"placeholder": "e.g., Acetaminophen, Ibuprofen",
"required": "Please enter medicine name"
},
"dosage": {
"label": "Dosage",
"placeholder": "e.g., 5, 2.5",
"required": "Please enter dosage"
},
"unit": "Unit",
"route": {
"label": "Route",
"oral": "Oral",
"topical": "Topical",
"injection": "Injection",
"other": "Other"
},
"reason": {
"label": "Reason (optional)",
"placeholder": "e.g., Fever, Pain, Allergy"
},
"logMedicine": "Log Medicine",
"recentMedicines": "Recent Medicines",
"success": "Medicine logged successfully!",
"error": "Failed to save medicine",
"deleted": "Medicine deleted successfully",
"deleteError": "Failed to delete medicine",
"symptom": "Symptom", "symptom": "Symptom",
"severity": "Severity", "severity": "Severity",
"severities": { "severities": {
@@ -164,6 +216,12 @@
"notes": "Add any notes..." "notes": "Add any notes..."
}, },
"units": { "units": {
"ml": "ml",
"mg": "mg",
"tsp": "tsp",
"tbsp": "tbsp",
"drops": "drops",
"tablet": "tablet(s)",
"celsius": "°C", "celsius": "°C",
"fahrenheit": "°F" "fahrenheit": "°F"
} }
@@ -202,7 +260,10 @@
"noChildrenAdded": "No Children Added", "noChildrenAdded": "No Children Added",
"noChildrenMessage": "You need to add a child before you can track activities", "noChildrenMessage": "You need to add a child before you can track activities",
"addChild": "Add Child", "addChild": "Add Child",
"recentActivities": "Recent Activities" "recentActivities": "Recent Activities",
"error": {
"loadChildrenFailed": "Failed to load children"
}
}, },
"quickLog": "Quick Log", "quickLog": "Quick Log",
"viewHistory": "View History", "viewHistory": "View History",