feat: Complete high-priority i18n localization with date/time support

This commit implements comprehensive localization for high-priority components:

## Tracking Pages (4 files)
- Localized feeding, sleep, diaper, and medicine tracking pages
- Replaced hardcoded strings with translation keys from tracking namespace
- Added useTranslation hook integration
- All form labels, buttons, and messages now support multiple languages

## Child Dialog Components (2 files)
- Localized ChildDialog (add/edit child form)
- Localized DeleteConfirmDialog
- Added new translation keys to children.json for dialog content
- Includes validation messages and action buttons

## Date/Time Localization (14 files + new hook)
- Created useLocalizedDate hook wrapping date-fns with locale support
- Supports 5 languages: English, Spanish, French, Portuguese, Chinese
- Updated all date formatting across:
  * Tracking pages (feeding, sleep, diaper, medicine)
  * Activity pages (activities, history, track activity)
  * Settings components (sessions, biometric, device trust)
  * Analytics components (insights, growth, sleep chart, feeding graph)
- Date displays automatically adapt to user's language (e.g., "2 hours ago" → "hace 2 horas")

## Translation Updates
- Enhanced children.json with dialog section containing:
  * Form field labels (name, birthDate, gender, photoUrl)
  * Action buttons (add, update, delete, cancel, saving, deleting)
  * Delete confirmation messages
  * Validation error messages

Files changed: 17 files (+164, -113)
Languages supported: en, es, fr, pt-BR, zh-CN

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 11:49:48 +00:00
parent 8b808e82ad
commit b56f9546c2
18 changed files with 256 additions and 113 deletions

View File

@@ -17,7 +17,8 @@ import {
Pie,
Cell,
} from 'recharts';
import { format, subDays } from 'date-fns';
import { subDays } from 'date-fns';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
import apiClient from '@/lib/api/client';
interface FeedingData {
@@ -42,6 +43,7 @@ const COLORS = {
};
export default function FeedingFrequencyGraph() {
const { format } = useLocalizedDate();
const [data, setData] = useState<FeedingData[]>([]);
const [typeData, setTypeData] = useState<FeedingTypeData[]>([]);
const [isLoading, setIsLoading] = useState(true);

View File

@@ -24,7 +24,7 @@ import {
ResponsiveContainer,
ReferenceLine,
} from 'recharts';
import { format } from 'date-fns';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
import apiClient from '@/lib/api/client';
interface GrowthData {
@@ -55,6 +55,7 @@ const WHO_WEIGHT_PERCENTILES = {
};
export default function GrowthCurve() {
const { format } = useLocalizedDate();
const [data, setData] = useState<GrowthData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -14,7 +14,8 @@ import {
Legend,
ResponsiveContainer,
} from 'recharts';
import { format, subDays } from 'date-fns';
import { subDays } from 'date-fns';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
import apiClient from '@/lib/api/client';
interface SleepData {
@@ -26,6 +27,7 @@ interface SleepData {
}
export default function WeeklySleepChart() {
const { format } = useLocalizedDate();
const [data, setData] = useState<SleepData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -13,6 +13,7 @@ import {
Alert,
} from '@mui/material';
import { Child, CreateChildData } from '@/lib/api/children';
import { useTranslation } from '@/hooks/useTranslation';
interface ChildDialogProps {
open: boolean;
@@ -23,6 +24,7 @@ interface ChildDialogProps {
}
export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false }: ChildDialogProps) {
const { t } = useTranslation('children');
const [formData, setFormData] = useState<CreateChildData>({
name: '',
birthDate: '',
@@ -61,11 +63,11 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
// Validation
if (!formData.name.trim()) {
setError('Please enter a name');
setError(t('dialog.validation.nameRequired'));
return;
}
if (!formData.birthDate) {
setError('Please select a birth date');
setError(t('dialog.validation.birthDateRequired'));
return;
}
@@ -74,7 +76,7 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
const today = new Date();
today.setHours(0, 0, 0, 0);
if (selectedDate > today) {
setError('Birth date cannot be in the future');
setError(t('dialog.validation.birthDateFuture'));
return;
}
@@ -82,7 +84,7 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
await onSubmit(formData);
onClose();
} catch (err: any) {
setError(err.message || 'Failed to save child');
setError(err.message || t('errors.saveFailed'));
}
};
@@ -95,7 +97,7 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
aria-labelledby="child-dialog-title"
aria-describedby="child-dialog-description"
>
<DialogTitle id="child-dialog-title">{child ? 'Edit Child' : 'Add Child'}</DialogTitle>
<DialogTitle id="child-dialog-title">{child ? t('editChild') : t('addChild')}</DialogTitle>
<DialogContent>
<Box
id="child-dialog-description"
@@ -108,7 +110,7 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
)}
<TextField
label="Name"
label={t('dialog.name')}
value={formData.name}
onChange={handleChange('name')}
fullWidth
@@ -118,7 +120,7 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
/>
<TextField
label="Birth Date"
label={t('dialog.birthDate')}
type="date"
value={formData.birthDate}
onChange={handleChange('birthDate')}
@@ -131,7 +133,7 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
/>
<TextField
label="Gender"
label={t('dialog.gender')}
value={formData.gender}
onChange={handleChange('gender')}
fullWidth
@@ -139,27 +141,27 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
select
disabled={isLoading}
>
<MenuItem value="male">Male</MenuItem>
<MenuItem value="female">Female</MenuItem>
<MenuItem value="other">Other</MenuItem>
<MenuItem value="male">{t('gender.male')}</MenuItem>
<MenuItem value="female">{t('gender.female')}</MenuItem>
<MenuItem value="other">{t('gender.other')}</MenuItem>
</TextField>
<TextField
label="Photo URL (Optional)"
label={t('dialog.photoUrl')}
value={formData.photoUrl}
onChange={handleChange('photoUrl')}
fullWidth
placeholder="https://example.com/photo.jpg"
placeholder={t('dialog.photoPlaceholder')}
disabled={isLoading}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={isLoading}>
Cancel
{t('dialog.cancel')}
</Button>
<Button onClick={handleSubmit} variant="contained" disabled={isLoading}>
{isLoading ? 'Saving...' : child ? 'Update' : 'Add'}
{isLoading ? t('dialog.saving') : child ? t('dialog.update') : t('dialog.add')}
</Button>
</DialogActions>
</Dialog>

View File

@@ -9,6 +9,7 @@ import {
Typography,
} from '@mui/material';
import { Warning } from '@mui/icons-material';
import { useTranslation } from '@/hooks/useTranslation';
interface DeleteConfirmDialogProps {
open: boolean;
@@ -25,6 +26,8 @@ export function DeleteConfirmDialog({
childName,
isLoading = false,
}: DeleteConfirmDialogProps) {
const { t } = useTranslation('children');
return (
<Dialog
open={open}
@@ -37,22 +40,22 @@ export function DeleteConfirmDialog({
>
<DialogTitle id="delete-dialog-title" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Warning color="warning" aria-hidden="true" />
Confirm Delete
{t('dialog.confirmDelete')}
</DialogTitle>
<DialogContent>
<Typography variant="body1" id="delete-dialog-description">
Are you sure you want to delete <strong>{childName}</strong>?
{t('dialog.confirmDeleteMessage')} <strong>{childName}</strong>?
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
This action cannot be undone. All associated data will be permanently removed.
{t('dialog.confirmDeleteWarning')}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={isLoading}>
Cancel
{t('dialog.cancel')}
</Button>
<Button onClick={onConfirm} color="error" variant="contained" disabled={isLoading}>
{isLoading ? 'Deleting...' : 'Delete'}
{isLoading ? t('dialog.deleting') : t('dialog.delete')}
</Button>
</DialogActions>
</Dialog>

View File

@@ -39,7 +39,8 @@ import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { trackingApi, Activity, ActivityType } from '@/lib/api/tracking';
import { childrenApi, Child } from '@/lib/api/children';
import { format, subDays, startOfDay, endOfDay, parseISO, differenceInMinutes, formatDistanceToNow } from 'date-fns';
import { subDays, startOfDay, endOfDay, parseISO, differenceInMinutes } from 'date-fns';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
type DateRange = '7days' | '30days' | '3months';
@@ -97,6 +98,7 @@ const getActivityColor = (type: ActivityType) => {
export const InsightsDashboard: React.FC = () => {
const router = useRouter();
const { format, formatDistanceToNow } = useLocalizedDate();
const [children, setChildren] = useState<Child[]>([]);
const [selectedChild, setSelectedChild] = useState<string>('');
const [dateRange, setDateRange] = useState<DateRange>('7days');

View File

@@ -33,9 +33,10 @@ import {
import { biometricApi, type BiometricCredential } from '@/lib/api/biometric';
import { startRegistration } from '@simplewebauthn/browser';
import { motion } from 'framer-motion';
import { formatDistanceToNow } from 'date-fns';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
export function BiometricSettings() {
const { formatDistanceToNow } = useLocalizedDate();
const [credentials, setCredentials] = useState<BiometricCredential[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -35,9 +35,10 @@ import {
} from '@mui/icons-material';
import { devicesApi, type DeviceInfo } from '@/lib/api/devices';
import { motion } from 'framer-motion';
import { formatDistanceToNow } from 'date-fns';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
export function DeviceTrustManagement() {
const { formatDistanceToNow } = useLocalizedDate();
const [devices, setDevices] = useState<DeviceInfo[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -31,9 +31,10 @@ import {
} from '@mui/icons-material';
import { sessionsApi, type SessionInfo } from '@/lib/api/sessions';
import { motion } from 'framer-motion';
import { formatDistanceToNow } from 'date-fns';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
export function SessionsManagement() {
const { formatDistanceToNow } = useLocalizedDate();
const [sessions, setSessions] = useState<SessionInfo[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);