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>
134 lines
3.8 KiB
TypeScript
134 lines
3.8 KiB
TypeScript
import { useCallback } from 'react';
|
|
import { useLocale } from './useLocale';
|
|
import {
|
|
convertWeight,
|
|
convertHeight,
|
|
convertTemperature,
|
|
convertVolume,
|
|
} from '@/lib/utils/unitConversion';
|
|
|
|
/**
|
|
* Custom hook for formatting dates, times, numbers, and measurements
|
|
* based on the user's locale and measurement preferences
|
|
*/
|
|
export function useFormatting() {
|
|
const { language, measurementSystem } = useLocale();
|
|
|
|
const formatDate = useCallback(
|
|
(date: Date | string, options?: Intl.DateTimeFormatOptions) => {
|
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
return new Intl.DateTimeFormat(language, options || {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
}).format(dateObj);
|
|
},
|
|
[language]
|
|
);
|
|
|
|
const formatTime = useCallback(
|
|
(date: Date | string, options?: Intl.DateTimeFormatOptions) => {
|
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
return new Intl.DateTimeFormat(language, options || {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(dateObj);
|
|
},
|
|
[language]
|
|
);
|
|
|
|
const formatDateTime = useCallback(
|
|
(date: Date | string, options?: Intl.DateTimeFormatOptions) => {
|
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
return new Intl.DateTimeFormat(language, options || {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(dateObj);
|
|
},
|
|
[language]
|
|
);
|
|
|
|
const formatNumber = useCallback(
|
|
(value: number, options?: Intl.NumberFormatOptions) => {
|
|
return new Intl.NumberFormat(language, options).format(value);
|
|
},
|
|
[language]
|
|
);
|
|
|
|
const formatWeight = useCallback(
|
|
(valueInKg: number, showUnit: boolean = true) => {
|
|
const converted = convertWeight(valueInKg, measurementSystem);
|
|
const formatted = formatNumber(converted.value, {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 2,
|
|
});
|
|
return showUnit ? `${formatted} ${converted.unit}` : formatted;
|
|
},
|
|
[measurementSystem, formatNumber]
|
|
);
|
|
|
|
const formatHeight = useCallback(
|
|
(valueInCm: number, showUnit: boolean = true) => {
|
|
const converted = convertHeight(valueInCm, measurementSystem);
|
|
const formatted = formatNumber(converted.value, {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 1,
|
|
});
|
|
return showUnit ? `${formatted} ${converted.unit}` : formatted;
|
|
},
|
|
[measurementSystem, formatNumber]
|
|
);
|
|
|
|
const formatTemperature = useCallback(
|
|
(valueInCelsius: number, showUnit: boolean = true) => {
|
|
const converted = convertTemperature(valueInCelsius, measurementSystem);
|
|
const formatted = formatNumber(converted.value, {
|
|
minimumFractionDigits: 1,
|
|
maximumFractionDigits: 1,
|
|
});
|
|
return showUnit ? `${formatted}${converted.unit}` : formatted;
|
|
},
|
|
[measurementSystem, formatNumber]
|
|
);
|
|
|
|
const formatVolume = useCallback(
|
|
(valueInMl: number, showUnit: boolean = true) => {
|
|
const converted = convertVolume(valueInMl, measurementSystem);
|
|
const formatted = formatNumber(converted.value, {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 1,
|
|
});
|
|
return showUnit ? `${formatted} ${converted.unit}` : formatted;
|
|
},
|
|
[measurementSystem, formatNumber]
|
|
);
|
|
|
|
const formatDuration = useCallback(
|
|
(minutes: number) => {
|
|
if (minutes < 60) {
|
|
return `${minutes} min`;
|
|
}
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`;
|
|
},
|
|
[]
|
|
);
|
|
|
|
return {
|
|
formatDate,
|
|
formatTime,
|
|
formatDateTime,
|
|
formatNumber,
|
|
formatWeight,
|
|
formatHeight,
|
|
formatTemperature,
|
|
formatVolume,
|
|
formatDuration,
|
|
measurementSystem,
|
|
};
|
|
}
|