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>

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 })}