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

View File

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

View File

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

View File

@@ -52,6 +52,7 @@ import { useAuth } from '@/lib/auth/AuthContext';
import apiClient from '@/lib/api/client';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useTranslation } from '@/hooks/useTranslation';
interface Message {
id: string;
@@ -83,43 +84,9 @@ interface ConversationGroup {
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 = () => {
const { t } = useTranslation('ai');
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -141,6 +108,43 @@ export const AIChatInterface: React.FC = () => {
const theme = useTheme();
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 = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
@@ -243,10 +247,11 @@ export const AIChatInterface: React.FC = () => {
// Group conversations by their group name
const organizeConversations = (): ConversationGroup[] => {
const groups: { [key: string]: Conversation[] } = {};
const ungroupedLabel = t('interface.ungrouped');
// Separate conversations by group
conversations.forEach((conv) => {
const groupName = conv.metadata?.groupName || 'Ungrouped';
const groupName = conv.metadata?.groupName || ungroupedLabel;
if (!groups[groupName]) {
groups[groupName] = [];
}
@@ -262,8 +267,8 @@ export const AIChatInterface: React.FC = () => {
}))
.sort((a, b) => {
// Ungrouped always last
if (a.name === 'Ungrouped') return 1;
if (b.name === 'Ungrouped') return -1;
if (a.name === ungroupedLabel) return 1;
if (b.name === ungroupedLabel) return -1;
return a.name.localeCompare(b.name);
});
@@ -372,7 +377,7 @@ export const AIChatInterface: React.FC = () => {
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: 'Sorry, I encountered an error. Please try again.',
content: t('interface.errorMessage'),
timestamp: new Date(),
};
setMessages((prev) => [...prev, errorMessage]);
@@ -390,10 +395,10 @@ export const AIChatInterface: React.FC = () => {
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" fontWeight="600">
Chat History
{t('history.title')}
</Typography>
{isMobile && (
<IconButton onClick={() => setDrawerOpen(false)} size="small" aria-label="Close drawer">
<IconButton onClick={() => setDrawerOpen(false)} size="small" aria-label={t('interface.closeDrawer')}>
<Close />
</IconButton>
)}
@@ -405,7 +410,7 @@ export const AIChatInterface: React.FC = () => {
onClick={handleNewConversation}
sx={{ borderRadius: 2 }}
>
New Chat
{t('chat.newChat')}
</Button>
</Box>
<List sx={{ flex: 1, overflow: 'auto', py: 1 }}>
@@ -413,7 +418,7 @@ export const AIChatInterface: React.FC = () => {
<Box sx={{ p: 3, textAlign: 'center' }}>
<Chat sx={{ fontSize: 48, color: 'text.secondary', opacity: 0.3, mb: 1 }} />
<Typography variant="body2" color="text.secondary">
No conversations yet
{t('history.noHistory')}
</Typography>
</Box>
) : (
@@ -434,7 +439,7 @@ export const AIChatInterface: React.FC = () => {
</ListItemIcon>
<ListItemText
primary={group.name}
secondary={`${group.conversations.length} chat${group.conversations.length !== 1 ? 's' : ''}`}
secondary={t('interface.chatCount', { count: group.conversations.length })}
primaryTypographyProps={{
variant: 'body2',
fontWeight: 600,
@@ -495,7 +500,7 @@ export const AIChatInterface: React.FC = () => {
}
}}
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" />}
</IconButton>
@@ -566,10 +571,10 @@ export const AIChatInterface: React.FC = () => {
</Avatar>
<Box>
<Typography variant="h6" fontWeight="600">
AI Parenting Assistant
{t('interface.assistantTitle')}
</Typography>
<Typography variant="caption" color="text.secondary">
Ask me anything about parenting and childcare
{t('interface.assistantSubtitle')}
</Typography>
</Box>
</Box>
@@ -599,7 +604,7 @@ export const AIChatInterface: React.FC = () => {
>
<AutoAwesome sx={{ fontSize: 64, color: 'primary.main', opacity: 0.5 }} />
<Typography variant="h6" color="text.secondary" textAlign="center">
Hi {user?.name}! How can I help you today?
{t('interface.greeting', { name: user?.name })}
</Typography>
<Box
sx={{
@@ -732,7 +737,7 @@ export const AIChatInterface: React.FC = () => {
transition: 'opacity 0.3s ease-in-out',
}}
>
{currentThinkingMessages[currentThinkingIndex] || 'Thinking...'}
{currentThinkingMessages[currentThinkingIndex] || t('chat.thinking')}
</Typography>
</Paper>
</Box>
@@ -758,7 +763,7 @@ export const AIChatInterface: React.FC = () => {
fullWidth
multiline
maxRows={4}
placeholder="Ask me anything..."
placeholder={t('interface.inputPlaceholder')}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => {
@@ -795,24 +800,23 @@ export const AIChatInterface: React.FC = () => {
</IconButton>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
This AI assistant provides general information. Always consult healthcare professionals
for medical advice.
{t('interface.disclaimerFooter')}
</Typography>
</Paper>
</Box>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Delete Conversation</DialogTitle>
<DialogTitle>{t('interface.deleteDialogTitle')}</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete this conversation? This action cannot be undone.
{t('interface.deleteDialogMessage')}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
<Button onClick={() => setDeleteDialogOpen(false)}>{t('interface.cancel')}</Button>
<Button onClick={handleDeleteConversation} color="error" variant="contained">
Delete
{t('interface.delete')}
</Button>
</DialogActions>
</Dialog>
@@ -837,7 +841,7 @@ export const AIChatInterface: React.FC = () => {
<ListItemIcon>
<DriveFileMove fontSize="small" />
</ListItemIcon>
<ListItemText>Move to Group</ListItemText>
<ListItemText>{t('interface.moveToGroup')}</ListItemText>
</MenuItem>
<MenuItem
onClick={() => {
@@ -851,7 +855,7 @@ export const AIChatInterface: React.FC = () => {
<ListItemIcon>
<Delete fontSize="small" />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
<ListItemText>{t('interface.delete')}</ListItemText>
</MenuItem>
</Menu>
@@ -862,7 +866,7 @@ export const AIChatInterface: React.FC = () => {
maxWidth="xs"
fullWidth
>
<DialogTitle>Move to Group</DialogTitle>
<DialogTitle>{t('interface.moveToGroup')}</DialogTitle>
<DialogContent>
<List>
<ListItemButton
@@ -875,7 +879,7 @@ export const AIChatInterface: React.FC = () => {
<ListItemIcon>
<Chat />
</ListItemIcon>
<ListItemText primary="Ungrouped" />
<ListItemText primary={t('interface.ungrouped')} />
</ListItemButton>
<Divider />
{getExistingGroups().map((groupName) => (
@@ -898,13 +902,13 @@ export const AIChatInterface: React.FC = () => {
<ListItemIcon>
<CreateNewFolder />
</ListItemIcon>
<ListItemText primary="Create New Group" />
<ListItemText primary={t('interface.createNewGroup')} />
</ListItemButton>
</List>
</DialogContent>
<DialogActions>
<Button onClick={() => setMoveToGroupDialog({ open: false, conversationId: null })}>
Cancel
{t('interface.cancel')}
</Button>
</DialogActions>
</Dialog>
@@ -919,12 +923,12 @@ export const AIChatInterface: React.FC = () => {
maxWidth="xs"
fullWidth
>
<DialogTitle>Create New Group</DialogTitle>
<DialogTitle>{t('interface.createNewGroup')}</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Group Name"
label={t('interface.groupNameLabel')}
fullWidth
variant="outlined"
value={newGroupName}
@@ -943,10 +947,10 @@ export const AIChatInterface: React.FC = () => {
setNewGroupName('');
}}
>
Cancel
{t('interface.cancel')}
</Button>
<Button onClick={handleCreateNewGroup} variant="contained" disabled={!newGroupName.trim()}>
Create
{t('interface.create')}
</Button>
</DialogActions>
</Dialog>

View File

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

View File

@@ -58,5 +58,50 @@
"safety": "Safety",
"nutrition": "Nutrition",
"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",
"duration": "Duration",
"startTime": "Start Time",
"endTime": "End Time",
"startTime": "Start Timer",
"endTime": "Stop Timer",
"reset": "Reset",
"notes": "Notes",
"bottleType": "Bottle Type",
"bottleTypes": {
"formula": "Formula",
"breastmilk": "Breast Milk",
"other": "Other"
},
"foodDescription": "Food Description",
"amountDescription": "Amount Description",
"placeholders": {
"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": {
"ml": "ml",
"oz": "oz",
"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": {
@@ -148,7 +172,35 @@
},
"temperature": "Temperature",
"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",
"severity": "Severity",
"severities": {
@@ -164,6 +216,12 @@
"notes": "Add any notes..."
},
"units": {
"ml": "ml",
"mg": "mg",
"tsp": "tsp",
"tbsp": "tbsp",
"drops": "drops",
"tablet": "tablet(s)",
"celsius": "°C",
"fahrenheit": "°F"
}
@@ -202,7 +260,10 @@
"noChildrenAdded": "No Children Added",
"noChildrenMessage": "You need to add a child before you can track activities",
"addChild": "Add Child",
"recentActivities": "Recent Activities"
"recentActivities": "Recent Activities",
"error": {
"loadChildrenFailed": "Failed to load children"
}
},
"quickLog": "Quick Log",
"viewHistory": "View History",