diff --git a/maternal-web/app/children/page.tsx b/maternal-web/app/children/page.tsx index 1c4ee7d..e55a649 100644 --- a/maternal-web/app/children/page.tsx +++ b/maternal-web/app/children/page.tsx @@ -239,7 +239,7 @@ export default function ChildrenPage() { {child.name} diff --git a/maternal-web/app/family/page.tsx b/maternal-web/app/family/page.tsx index 7dbd477..fbe54cd 100644 --- a/maternal-web/app/family/page.tsx +++ b/maternal-web/app/family/page.tsx @@ -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(null); const [members, setMembers] = useState([]); @@ -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() { - Family + {t('pageTitle')} - Manage your family members and share access + {t('pageSubtitle')} @@ -177,7 +179,7 @@ export default function FamilyPage() { onClick={() => setJoinDialogOpen(true)} disabled={loading} > - Join Family + {t('buttons.joinFamily')} @@ -208,10 +210,10 @@ export default function FamilyPage() { - Family Share Code + {t('shareCode.title')} - Share this code with family members to give them access to your family's data + {t('shareCode.description')} } onClick={handleCopyCode} > - Copy Code + {t('buttons.copyCode')} @@ -242,30 +244,30 @@ export default function FamilyPage() { - Family Members ({members.length}) + {t('members.title', { count: members.length })} {members.length === 0 ? ( - No family members yet + {t('members.noMembers')} - Invite family members to collaborate on child care + {t('members.noMembersDescription')} ) : ( {members.map((member, index) => { - const memberName = member.user?.name || 'Unknown User'; + const memberName = member.user?.name || t('placeholders.unknownUser'); return ( {isCurrentUser(member.userId) && ( - + )} - {member.user?.email || 'No email'} + {member.user?.email || t('placeholders.noEmail')} @@ -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 })} > diff --git a/maternal-web/app/track/feeding/page.tsx b/maternal-web/app/track/feeding/page.tsx index 88bf450..473d450 100644 --- a/maternal-web/app/track/feeding/page.tsx +++ b/maternal-web/app/track/feeding/page.tsx @@ -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() { - No Children Added + {t('common.noChildrenAdded')} - You need to add a child before you can track feeding activities + {t('common.noChildrenMessage')} @@ -414,11 +414,11 @@ function FeedingTrackPage() { {children.length > 1 && ( - Select Child + {t('common.selectChild')} setBottleType(e.target.value as 'formula' | 'breastmilk' | 'other')} - label={t('feeding.type')} + label={t('feeding.bottleType')} > - {t('feeding.types.bottle')} - {t('feeding.types.breast')} - Other + {t('feeding.bottleTypes.formula')} + {t('feeding.bottleTypes.breastmilk')} + {t('feeding.bottleTypes.other')} @@ -543,20 +543,20 @@ function FeedingTrackPage() { setFoodDescription(e.target.value)} sx={{ mb: 3 }} - placeholder={t('feeding.placeholders.notes')} + placeholder={t('feeding.placeholders.foodDescription')} /> setAmountDescription(e.target.value)} sx={{ mb: 3 }} - placeholder={t('feeding.placeholders.amount')} + placeholder={t('feeding.placeholders.amountDescription')} /> )} @@ -583,7 +583,7 @@ function FeedingTrackPage() { onClick={handleSubmit} disabled={loading} > - {loading ? t('feeding.addFeeding') : t('feeding.addFeeding')} + {loading ? t('common.loading') : t('feeding.addFeeding')} @@ -591,7 +591,7 @@ function FeedingTrackPage() { - {t('feeding.title')} + {t('feeding.recentFeedings')} @@ -685,10 +685,10 @@ function FeedingTrackPage() { diff --git a/maternal-web/app/track/medicine/page.tsx b/maternal-web/app/track/medicine/page.tsx index 4bd4ef6..376576d 100644 --- a/maternal-web/app/track/medicine/page.tsx +++ b/maternal-web/app/track/medicine/page.tsx @@ -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() { - No Children Added + {t('common.noChildrenAdded')} - You need to add a child before you can track medicine activities + {t('common.noChildrenMessage')} @@ -352,11 +352,11 @@ function MedicineTrackPage() { {children.length > 1 && ( - Select Child + {t('common.selectChild')} { @@ -422,39 +422,39 @@ function MedicineTrackPage() { setDosage(0); } }} - label="Unit" + label={t('health.unit')} > - ml - mg - tsp - tbsp - drops - tablet(s) + {t('health.units.ml')} + {t('health.units.mg')} + {t('health.units.tsp')} + {t('health.units.tbsp')} + {t('health.units.drops')} + {t('health.units.tablet')} - Route + {t('health.route.label')} setReason(e.target.value)} sx={{ mb: 3 }} - placeholder="e.g., Fever, Pain, Allergy" + placeholder={t('health.reason.placeholder')} /> - {loading ? t('activities.medicine') : t('activities.medicine')} + {loading ? t('common.loading') : t('health.logMedicine')} @@ -485,7 +485,7 @@ function MedicineTrackPage() { - {t('activities.medicine')} + {t('health.recentMedicines')} @@ -578,10 +578,10 @@ function MedicineTrackPage() { diff --git a/maternal-web/components/features/ai-chat/AIChatInterface.tsx b/maternal-web/components/features/ai-chat/AIChatInterface.tsx index 5fd43a9..81d6c8b 100644 --- a/maternal-web/components/features/ai-chat/AIChatInterface.tsx +++ b/maternal-web/components/features/ai-chat/AIChatInterface.tsx @@ -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([]); 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 = () => { - Chat History + {t('history.title')} {isMobile && ( - setDrawerOpen(false)} size="small" aria-label="Close drawer"> + setDrawerOpen(false)} size="small" aria-label={t('interface.closeDrawer')}> )} @@ -405,7 +410,7 @@ export const AIChatInterface: React.FC = () => { onClick={handleNewConversation} sx={{ borderRadius: 2 }} > - New Chat + {t('chat.newChat')} @@ -413,7 +418,7 @@ export const AIChatInterface: React.FC = () => { - No conversations yet + {t('history.noHistory')} ) : ( @@ -434,7 +439,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 ? : } @@ -566,10 +571,10 @@ export const AIChatInterface: React.FC = () => { - AI Parenting Assistant + {t('interface.assistantTitle')} - Ask me anything about parenting and childcare + {t('interface.assistantSubtitle')} @@ -599,7 +604,7 @@ export const AIChatInterface: React.FC = () => { > - Hi {user?.name}! How can I help you today? + {t('interface.greeting', { name: user?.name })} { transition: 'opacity 0.3s ease-in-out', }} > - {currentThinkingMessages[currentThinkingIndex] || 'Thinking...'} + {currentThinkingMessages[currentThinkingIndex] || t('chat.thinking')} @@ -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 = () => { - This AI assistant provides general information. Always consult healthcare professionals - for medical advice. + {t('interface.disclaimerFooter')} {/* Delete Confirmation Dialog */} setDeleteDialogOpen(false)}> - Delete Conversation + {t('interface.deleteDialogTitle')} - Are you sure you want to delete this conversation? This action cannot be undone. + {t('interface.deleteDialogMessage')} - + @@ -837,7 +841,7 @@ export const AIChatInterface: React.FC = () => { - Move to Group + {t('interface.moveToGroup')} { @@ -851,7 +855,7 @@ export const AIChatInterface: React.FC = () => { - Delete + {t('interface.delete')} @@ -862,7 +866,7 @@ export const AIChatInterface: React.FC = () => { maxWidth="xs" fullWidth > - Move to Group + {t('interface.moveToGroup')} { - + {getExistingGroups().map((groupName) => ( @@ -898,13 +902,13 @@ export const AIChatInterface: React.FC = () => { - + @@ -919,12 +923,12 @@ export const AIChatInterface: React.FC = () => { maxWidth="xs" fullWidth > - Create New Group + {t('interface.createNewGroup')} { setNewGroupName(''); }} > - Cancel + {t('interface.cancel')} diff --git a/maternal-web/components/features/analytics/InsightsDashboard.tsx b/maternal-web/components/features/analytics/InsightsDashboard.tsx index c3ee7ca..af32bb7 100644 --- a/maternal-web/components/features/analytics/InsightsDashboard.tsx +++ b/maternal-web/components/features/analytics/InsightsDashboard.tsx @@ -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([]); const [selectedChild, setSelectedChild] = useState(''); const [dateRange, setDateRange] = useState('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 = () => { > - Insights & Analytics + {t('title')} - Track patterns and get insights about your child's activities + {t('subtitle')} {/* Filters */} @@ -281,11 +283,11 @@ export const InsightsDashboard: React.FC = () => { {children.length > 1 && ( - Child + {t('filters.child')}