feat: Complete comprehensive localization of all tracking and management pages
- 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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user