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

@@ -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>