feat: Implement frontend localization with i18n and measurement units
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

Implemented comprehensive frontend localization infrastructure supporting
5 languages (English, Spanish, French, Portuguese, Chinese) with measurement
unit preferences (Metric/Imperial). This lays the foundation for international
user support.

**Core Infrastructure:**
- Installed i18next, react-i18next, i18next-browser-languagedetector
- Created I18nProvider component integrated into app layout
- Configured i18next with language detection and localStorage persistence
- Created 35 translation files (5 languages × 7 namespaces)

**Translation Namespaces:**
- common: App-wide UI elements, navigation, actions
- tracking: Activity tracking (feeding, sleep, diaper, milestones)
- ai: AI assistant chat interface
- auth: Authentication flows (login, signup, password reset)
- settings: Settings and preferences
- onboarding: Onboarding flow
- errors: Error messages and validation

**Custom Hooks:**
- useTranslation: Type-safe translation wrapper
- useLocale: Language and measurement system management
- useFormatting: Date, time, number, and unit formatting

**Measurement Unit Support:**
- Created unit conversion utilities (weight, height, temperature, volume)
- Metric: kg, cm, °C, ml
- Imperial: lb, in, °F, oz
- Bidirectional conversion functions

**UI Components:**
- LanguageSelector: Dropdown to change app language
- MeasurementUnitSelector: Toggle between Metric/Imperial
- Integrated both into Settings page Preferences section

**Next Steps (Remaining):**
- Add measurement preferences to backend user schema
- Create onboarding flow with language/measurement selection
- Apply translations to existing components (dashboard, tracking forms)
- Implement multi-language AI responses
- Add professional translations (currently using basic translations)

**File Highlights:**
- lib/i18n/config.ts: i18next configuration
- hooks/useFormatting.ts: Formatting utilities with locale support
- lib/utils/unitConversion.ts: Unit conversion logic
- components/settings/*Selector.tsx: Language and measurement selectors
- locales/*/: Translation files for 5 languages

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 10:52:38 +00:00
parent cd1ed96714
commit c1e37d30b0
49 changed files with 5167 additions and 20 deletions

View File

@@ -0,0 +1,25 @@
'use client';
import { ReactNode, useEffect } from 'react';
import { I18nextProvider } from 'react-i18next';
import i18n from '@/lib/i18n/config';
interface I18nProviderProps {
children: ReactNode;
}
/**
* Provider component for i18next internationalization
* Initializes i18n and provides translation context to all children
*/
export function I18nProvider({ children }: I18nProviderProps) {
useEffect(() => {
// Initialize i18n language from localStorage or browser
const savedLanguage = localStorage.getItem('preferred-language');
if (savedLanguage && i18n.language !== savedLanguage) {
i18n.changeLanguage(savedLanguage);
}
}, []);
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}

View File

@@ -0,0 +1,43 @@
'use client';
import { FormControl, InputLabel, Select, MenuItem, SelectChangeEvent } from '@mui/material';
import { useLocale } from '@/hooks/useLocale';
import { useTranslation } from '@/hooks/useTranslation';
interface LanguageSelectorProps {
variant?: 'standard' | 'outlined' | 'filled';
fullWidth?: boolean;
}
/**
* Language selector component for choosing the app's display language
*/
export function LanguageSelector({ variant = 'outlined', fullWidth = true }: LanguageSelectorProps) {
const { language, setLanguage, supportedLanguages } = useLocale();
const { t } = useTranslation('settings');
const handleLanguageChange = async (event: SelectChangeEvent) => {
await setLanguage(event.target.value);
};
return (
<FormControl variant={variant} fullWidth={fullWidth}>
<InputLabel id="language-selector-label">
{t('preferences.language')}
</InputLabel>
<Select
labelId="language-selector-label"
id="language-selector"
value={language}
label={t('preferences.language')}
onChange={handleLanguageChange}
>
{supportedLanguages.map((lang) => (
<MenuItem key={lang.code} value={lang.code}>
{lang.nativeName} ({lang.name})
</MenuItem>
))}
</Select>
</FormControl>
);
}

View File

@@ -0,0 +1,79 @@
'use client';
import { useState, useEffect } from 'react';
import {
FormControl,
InputLabel,
Select,
MenuItem,
SelectChangeEvent,
Box,
Typography,
} from '@mui/material';
import { useLocale, MeasurementSystem } from '@/hooks/useLocale';
import { useTranslation } from '@/hooks/useTranslation';
interface MeasurementUnitSelectorProps {
variant?: 'standard' | 'outlined' | 'filled';
fullWidth?: boolean;
showDescription?: boolean;
}
/**
* Measurement unit selector component for choosing between Metric and Imperial systems
*/
export function MeasurementUnitSelector({
variant = 'outlined',
fullWidth = true,
showDescription = true,
}: MeasurementUnitSelectorProps) {
const { measurementSystem, setMeasurementSystem } = useLocale();
const { t } = useTranslation('settings');
const [selectedSystem, setSelectedSystem] = useState<MeasurementSystem>(measurementSystem);
useEffect(() => {
setSelectedSystem(measurementSystem);
}, [measurementSystem]);
const handleSystemChange = (event: SelectChangeEvent) => {
const newSystem = event.target.value as MeasurementSystem;
setSelectedSystem(newSystem);
setMeasurementSystem(newSystem);
};
return (
<FormControl variant={variant} fullWidth={fullWidth}>
<InputLabel id="measurement-unit-selector-label">
{t('preferences.measurementUnits')}
</InputLabel>
<Select
labelId="measurement-unit-selector-label"
id="measurement-unit-selector"
value={selectedSystem}
label={t('preferences.measurementUnits')}
onChange={handleSystemChange}
>
<MenuItem value="metric">
<Box>
<Typography variant="body1">{t('preferences.metric')}</Typography>
{showDescription && (
<Typography variant="caption" color="text.secondary">
kg, cm, °C, ml
</Typography>
)}
</Box>
</MenuItem>
<MenuItem value="imperial">
<Box>
<Typography variant="body1">{t('preferences.imperial')}</Typography>
{showDescription && (
<Typography variant="caption" color="text.secondary">
lb, in, °F, oz
</Typography>
)}
</Box>
</MenuItem>
</Select>
</FormControl>
);
}