diff --git a/app/(tabs)/challenges.tsx b/app/(tabs)/challenges.tsx index 5f6f7d4..a910746 100644 --- a/app/(tabs)/challenges.tsx +++ b/app/(tabs)/challenges.tsx @@ -602,14 +602,14 @@ const styles = StyleSheet.create({ marginBottom: 26, }, title: { - fontSize: 32, + fontSize: 28, fontWeight: '700', letterSpacing: 1, fontFamily: 'AliBold' }, subtitle: { marginTop: 6, - fontSize: 14, + fontSize: 12, fontWeight: '500', opacity: 0.8, fontFamily: 'AliRegular' @@ -619,8 +619,8 @@ const styles = StyleSheet.create({ alignItems: 'center', }, joinButtonGlass: { - paddingHorizontal: 16, - paddingVertical: 10, + paddingHorizontal: 14, + paddingVertical: 8, borderRadius: 16, minWidth: 70, alignItems: 'center', @@ -629,7 +629,7 @@ const styles = StyleSheet.create({ borderColor: 'rgba(255,255,255,0.45)', }, joinButtonLabel: { - fontSize: 14, + fontSize: 12, fontWeight: '700', color: '#0f1528', letterSpacing: 0.5, @@ -639,8 +639,8 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(255,255,255,0.7)', }, createButton: { - width: 40, - height: 40, + width: 36, + height: 36, borderRadius: 20, alignItems: 'center', justifyContent: 'center', diff --git a/app/auth/login.tsx b/app/auth/login.tsx index 5ca84f6..b73778c 100644 --- a/app/auth/login.tsx +++ b/app/auth/login.tsx @@ -13,6 +13,7 @@ import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; import { Colors } from '@/constants/Colors'; import { useAppDispatch } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { useI18n } from '@/hooks/useI18n'; import { fetchMyProfile, login } from '@/store/userSlice'; import Toast from 'react-native-toast-message'; @@ -23,6 +24,7 @@ export default function LoginScreen() { const color = Colors[scheme]; const pageBackground = scheme === 'light' ? color.pageBackgroundEmphasis : color.background; const dispatch = useAppDispatch(); + const { t } = useI18n(); const AnimatedLinear = useMemo(() => Animated.createAnimatedComponent(LinearGradient), []); // 背景动效:轻微平移/旋转与呼吸动画 @@ -79,12 +81,12 @@ export default function LoginScreen() { const guardAgreement = useCallback((action: () => void) => { if (!hasAgreed) { Alert.alert( - '请先阅读并同意', - '继续登录前,请阅读并勾选《隐私政策》和《用户协议》。点击“同意并继续”将默认勾选并继续登录。', + t('login.agreement.alert.title'), + t('login.agreement.alert.message'), [ - { text: '取消', style: 'cancel' }, + { text: t('login.agreement.alert.cancel'), style: 'cancel' }, { - text: '同意并继续', + text: t('login.agreement.alert.confirm'), onPress: () => { setHasAgreed(true); setTimeout(() => action(), 0); @@ -96,7 +98,7 @@ export default function LoginScreen() { return; } action(); - }, [hasAgreed]); + }, [hasAgreed, t]); const onAppleLogin = useCallback(async () => { if (!appleAvailable) return; @@ -110,7 +112,7 @@ export default function LoginScreen() { }); const identityToken = (credential as any)?.identityToken; if (!identityToken || typeof identityToken !== 'string') { - throw new Error('未获取到 Apple 身份令牌'); + throw new Error(t('login.errors.appleIdentityTokenMissing')); } await dispatch(login({ appleIdentityToken: identityToken })).unwrap(); @@ -118,7 +120,7 @@ export default function LoginScreen() { await dispatch(fetchMyProfile()) Toast.show({ - text1: '登录成功', + text1: t('login.success.loginSuccess'), type: 'success', }); // 登录成功后处理重定向 @@ -145,12 +147,12 @@ export default function LoginScreen() { console.log('err.code', err.code); if (err?.code === 'ERR_CANCELED' || err?.code === 'ERR_REQUEST_CANCELED') return; - const message = err?.message || '登录失败,请稍后再试'; - Alert.alert('登录失败', message); + const message = err?.message || t('login.errors.loginFailed'); + Alert.alert(t('login.errors.loginFailedTitle'), message); } finally { setLoading(false); } - }, [appleAvailable, router, searchParams?.redirectParams, searchParams?.redirectTo]); + }, [appleAvailable, router, searchParams?.redirectParams, searchParams?.redirectTo, dispatch, t]); // 登录按钮不再因未勾选协议而禁用,仅在加载中禁用 @@ -244,14 +246,14 @@ export default function LoginScreen() { )} - 登录 + {t('login.title')} Out Live - 健康生活,自律让我更自由 + {t('login.subtitle')} {/* Apple 登录 */} @@ -276,12 +278,12 @@ export default function LoginScreen() { color="#FFFFFF" style={{ marginRight: 10 }} /> - 登录中... + {t('login.loggingIn')} ) : ( <> - 使用 Apple 登录 + {t('login.appleLogin')} )} @@ -294,12 +296,12 @@ export default function LoginScreen() { color="#FFFFFF" style={{ marginRight: 10 }} /> - 登录中... + {t('login.loggingIn')} ) : ( <> - 使用 Apple 登录 + {t('login.appleLogin')} )} @@ -319,13 +321,13 @@ export default function LoginScreen() { {hasAgreed && } - 我已阅读并同意 + {t('login.agreement.readAndAgree')} Linking.openURL(PRIVACY_POLICY_URL)}> - 《隐私政策》 + {t('login.agreement.privacyPolicy')} - + {t('login.agreement.and')} Linking.openURL(USER_AGREEMENT_URL)}> - 《用户协议》 + {t('login.agreement.userAgreement')} diff --git a/app/basal-metabolism-detail.tsx b/app/basal-metabolism-detail.tsx index ef72c6a..0c9abab 100644 --- a/app/basal-metabolism-detail.tsx +++ b/app/basal-metabolism-detail.tsx @@ -2,9 +2,10 @@ import { DateSelector } from '@/components/DateSelector'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAppSelector } from '@/hooks/redux'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { selectUserAge, selectUserProfile } from '@/store/userSlice'; -import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; +import { getLocalizedDateFormat, getMonthDays, getTodayIndexInMonth } from '@/utils/date'; import { fetchBasalEnergyBurned } from '@/utils/health'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; @@ -24,6 +25,7 @@ type BasalMetabolismData = { }; export default function BasalMetabolismDetailScreen() { + const { t, i18n } = useI18n(); const userProfile = useAppSelector(selectUserProfile); const userAge = useAppSelector(selectUserAge); const safeAreaTop = useSafeAreaTop() @@ -140,9 +142,9 @@ export default function BasalMetabolismDetailScreen() { // 获取当前选中日期 const currentSelectedDate = useMemo(() => { - const days = getMonthDaysZh(); + const days = getMonthDays(undefined, i18n.language as 'zh' | 'en'); return days[selectedIndex]?.date?.toDate() ?? new Date(); - }, [selectedIndex]); + }, [selectedIndex, i18n.language]); // 计算BMR范围 @@ -203,7 +205,7 @@ export default function BasalMetabolismDetailScreen() { setSelectedIndex(index); // 获取选中日期 - const days = getMonthDaysZh(); + const days = getMonthDays(undefined, i18n.language as 'zh' | 'en'); const selectedDate = days[index]?.date?.toDate(); if (selectedDate) { @@ -247,7 +249,7 @@ export default function BasalMetabolismDetailScreen() { } } catch (err) { if (!isCancelled) { - setError(err instanceof Error ? err.message : '获取数据失败'); + setError(err instanceof Error ? err.message : t('basalMetabolismDetail.chart.error.fetchFailed')); } } finally { if (!isCancelled) { @@ -280,7 +282,8 @@ export default function BasalMetabolismDetailScreen() { // 显示周数 const weekOfYear = dayjs(item.date).week(); const firstWeekOfYear = dayjs(item.date).startOf('year').week(); - return `第${weekOfYear - firstWeekOfYear + 1}周`; + const weekNumber = weekOfYear - firstWeekOfYear + 1; + return t('basalMetabolismDetail.chart.weekLabel', { week: weekNumber }); default: return dayjs(item.date).format('MM-DD'); } @@ -319,7 +322,7 @@ export default function BasalMetabolismDetailScreen() { {/* 头部导航 */} - {dayjs(currentSelectedDate).format('M月D日')} 基础代谢 + {t('basalMetabolismDetail.currentData.title', { + date: getLocalizedDateFormat(dayjs(currentSelectedDate), i18n.language as 'zh' | 'en') + })} @@ -366,21 +371,24 @@ export default function BasalMetabolismDetailScreen() { if (selectedDateData?.value) { return Math.round(selectedDateData.value).toString(); } - return '--'; + return t('basalMetabolismDetail.currentData.noData'); })()} - 千卡 + {t('basalMetabolismDetail.currentData.unit')} {bmrRange && ( - 正常范围: {bmrRange.min}-{bmrRange.max} 千卡 + {t('basalMetabolismDetail.currentData.normalRange', { + min: bmrRange.min, + max: bmrRange.max + })} )} {/* 基础代谢统计 */} - 基础代谢统计 + {t('basalMetabolismDetail.stats.title')} {/* Tab 切换 */} @@ -390,7 +398,7 @@ export default function BasalMetabolismDetailScreen() { activeOpacity={0.7} > - 按周 + {t('basalMetabolismDetail.stats.tabs.week')} - 按月 + {t('basalMetabolismDetail.stats.tabs.month')} @@ -408,28 +416,30 @@ export default function BasalMetabolismDetailScreen() { {isLoading ? ( - 加载中... + {t('basalMetabolismDetail.chart.loadingText')} ) : error ? ( - 加载失败: {error} + + {t('basalMetabolismDetail.chart.error.text', { error })} + { - // 重新加载数据 + // {t('basalMetabolismDetail.comments.reloadData')} setIsLoading(true); setError(null); fetchBasalMetabolismData(activeTab).then(data => { setChartData(data); setIsLoading(false); }).catch(err => { - setError(err instanceof Error ? err.message : '获取数据失败'); + setError(err instanceof Error ? err.message : t('basalMetabolismDetail.chart.error.fetchFailed')); setIsLoading(false); }); }} activeOpacity={0.7} > - 重试 + {t('basalMetabolismDetail.chart.error.retry')} ) : processedChartData.datasets.length > 0 && processedChartData.datasets[0].data.length > 0 ? ( @@ -441,7 +451,7 @@ export default function BasalMetabolismDetailScreen() { width={Dimensions.get('window').width - 80} height={220} yAxisLabel="" - yAxisSuffix="千卡" + yAxisSuffix={t('basalMetabolismDetail.chart.yAxisSuffix')} chartConfig={{ backgroundColor: '#ffffff', backgroundGradientFrom: '#ffffff', @@ -470,7 +480,7 @@ export default function BasalMetabolismDetailScreen() { /> ) : ( - 暂无数据 + {t('basalMetabolismDetail.chart.empty')} )} @@ -490,56 +500,66 @@ export default function BasalMetabolismDetailScreen() { style={styles.closeButton} onPress={() => setInfoModalVisible(false)} > - × + {t('basalMetabolismDetail.modal.closeButton')} {/* 标题 */} - 基础代谢 + {t('basalMetabolismDetail.modal.title')} {/* 基础代谢定义 */} - 基础代谢,也称基础代谢率(BMR),是指人体在完全静息状态下维持基本生命功能(心跳、呼吸、体温调节等)所需的最低能量消耗,通常以卡路里为单位。 + {t('basalMetabolismDetail.modal.description')} {/* 为什么重要 */} - 为什么重要? + {t('basalMetabolismDetail.modal.sections.importance.title')} - 基础代谢占总能量消耗的60-75%,是能量平衡的基础。了解您的基础代谢有助于制定科学的营养计划、优化体重管理策略,以及评估代谢健康状态。 + {t('basalMetabolismDetail.modal.sections.importance.content')} {/* 正常范围 */} - 正常范围 + {t('basalMetabolismDetail.modal.sections.normalRange.title')} - - 男性:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 + 5 + - {t('basalMetabolismDetail.modal.sections.normalRange.formulas.male')} - - 女性:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 - 161 + - {t('basalMetabolismDetail.modal.sections.normalRange.formulas.female')} {bmrRange ? ( <> - 您的正常区间:{bmrRange.min}-{bmrRange.max}千卡/天 + + {t('basalMetabolismDetail.modal.sections.normalRange.userRange', { + min: bmrRange.min, + max: bmrRange.max + })} + - (在公式基础计算值上下浮动15%都属于正常范围) + {t('basalMetabolismDetail.modal.sections.normalRange.rangeNote')} - 基于您的信息:{userProfile.gender === 'male' ? '男性' : '女性'},{userAge}岁,{userProfile.height}cm,{userProfile.weight}kg + {t('basalMetabolismDetail.modal.sections.normalRange.userInfo', { + gender: t(`basalMetabolismDetail.gender.${userProfile.gender === 'male' ? 'male' : 'female'}`), + age: userAge, + height: userProfile.height, + weight: userProfile.weight + })} ) : ( - 请完善基本信息以计算您的代谢率 + + {t('basalMetabolismDetail.modal.sections.normalRange.incompleteInfo')} + )} {/* 提高代谢率的策略 */} - 提高代谢率的策略 - 科学研究支持以下方法: + {t('basalMetabolismDetail.modal.sections.strategies.title')} + {t('basalMetabolismDetail.modal.sections.strategies.subtitle')} - 1.增加肌肉量 (每周2-3次力量训练) - 2.高强度间歇训练 (HIIT) - 3.充分蛋白质摄入 (体重每公斤1.6-2.2g) - 4.保证充足睡眠 (7-9小时/晚) - 5.避免过度热量限制 (不低于BMR的80%) + {(t('basalMetabolismDetail.modal.sections.strategies.items', { returnObjects: true }) as string[]).map((item: string, index: number) => ( + {item} + ))} diff --git a/app/challenges/[id]/leaderboard.tsx b/app/challenges/[id]/leaderboard.tsx index 020b70b..d52da06 100644 --- a/app/challenges/[id]/leaderboard.tsx +++ b/app/challenges/[id]/leaderboard.tsx @@ -4,6 +4,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { fetchChallengeDetail, @@ -37,6 +38,7 @@ export default function ChallengeLeaderboardScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const insets = useSafeAreaInsets(); + const { t } = useI18n(); const challengeSelector = useMemo(() => (id ? selectChallengeById(id) : undefined), [id]); const challenge = useAppSelector((state) => (challengeSelector ? challengeSelector(state) : undefined)); @@ -75,12 +77,12 @@ export default function ChallengeLeaderboardScreen() { if (!id) { return ( - router.back()} withSafeTop /> + router.back()} withSafeTop /> - 未找到该挑战。 + {t('challengeDetail.leaderboard.notFound')} ); @@ -89,10 +91,10 @@ export default function ChallengeLeaderboardScreen() { if (detailStatus === 'loading' && !challenge) { return ( - router.back()} withSafeTop /> + router.back()} withSafeTop /> - 加载榜单中… + {t('challengeDetail.leaderboard.loading')} ); @@ -131,10 +133,10 @@ export default function ChallengeLeaderboardScreen() { if (!challenge) { return ( - router.back()} withSafeTop /> + router.back()} withSafeTop /> - {detailError ?? '暂时无法加载榜单,请稍后再试。'} + {detailError ?? t('challengeDetail.leaderboard.loadFailed')} @@ -146,7 +148,7 @@ export default function ChallengeLeaderboardScreen() { return ( - router.back()} withSafeTop /> + router.back()} withSafeTop /> - 加载榜单中… + {t('challengeDetail.leaderboard.loading')} ) : rankingData.length ? ( rankingData.map((item, index) => ( @@ -196,18 +198,18 @@ export default function ChallengeLeaderboardScreen() { ) : ( - 榜单即将开启,快来抢占席位。 + {t('challengeDetail.leaderboard.empty')} )} {isLoadingMore ? ( - 加载更多… + {t('challengeDetail.leaderboard.loadMore')} ) : null} {rankingLoadMoreStatus === 'failed' ? ( - 加载更多失败,请下拉刷新重试 + {t('challengeDetail.leaderboard.loadMoreFailed')} ) : null} diff --git a/app/circumference-detail.tsx b/app/circumference-detail.tsx index 6b63f52..4df9f98 100644 --- a/app/circumference-detail.tsx +++ b/app/circumference-detail.tsx @@ -25,6 +25,7 @@ const CIRCUMFERENCE_TYPES = [ { key: 'calfCircumference', label: '小腿围', color: '#DDA0DD' }, ]; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { CircumferencePeriod } from '@/services/circumferenceAnalysis'; @@ -35,6 +36,7 @@ export default function CircumferenceDetailScreen() { const dispatch = useAppDispatch(); const userProfile = useAppSelector(selectUserProfile); const { ensureLoggedIn } = useAuthGuard(); + const { t } = useI18n(); // 日期相关状态 const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); @@ -78,37 +80,37 @@ export default function CircumferenceDetailScreen() { const measurements = [ { key: 'chestCircumference', - label: '胸围', + label: t('circumferenceDetail.measurements.chest'), value: userProfile?.chestCircumference, color: '#FF6B6B', }, { key: 'waistCircumference', - label: '腰围', + label: t('circumferenceDetail.measurements.waist'), value: userProfile?.waistCircumference, color: '#4ECDC4', }, { key: 'upperHipCircumference', - label: '上臀围', + label: t('circumferenceDetail.measurements.upperHip'), value: userProfile?.upperHipCircumference, color: '#45B7D1', }, { key: 'armCircumference', - label: '臂围', + label: t('circumferenceDetail.measurements.arm'), value: userProfile?.armCircumference, color: '#96CEB4', }, { key: 'thighCircumference', - label: '大腿围', + label: t('circumferenceDetail.measurements.thigh'), value: userProfile?.thighCircumference, color: '#FFEAA7', }, { key: 'calfCircumference', - label: '小腿围', + label: t('circumferenceDetail.measurements.calf'), value: userProfile?.calfCircumference, color: '#DDA0DD', }, @@ -243,10 +245,10 @@ export default function CircumferenceDetailScreen() { // 将YYYY-MM-DD格式转换为第几周 const weekOfYear = dayjs(item.label).week(); const firstWeekOfMonth = dayjs(item.label).startOf('month').week(); - return `第${weekOfYear - firstWeekOfMonth + 1}周`; + return t('circumferenceDetail.chart.weekLabel', { week: weekOfYear - firstWeekOfMonth + 1 }); case 'year': // 将YYYY-MM格式转换为月份 - return dayjs(item.label).format('M月'); + return t('circumferenceDetail.chart.monthLabel', { month: dayjs(item.label).format('M') }); default: return item.label; } @@ -287,7 +289,7 @@ export default function CircumferenceDetailScreen() { {/* 头部导航 */} @@ -338,7 +340,7 @@ export default function CircumferenceDetailScreen() { {/* 围度统计 */} - 围度统计 + {t('circumferenceDetail.title')} {/* Tab 切换 */} @@ -348,7 +350,7 @@ export default function CircumferenceDetailScreen() { activeOpacity={0.7} > - 按周 + {t('circumferenceDetail.tabs.week')} - 按月 + {t('circumferenceDetail.tabs.month')} - 按年 + {t('circumferenceDetail.tabs.year')} @@ -390,7 +392,7 @@ export default function CircumferenceDetailScreen() { styles.legendText, !isVisible && styles.legendTextHidden ]}> - {type.label} + {t(`circumferenceDetail.measurements.${type.key.replace('Circumference', '').toLowerCase()}`)} ); @@ -401,17 +403,17 @@ export default function CircumferenceDetailScreen() { {isLoading ? ( - 加载中... + {t('circumferenceDetail.loading')} ) : error ? ( - 加载失败: {error} + {t('circumferenceDetail.error')}: {error} dispatch(fetchCircumferenceAnalysis(activeTab))} activeOpacity={0.7} > - 重试 + {t('circumferenceDetail.retry')} ) : processedChartData.datasets.length > 0 ? ( @@ -453,8 +455,8 @@ export default function CircumferenceDetailScreen() { {processedChartData.datasets.length === 0 && !isLoading && !error - ? '暂无数据' - : '请选择要显示的围度数据' + ? t('circumferenceDetail.chart.empty') + : t('circumferenceDetail.chart.noSelection') } @@ -469,12 +471,12 @@ export default function CircumferenceDetailScreen() { setModalVisible(false); setSelectedMeasurement(null); }} - title={selectedMeasurement ? `设置${selectedMeasurement.label}` : '设置围度'} + title={selectedMeasurement ? t('circumferenceDetail.modal.title', { label: selectedMeasurement.label }) : t('circumferenceDetail.modal.defaultTitle')} items={circumferenceOptions} selectedValue={selectedMeasurement?.currentValue} onValueChange={() => { }} // Real-time update not needed onConfirm={handleUpdateMeasurement} - confirmButtonText="确认" + confirmButtonText={t('circumferenceDetail.modal.confirm')} pickerHeight={180} /> diff --git a/app/fitness-rings-detail.tsx b/app/fitness-rings-detail.tsx index 2b47e4f..6c213ea 100644 --- a/app/fitness-rings-detail.tsx +++ b/app/fitness-rings-detail.tsx @@ -3,6 +3,7 @@ import { ThemedView } from '@/components/ThemedView'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { fetchActivityRingsForDate, @@ -51,6 +52,7 @@ type WeekData = { }; export default function FitnessRingsDetailScreen() { + const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() const colorScheme = useColorScheme(); const [weekData, setWeekData] = useState([]); @@ -82,7 +84,7 @@ export default function FitnessRingsDetailScreen() { exerciseInfoAnim.setValue(0); } } catch (error) { - console.error('加载锻炼分钟说明偏好失败:', error); + console.error(t('fitnessRingsDetail.errors.loadExerciseInfoPreference'), error); } }; @@ -98,7 +100,15 @@ export default function FitnessRingsDetailScreen() { for (let i = 0; i < 7; i++) { const currentDay = startOfWeek.add(i, 'day'); const isToday = currentDay.isSame(today, 'day'); - const dayNames = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; + const dayNames = [ + t('fitnessRingsDetail.weekDays.monday'), + t('fitnessRingsDetail.weekDays.tuesday'), + t('fitnessRingsDetail.weekDays.wednesday'), + t('fitnessRingsDetail.weekDays.thursday'), + t('fitnessRingsDetail.weekDays.friday'), + t('fitnessRingsDetail.weekDays.saturday'), + t('fitnessRingsDetail.weekDays.sunday') + ]; try { const activityRingsData = await fetchActivityRingsForDate(currentDay.toDate()); @@ -303,7 +313,7 @@ export default function FitnessRingsDetailScreen() { setShowExerciseInfo(false); }); } catch (error) { - console.error('保存锻炼分钟说明偏好失败:', error); + console.error(t('fitnessRingsDetail.errors.saveExerciseInfoPreference'), error); } }; @@ -380,7 +390,7 @@ export default function FitnessRingsDetailScreen() { {/* 活动热量卡片 */} - 活动热量 + {t('fitnessRingsDetail.cards.activeCalories.title')} ? @@ -390,25 +400,25 @@ export default function FitnessRingsDetailScreen() { {Math.round(activeEnergyBurned)}/{activeEnergyBurnedGoal} - 千卡 + {t('fitnessRingsDetail.cards.activeCalories.unit')} - {Math.round(activeEnergyBurned)}千卡 + {Math.round(activeEnergyBurned)}{t('fitnessRingsDetail.cards.activeCalories.unit')} {renderBarChart( hourlyCaloriesData.map(h => h.calories), Math.max(activeEnergyBurnedGoal / 24, 1), '#FF3B30', - '千卡' + t('fitnessRingsDetail.cards.activeCalories.unit') )} {/* 锻炼分钟卡片 */} - 锻炼分钟数 + {t('fitnessRingsDetail.cards.exerciseMinutes.title')} ? @@ -418,18 +428,18 @@ export default function FitnessRingsDetailScreen() { {Math.round(appleExerciseTime)}/{appleExerciseTimeGoal} - 分钟 + {t('fitnessRingsDetail.cards.exerciseMinutes.unit')} - {Math.round(appleExerciseTime)}分钟 + {Math.round(appleExerciseTime)}{t('fitnessRingsDetail.cards.exerciseMinutes.unit')} {renderBarChart( hourlyExerciseData.map(h => h.minutes), Math.max(appleExerciseTimeGoal / 8, 1), '#FF9500', - '分钟' + t('fitnessRingsDetail.cards.exerciseMinutes.unit') )} {/* 锻炼分钟说明 */} @@ -450,15 +460,15 @@ export default function FitnessRingsDetailScreen() { } ]} > - 锻炼分钟数: + {t('fitnessRingsDetail.cards.exerciseMinutes.info.title')} - 进行强度不低于"快走"的运动锻炼,就会积累对应时长的锻炼分钟数。 + {t('fitnessRingsDetail.cards.exerciseMinutes.info.description')} - 世卫组织推荐的成年人每天至少保持30分钟以上的中高强度运动。 + {t('fitnessRingsDetail.cards.exerciseMinutes.info.recommendation')} - 知道了 + {t('fitnessRingsDetail.cards.exerciseMinutes.info.knowButton')} )} @@ -467,7 +477,7 @@ export default function FitnessRingsDetailScreen() { {/* 活动小时数卡片 */} - 活动小时数 + {t('fitnessRingsDetail.cards.standHours.title')} ? @@ -477,18 +487,18 @@ export default function FitnessRingsDetailScreen() { {Math.round(appleStandHours)}/{appleStandHoursGoal} - 小时 + {t('fitnessRingsDetail.cards.standHours.unit')} - {Math.round(appleStandHours)}小时 + {Math.round(appleStandHours)}{t('fitnessRingsDetail.cards.standHours.unit')} {renderBarChart( hourlyStandData.map(h => h.hasStood), 1, '#007AFF', - '小时' + t('fitnessRingsDetail.cards.standHours.unit') )} @@ -536,9 +546,9 @@ export default function FitnessRingsDetailScreen() { {/* 周闭环天数统计 */} - 周闭环天数 + {t('fitnessRingsDetail.stats.weeklyClosedRings')} - {getClosedRingCount()}天 + {getClosedRingCount()}{t('fitnessRingsDetail.stats.daysUnit')} @@ -575,12 +585,12 @@ export default function FitnessRingsDetailScreen() { {Platform.OS === 'ios' && ( - 取消 + {t('fitnessRingsDetail.datePicker.cancel')} { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}> - 确定 + {t('fitnessRingsDetail.datePicker.confirm')} )} diff --git a/app/medications/ai-camera.tsx b/app/medications/ai-camera.tsx index b3b09fc..d711926 100644 --- a/app/medications/ai-camera.tsx +++ b/app/medications/ai-camera.tsx @@ -4,6 +4,7 @@ import { Colors } from '@/constants/Colors'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useCosUpload } from '@/hooks/useCosUpload'; +import { useI18n } from '@/hooks/useI18n'; import { createMedicationRecognitionTask } from '@/services/medications'; import { getItem, setItem } from '@/utils/kvStore'; import { Ionicons } from '@expo/vector-icons'; @@ -39,9 +40,9 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; const MEDICATION_GUIDE_SEEN_KEY = 'medication_ai_camera_guide_seen'; const captureSteps = [ - { key: 'front', title: '正面', subtitle: '保证药品名称清晰可见', mandatory: true }, - { key: 'side', title: '背面', subtitle: '包含规格、成分等信息', mandatory: true }, - { key: 'aux', title: '侧面', subtitle: '补充更多细节提升准确率', mandatory: false }, + { key: 'front', mandatory: true }, + { key: 'side', mandatory: true }, + { key: 'aux', mandatory: false }, ] as const; type CaptureKey = (typeof captureSteps)[number]['key']; @@ -51,6 +52,7 @@ type Shot = { }; export default function MedicationAiCameraScreen() { + const { t } = useI18n(); const insets = useSafeAreaInsets(); const scheme = (useColorScheme() ?? 'light') as keyof typeof Colors; const colors = Colors[scheme]; @@ -113,7 +115,14 @@ export default function MedicationAiCameraScreen() { } }, [allRequiredCaptured]); - const stepTitle = useMemo(() => `步骤 ${currentStepIndex + 1} / ${captureSteps.length}`, [currentStepIndex]); + const stepTitle = useMemo( + () => + t('medications.aiCamera.steps.stepProgress', { + current: currentStepIndex + 1, + total: captureSteps.length, + }), + [currentStepIndex, t] + ); // 计算固定的相机高度,不受按钮状态影响,避免布局跳动 const cameraHeight = useMemo(() => { @@ -149,7 +158,7 @@ export default function MedicationAiCameraScreen() { if (!result.canceled && result.assets?.length) { const asset = result.assets[0]; setShots((prev) => ({ ...prev, [currentStep.key]: { uri: asset.uri } })); - + // 拍摄完成后自动进入下一步(如果还有下一步) if (currentStepIndex < captureSteps.length - 1) { setTimeout(() => { @@ -159,7 +168,10 @@ export default function MedicationAiCameraScreen() { } } catch (error) { console.error('[MEDICATION_AI] pick image failed', error); - Alert.alert('选择失败', '请重试或更换图片'); + Alert.alert( + t('medications.aiCamera.alerts.pickFailed.title'), + t('medications.aiCamera.alerts.pickFailed.message') + ); } }; @@ -169,7 +181,7 @@ export default function MedicationAiCameraScreen() { const photo = await cameraRef.current.takePictureAsync({ quality: 0.85 }); if (photo?.uri) { setShots((prev) => ({ ...prev, [currentStep.key]: { uri: photo.uri } })); - + // 拍摄完成后自动进入下一步(如果还有下一步) if (currentStepIndex < captureSteps.length - 1) { setTimeout(() => { @@ -179,7 +191,10 @@ export default function MedicationAiCameraScreen() { } } catch (error) { console.error('[MEDICATION_AI] take picture failed', error); - Alert.alert('拍摄失败', '请重试'); + Alert.alert( + t('medications.aiCamera.alerts.captureFailed.title'), + t('medications.aiCamera.alerts.captureFailed.message') + ); } }; @@ -192,7 +207,10 @@ export default function MedicationAiCameraScreen() { const handleStartRecognition = async () => { // 检查必需照片是否完成 if (!allRequiredCaptured) { - Alert.alert('照片不足', '请至少完成正面和背面拍摄'); + Alert.alert( + t('medications.aiCamera.alerts.insufficientPhotos.title'), + t('medications.aiCamera.alerts.insufficientPhotos.message') + ); return; } @@ -209,7 +227,9 @@ export default function MedicationAiCameraScreen() { const [frontUpload, sideUpload, auxUpload] = await Promise.all([ upload({ uri: shots.front.uri, name: `front-${Date.now()}.jpg`, type: 'image/jpeg' }), upload({ uri: shots.side.uri, name: `side-${Date.now()}.jpg`, type: 'image/jpeg' }), - shots.aux ? upload({ uri: shots.aux.uri, name: `aux-${Date.now()}.jpg`, type: 'image/jpeg' }) : Promise.resolve(null), + shots.aux + ? upload({ uri: shots.aux.uri, name: `aux-${Date.now()}.jpg`, type: 'image/jpeg' }) + : Promise.resolve(null), ]); const task = await createMedicationRecognitionTask({ @@ -227,7 +247,10 @@ export default function MedicationAiCameraScreen() { }); } catch (error: any) { console.error('[MEDICATION_AI] recognize failed', error); - Alert.alert('创建任务失败', error?.message || '请检查网络后重试'); + Alert.alert( + t('medications.aiCamera.alerts.taskFailed.title'), + error?.message || t('medications.aiCamera.alerts.taskFailed.defaultMessage') + ); } finally { setCreatingTask(false); } @@ -278,12 +301,16 @@ export default function MedicationAiCameraScreen() { isInteractive={true} > - 翻转 + + {t('medications.aiCamera.buttons.flip')} + ) : ( - 翻转 + + {t('medications.aiCamera.buttons.flip')} + )} @@ -440,12 +467,16 @@ export default function MedicationAiCameraScreen() { isInteractive={true} > - 拍照 + + {t('medications.aiCamera.buttons.capture')} + ) : ( - 拍照 + + {t('medications.aiCamera.buttons.capture')} + )} @@ -470,7 +501,9 @@ export default function MedicationAiCameraScreen() { ) : ( <> - 完成 + + {t('medications.aiCamera.buttons.complete')} + )} @@ -481,7 +514,9 @@ export default function MedicationAiCameraScreen() { ) : ( <> - 完成 + + {t('medications.aiCamera.buttons.complete')} + )} @@ -501,12 +536,25 @@ export default function MedicationAiCameraScreen() { if (!permission.granted) { return ( - router.back()} transparent /> + router.back()} + transparent + /> - 需要相机权限 - 授权后即可快速拍摄药品包装,自动识别信息 - - 授权访问相机 + + {t('medications.aiCamera.permission.title')} + + + {t('medications.aiCamera.permission.description')} + + + + {t('medications.aiCamera.permission.button')} + @@ -524,14 +572,14 @@ export default function MedicationAiCameraScreen() { router.back()} transparent right={ setShowGuideModal(true)} activeOpacity={0.7} - accessibilityLabel="查看拍摄说明" + accessibilityLabel={t('medications.aiCamera.guideModal.title')} > {isLiquidGlassAvailable() ? ( {stepTitle} - {currentStep.title} - {currentStep.subtitle} + + {t(`medications.aiCamera.steps.${currentStep.key}.title`)} + + + {t(`medications.aiCamera.steps.${currentStep.key}.subtitle`)} + @@ -587,14 +639,22 @@ export default function MedicationAiCameraScreen() { style={[styles.shotCard, active && styles.shotCardActive]} > - {step.title} - {!step.mandatory ? '(可选)' : ''} + {t(`medications.aiCamera.steps.${step.key}.title`)} + {!step.mandatory + ? ` ${t('medications.aiCamera.steps.optional')}` + : ''} {shot ? ( - + ) : ( - 未拍摄 + + {t('medications.aiCamera.steps.notTaken')} + )} @@ -617,12 +677,16 @@ export default function MedicationAiCameraScreen() { isInteractive={true} > - 从相册 + + {t('medications.aiCamera.buttons.album')} + ) : ( - 从相册 + + {t('medications.aiCamera.buttons.album')} + )} diff --git a/app/settings/tab-bar-config.tsx b/app/settings/tab-bar-config.tsx index c1fd530..9e1fc5e 100644 --- a/app/settings/tab-bar-config.tsx +++ b/app/settings/tab-bar-config.tsx @@ -7,11 +7,9 @@ import { type TabConfig, } from '@/store/tabBarConfigSlice'; import { Ionicons } from '@expo/vector-icons'; -import { isLiquidGlassAvailable } from 'expo-glass-effect'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import React, { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; import { Alert, ScrollView, @@ -25,14 +23,14 @@ import { import { HeaderBar } from '@/components/ui/HeaderBar'; import { IconSymbol } from '@/components/ui/IconSymbol'; import { palette } from '@/constants/Colors'; +import { useI18n } from '@/hooks/useI18n'; export default function TabBarConfigScreen() { - const { t } = useTranslation(); + const { t } = useI18n(); const router = useRouter(); const dispatch = useAppDispatch(); const safeAreaTop = useSafeAreaTop(60); const configs = useAppSelector(selectTabBarConfigs); - const isGlassAvailable = isLiquidGlassAvailable(); // 处理开关切换 const handleToggle = useCallback( diff --git a/app/sleep-detail.tsx b/app/sleep-detail.tsx index d3e2ab7..abe0dea 100644 --- a/app/sleep-detail.tsx +++ b/app/sleep-detail.tsx @@ -1,42 +1,42 @@ -import { ThemedView } from '@/components/ThemedView'; -import { - fetchCompleteSleepData, - formatSleepTime, - formatTime, - getSleepStageColor, - SleepStage, - type CompleteSleepData -} from '@/utils/sleepHealthKit'; -import { Ionicons } from '@expo/vector-icons'; -import dayjs from 'dayjs'; -import { router, useLocalSearchParams } from 'expo-router'; -import React, { useCallback, useEffect, useState } from 'react'; -import { - ActivityIndicator, - ScrollView, - StyleSheet, - Text, - TouchableOpacity, - View -} from 'react-native'; - import { InfoModal, type SleepDetailData } from '@/components/sleep/InfoModal'; import { SleepStagesInfoModal } from '@/components/sleep/SleepStagesInfoModal'; import { SleepStageTimeline } from '@/components/sleep/SleepStageTimeline'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; -import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; +import { useI18n } from '@/hooks/useI18n'; +import { + fetchCompleteSleepData, + formatSleepTime, + getSleepStageColor, + SleepStage, + type CompleteSleepData +} from '@/utils/sleepHealthKit'; +import { Ionicons } from '@expo/vector-icons'; +import dayjs from 'dayjs'; +import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; +import { Image } from 'expo-image'; +import { LinearGradient } from 'expo-linear-gradient'; +import { router, useLocalSearchParams } from 'expo-router'; +import React, { useCallback, useEffect, useState } from 'react'; +import { + ActivityIndicator, + Dimensions, + ScrollView, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View +} from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; -// SleepGradeCard 组件现在在 InfoModal 组件内部 - -// SleepStagesInfoModal 组件现在从独立文件导入 - -// InfoModal 组件现在从独立文件导入 +const { width } = Dimensions.get('window'); +const HERO_HEIGHT = width * 0.76; export default function SleepDetailScreen() { - const safeAreaTop = useSafeAreaTop() - + const { t } = useI18n(); + const insets = useSafeAreaInsets(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const [sleepData, setSleepData] = useState(null); @@ -64,19 +64,10 @@ export default function SleepDetailScreen() { const loadSleepData = useCallback(async () => { try { setLoading(true); - console.log('开始加载睡眠数据...'); - const data = await fetchCompleteSleepData(selectedDate); setSleepData(data); - - if (data) { - console.log('睡眠数据加载成功,得分:', data.sleepScore); - } else { - console.log('未找到睡眠数据'); - } - } catch (error) { - console.error('加载睡眠数据失败:', error); + console.error('Failed to load sleep data:', error); } finally { setLoading(false); } @@ -86,15 +77,6 @@ export default function SleepDetailScreen() { loadSleepData(); }, [loadSleepData]); - if (loading) { - return ( - - - 加载睡眠数据中... - - ); - } - // 如果没有数据,使用默认数据结构 const displayData: CompleteSleepData = sleepData || { sleepScore: 0, @@ -108,263 +90,265 @@ export default function SleepDetailScreen() { averageHeartRate: null, sleepHeartRateData: [], sleepEfficiency: 0, - qualityDescription: '暂无睡眠数据', - recommendation: '请确保在真实iOS设备上运行并授权访问健康数据,或等待有睡眠数据后再查看。' + qualityDescription: t('sleepDetail.noData'), + recommendation: t('sleepDetail.noDataRecommendation') }; + const formatDateTitle = (date: Date) => { + if (dayjs(date).isSame(dayjs(), 'day')) { + return t('sleepDetail.today'); + } + return dayjs(date).format('M月D日'); + }; + + const getScoreColor = (score: number) => { + if (score >= 85) return '#10B981'; // Green + if (score >= 70) return '#3B82F6'; // Blue + if (score >= 60) return '#F59E0B'; // Yellow + }; + + // 加载状态 + if (loading) { + return ( + + router.back()} withSafeTop transparent={false} /> + + + {t('sleepDetail.loading')} + + + ); + } + return ( - - {/* 顶部导航 */} - router.back()} - transparent={true} - variant="default" - /> - - + + + + {/* 顶部导航覆盖层 */} + + + {isLiquidGlassAvailable() ? ( + {}} + activeOpacity={0.7} + > + + + + + ) : ( + + + + )} + + } + /> + - {/* 睡眠得分圆形显示 */} - - - {displayData.sleepScore} - 睡眠得分 + {/* Hero 区域 */} + + + + + + + {/* 头部文本块 */} + + + + {displayData.sleepScore} + + {formatDateTitle(selectedDate)}{t('sleepDetail.sleepScore')} + + {displayData.qualityDescription} + {displayData.recommendation} + + + {/* 核心数据卡片 */} + + {/* 睡眠时长 */} + + + + + + + {t('sleepDetail.sleepDuration')} + setInfoModal({ visible: true, title: t('sleepDetail.infoModalTitles.sleepTime'), type: 'sleep-time' })} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + > + + + + + {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '--'} + + + + + {/* 分割线 */} + + + {/* 睡眠质量 */} + + + + + + + {t('sleepDetail.sleepQuality')} + setInfoModal({ visible: true, title: t('sleepDetail.infoModalTitles.sleepQuality'), type: 'sleep-quality' })} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + > + + + + + {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '--%'} + + - {/* 睡眠质量描述 */} - {displayData.qualityDescription} - - {/* 建议文本 */} - {displayData.recommendation} - - {/* 睡眠统计卡片 */} - - - - - - - - 睡眠时间 - - setInfoModal({ - visible: true, - title: '睡眠时间', - type: 'sleep-time' - })} - > - - - - - {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '--'} - - - - - - - - - - - 睡眠质量 - - setInfoModal({ - visible: true, - title: '睡眠质量', - type: 'sleep-quality' - })} - > - - - - - {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '--%'} - - + {/* 睡眠阶段时间轴 */} + + {t('sleepDetail.sleepStages')} + setSleepStagesModal({ visible: true })}> + {t('sleepDetail.learnMore')} + - {/* 睡眠阶段图表 */} - {/* setSleepStagesModal({ visible: true })} - /> */} + + + - {/* 苹果健康风格的睡眠阶段时间轴图表 */} - setSleepStagesModal({ visible: true })} - /> - - {/* 睡眠阶段统计 - 2x2网格布局 */} + {/* 睡眠阶段统计网格 */} - {/* 使用真实数据或默认数据,确保包含所有4个阶段 */} {(() => { let stagesToDisplay; if (displayData.sleepStages.length > 0) { - // 使用真实数据,确保所有阶段都存在 const existingStages = new Map(displayData.sleepStages.map(s => [s.stage, s])); stagesToDisplay = [ - existingStages.get(SleepStage.Awake) || { stage: SleepStage.Awake, duration: 0, percentage: 0, quality: 'good' as any }, - existingStages.get(SleepStage.REM) || { stage: SleepStage.REM, duration: 0, percentage: 0, quality: 'good' as any }, - existingStages.get(SleepStage.Core) || { stage: SleepStage.Core, duration: 0, percentage: 0, quality: 'good' as any }, - existingStages.get(SleepStage.Deep) || { stage: SleepStage.Deep, duration: 0, percentage: 0, quality: 'good' as any } + existingStages.get(SleepStage.Awake) || { stage: SleepStage.Awake, duration: 0, percentage: 0 }, + existingStages.get(SleepStage.REM) || { stage: SleepStage.REM, duration: 0, percentage: 0 }, + existingStages.get(SleepStage.Core) || { stage: SleepStage.Core, duration: 0, percentage: 0 }, + existingStages.get(SleepStage.Deep) || { stage: SleepStage.Deep, duration: 0, percentage: 0 } ]; } else { - // 使用默认数据 stagesToDisplay = [ - { stage: SleepStage.Awake, duration: 0, percentage: 0, quality: 'good' as any }, - { stage: SleepStage.REM, duration: 0, percentage: 0, quality: 'good' as any }, - { stage: SleepStage.Core, duration: 0, percentage: 0, quality: 'good' as any }, - { stage: SleepStage.Deep, duration: 0, percentage: 0, quality: 'poor' as any } + { stage: SleepStage.Awake, duration: 0, percentage: 0 }, + { stage: SleepStage.REM, duration: 0, percentage: 0 }, + { stage: SleepStage.Core, duration: 0, percentage: 0 }, + { stage: SleepStage.Deep, duration: 0, percentage: 0 } ]; } return stagesToDisplay; })().map((stageData, index) => { const getStageName = (stage: SleepStage) => { switch (stage) { - case SleepStage.Awake: return '清醒时间'; - case SleepStage.REM: return '快速眼动'; - case SleepStage.Core: return '核心睡眠'; - case SleepStage.Deep: return '深度睡眠'; - default: return '未知'; + case SleepStage.Awake: return t('sleepDetail.awake'); + case SleepStage.REM: return t('sleepDetail.rem'); + case SleepStage.Core: return t('sleepDetail.core'); + case SleepStage.Deep: return t('sleepDetail.deep'); + default: return t('sleepDetail.unknown'); } }; - const getQualityDisplay = (quality: any) => { - switch (quality) { - case 'excellent': return { text: '★ 优秀', color: '#10B981', bgColor: '#D1FAE5', progressColor: '#10B981', progressWidth: '100%' }; - case 'good': return { text: '✓ 良好', color: '#065F46', bgColor: '#D1FAE5', progressColor: '#10B981', progressWidth: '85%' }; - case 'fair': return { text: '○ 一般', color: '#92400E', bgColor: '#FEF3C7', progressColor: '#F59E0B', progressWidth: '65%' }; - case 'poor': return { text: '⚠ 低', color: '#DC2626', bgColor: '#FECACA', progressColor: '#F59E0B', progressWidth: '45%' }; - default: return { text: '✓ 正常', color: '#065F46', bgColor: '#D1FAE5', progressColor: '#10B981', progressWidth: '75%' }; - } - }; - - const qualityInfo = getQualityDisplay(stageData.quality); - + const stageColor = getSleepStageColor(stageData.stage); + return ( - - - {getStageName(stageData.stage)} - - + + + + + {getStageName(stageData.stage)} + + + {formatSleepTime(stageData.duration)} - - 占总体睡眠的 {stageData.percentage}% + + + + + {stageData.percentage}% ); })} - {/* Raw Sleep Samples List - 显示所有原始睡眠数据 */} - {sleepData && sleepData.rawSleepSamples && sleepData.rawSleepSamples.length > 100 && ( - + {/* 原始数据列表 (如果有大量数据) */} + {sleepData && sleepData.rawSleepSamples && sleepData.rawSleepSamples.length > 0 && ( + - - 原始睡眠数据 ({sleepData.rawSleepSamples.length} 条记录) - - - 查看数据间隔和可能的gap + + {t('sleepDetail.rawData')} ({sleepData.rawSleepSamples.length}) + - - - {sleepData.rawSleepSamples.map((sample, index) => { - // 计算与前一个样本的时间间隔 - const prevSample = index > 0 ? sleepData.rawSleepSamples[index - 1] : null; - let gapMinutes = 0; - let hasGap = false; - - if (prevSample) { - const prevEndTime = new Date(prevSample.endDate).getTime(); - const currentStartTime = new Date(sample.startDate).getTime(); - gapMinutes = (currentStartTime - prevEndTime) / (1000 * 60); - hasGap = gapMinutes > 1; // 大于1分钟视为有间隔 - } - - const startTime = formatTime(sample.startDate); - const endTime = formatTime(sample.endDate); - const duration = Math.round((new Date(sample.endDate).getTime() - new Date(sample.startDate).getTime()) / (1000 * 60)); - - // 获取睡眠阶段中文名称 - const getStageName = (value: SleepStage) => { - switch (value) { - case SleepStage.InBed: return '在床上'; - case SleepStage.Awake: return '清醒'; - case SleepStage.Core: return '核心睡眠'; - case SleepStage.Deep: return '深度睡眠'; - case SleepStage.REM: return 'REM睡眠'; - case SleepStage.Asleep: return '未指定睡眠'; - default: return value; - } - }; - - return ( - - {/* 显示数据间隔 */} - {hasGap && ( - - - - 数据间隔: {Math.round(gapMinutes)}分钟 - - - )} - - {/* 睡眠样本条目 */} - - - - - - {getStageName(sample.value)} - - - - {duration}分钟 - - - - - - {startTime} - {endTime} - - - #{index + 1} - - - - - ); - })} - + {/* 这里可以考虑是否真的需要显示长列表,或者只显示摘要 */} + + {t('sleepDetail.rawDataDescription', { count: sleepData.rawSleepSamples.length })} + )} + + {/* Modals */} {infoModal.type && ( setSleepStagesModal({ visible: false })} /> - + ); } const styles = StyleSheet.create({ container: { flex: 1, + backgroundColor: '#f3f4fb', + }, + safeArea: { + flex: 1, + }, + headerOverlay: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + zIndex: 20, + }, + heroContainer: { + height: HERO_HEIGHT, + width: '100%', + overflow: 'hidden', + position: 'absolute', + top: 0, + }, + heroImage: { + width: '100%', + height: '100%', }, scrollView: { flex: 1, }, scrollContent: { - paddingHorizontal: 20, paddingBottom: 40, }, - scoreContainer: { + headerTextBlock: { + paddingHorizontal: 24, + marginTop: HERO_HEIGHT - 60, alignItems: 'center', }, - circularProgressContainer: { - position: 'relative', + scoreCircle: { alignItems: 'center', justifyContent: 'center', }, - scoreTextContainer: { - alignItems: 'center', - justifyContent: 'center', - }, - scoreNumber: { + scoreValue: { fontSize: 48, fontWeight: '800', - color: '#1F2937', - lineHeight: 48, + fontFamily: 'AliBold', + lineHeight: 56, + textShadowColor: 'rgba(255, 255, 255, 0.5)', + textShadowOffset: { width: 0, height: 2 }, + textShadowRadius: 4, }, scoreLabel: { fontSize: 14, - color: '#6B7280', + color: '#596095', marginTop: 4, + fontFamily: 'AliRegular', + letterSpacing: 0.5, }, - qualityDescription: { + summary: { + marginTop: 16, fontSize: 18, fontWeight: '600', - color: '#1F2937', + color: '#1c1f3a', textAlign: 'center', - marginBottom: 16, + fontFamily: 'AliBold', lineHeight: 24, }, - recommendationText: { - fontSize: 14, - color: '#6B7280', - textAlign: 'center', - lineHeight: 20, - marginBottom: 32, - paddingHorizontal: 16, - }, - statsContainer: { - flexDirection: 'row', - gap: 12, - marginBottom: 32, - paddingHorizontal: 4, - }, - newStatCard: { - flex: 1, - borderRadius: 20, - padding: 16, - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.08, - shadowRadius: 12, - elevation: 4, - borderWidth: 1, - borderColor: 'rgba(0, 0, 0, 0.06)', - }, - statCardHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, - }, - statCardLeftGroup: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - statCardIcon: { - width: 20, - height: 20, - alignItems: 'center', - justifyContent: 'center', - alignSelf: 'center', - }, - infoButton: { - padding: 4, - alignSelf: 'center', - }, - statCard: { - flex: 1, - backgroundColor: 'rgba(255, 255, 255, 0.9)', - borderRadius: 16, - padding: 16, - alignItems: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 3, - }, - statIcon: { - fontSize: 18, - }, - statLabel: { - fontSize: 12, - fontWeight: '500', - letterSpacing: 0.2, - alignSelf: 'center', - }, - newStatValue: { - fontSize: 20, - fontWeight: '600', - marginBottom: 12, - letterSpacing: -0.5, - }, - qualityBadge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 8, - alignSelf: 'flex-start', - }, - goodQualityBadge: { - backgroundColor: '#D1FAE5', - }, - excellentQualityBadge: { - backgroundColor: '#FEF3C7', - }, - qualityBadgeText: { - fontSize: 12, - fontWeight: '600', - letterSpacing: 0.1, - }, - goodQualityText: { - color: '#065F46', - }, - excellentQualityText: { - color: '#92400E', - }, - statValue: { - fontSize: 18, - fontWeight: '700', - color: '#1F2937', - marginBottom: 4, - }, - statQuality: { - fontSize: 12, - color: '#10B981', - fontWeight: '500', - }, - chartContainer: { - backgroundColor: 'rgba(255, 255, 255, 0.9)', - borderRadius: 16, - padding: 16, - marginBottom: 24, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 3, - }, - chartHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 16, - }, - chartTimeLabel: { - alignItems: 'center', - }, - chartTimeText: { - fontSize: 12, - color: '#6B7280', - fontWeight: '500', - }, - chartHeartRate: { - alignItems: 'center', - }, - chartHeartRateText: { - fontSize: 12, - color: '#EF4444', - fontWeight: '600', - }, - chartBars: { - flexDirection: 'row', - alignItems: 'flex-end', - height: 120, - gap: 2, - }, - chartBar: { - borderRadius: 2, - minHeight: 8, - }, - chartTimeScale: { - flexDirection: 'row', - justifyContent: 'space-between', - paddingHorizontal: 4, + recommendation: { marginTop: 8, - }, - chartTimeScaleText: { - fontSize: 10, - color: '#9CA3AF', + fontSize: 14, + lineHeight: 20, + color: '#7080b4', textAlign: 'center', + fontFamily: 'AliRegular', + paddingHorizontal: 10, }, - layeredChartContainer: { - position: 'relative', - marginVertical: 16, + detailCard: { + marginTop: 28, + marginHorizontal: 20, + padding: 20, + borderRadius: 28, + backgroundColor: '#ffffff', + shadowColor: 'rgba(30, 41, 59, 0.1)', + shadowOpacity: 0.2, + shadowRadius: 20, + shadowOffset: { width: 0, height: 10 }, + elevation: 8, }, - sleepBlock: { - borderRadius: 2, - borderWidth: 0.5, - borderColor: 'rgba(255, 255, 255, 0.2)', - }, - baselineLine: { - height: 1, - backgroundColor: '#E5E7EB', - position: 'absolute', - }, - stagesContainer: { - backgroundColor: 'rgba(255, 255, 255, 0.9)', - borderRadius: 16, - padding: 16, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 3, - }, - stageRow: { + detailRow: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'space-between', paddingVertical: 12, - borderBottomWidth: 1, - borderBottomColor: '#F3F4F6', }, - stageInfo: { - flexDirection: 'row', + detailIconWrapper: { + width: 46, + height: 46, + borderRadius: 23, + backgroundColor: '#EEF0FF', alignItems: 'center', + justifyContent: 'center', + }, + detailTextWrapper: { + marginLeft: 16, flex: 1, }, - stageColorDot: { - width: 12, - height: 12, - borderRadius: 6, - marginRight: 12, + detailHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, }, - stageName: { + detailLabel: { fontSize: 14, - color: '#374151', - fontWeight: '500', + color: '#6f7ba7', + fontFamily: 'AliRegular', }, - stageStats: { - alignItems: 'flex-end', - }, - stagePercentage: { - fontSize: 16, + detailValue: { + fontSize: 20, fontWeight: '700', - color: '#1F2937', + color: '#1c1f3a', + marginTop: 4, + fontFamily: 'AliBold', }, - stageDuration: { - fontSize: 12, - color: '#6B7280', - marginTop: 2, + divider: { + height: 1, + backgroundColor: '#F0F2F9', + marginVertical: 4, + marginLeft: 62, // align with text }, - stageQuality: { - fontSize: 11, - fontWeight: '600', - marginTop: 2, - }, - loadingContainer: { - justifyContent: 'center', - alignItems: 'center', - }, - loadingText: { - fontSize: 16, - color: '#6B7280', - marginTop: 16, - }, - errorText: { - fontSize: 16, - color: '#6B7280', + sectionHeader: { + marginTop: 32, + marginHorizontal: 24, marginBottom: 16, - }, - retryButton: { - backgroundColor: Colors.light.primary, - borderRadius: 8, - paddingHorizontal: 24, - paddingVertical: 12, - }, - retryButtonText: { - color: '#FFFFFF', - fontSize: 14, - fontWeight: '600', - }, - noDataContainer: { - alignItems: 'center', - paddingVertical: 24, - }, - noDataText: { - fontSize: 14, - color: '#9CA3AF', - fontStyle: 'italic', - }, - // Info Modal 和 Grade Cards 样式已移动到独立组件中 - mockDataToggle: { - paddingHorizontal: 12, - paddingVertical: 6, - backgroundColor: 'rgba(255, 255, 255, 0.2)', - borderRadius: 16, - borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.3)', - }, - mockDataToggleText: { - fontSize: 12, - fontWeight: '600', - }, - // 简化睡眠阶段图表样式 - simplifiedChartContainer: { - backgroundColor: 'rgba(255, 255, 255, 0.9)', - borderRadius: 16, - padding: 16, - marginBottom: 24, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 3, - }, - chartTitleContainer: { flexDirection: 'row', + alignItems: 'center', justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 16, }, - chartTitle: { - fontSize: 16, + sectionTitle: { + fontSize: 18, + fontWeight: '700', + color: '#1c1f3a', + fontFamily: 'AliBold', + }, + sectionAction: { + fontSize: 13, fontWeight: '600', - color: '#1F2937', + color: '#5F6BF0', + fontFamily: 'AliBold', }, - chartInfoButton: { - padding: 4, - }, - simplifiedChartBar: { - flexDirection: 'row', - height: 24, - borderRadius: 12, + timelineCard: { + marginHorizontal: 20, + borderRadius: 24, + backgroundColor: '#ffffff', + paddingVertical: 8, + paddingHorizontal: 4, + shadowColor: 'rgba(30, 41, 59, 0.08)', + shadowOpacity: 0.15, + shadowRadius: 16, + shadowOffset: { width: 0, height: 8 }, + elevation: 4, overflow: 'hidden', - marginBottom: 16, }, - stageSegment: { - height: '100%', + timelineInner: { + backgroundColor: 'transparent', + shadowOpacity: 0, + elevation: 0, + padding: 0, + marginBottom: 0, + marginHorizontal: 0, }, - chartLegend: { - gap: 8, - }, - legendRow: { - flexDirection: 'row', - justifyContent: 'center', - }, - legendItem: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - }, - legendDot: { - width: 8, - height: 8, - borderRadius: 4, - marginRight: 6, - }, - legendText: { - fontSize: 12, - color: '#6B7280', - fontWeight: '500', - }, - // 睡眠阶段卡片网格样式 stagesGridContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, - paddingHorizontal: 4, + paddingHorizontal: 20, + marginTop: 20, }, stageCard: { - width: '48%', + width: (width - 52) / 2, // 20*2 margin + 12 gap + backgroundColor: '#ffffff', borderRadius: 20, padding: 16, - shadowColor: '#000', + shadowColor: 'rgba(30, 41, 59, 0.06)', shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.08, - shadowRadius: 12, - elevation: 4, - borderWidth: 1, - borderColor: 'rgba(0, 0, 0, 0.06)', - }, - stageCardTitle: { - fontSize: 14, - fontWeight: '500', - marginBottom: 8, - }, - stageCardValue: { - fontSize: 24, - fontWeight: '700', - lineHeight: 28, - marginBottom: 4, - }, - stageCardPercentage: { - fontSize: 12, - marginBottom: 12, - }, - stageCardQuality: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 8, - alignSelf: 'flex-start', - }, - normalQuality: { - backgroundColor: '#D1FAE5', - }, - lowQuality: { - backgroundColor: '#FECACA', - }, - stageCardQualityText: { - fontSize: 12, - fontWeight: '600', - }, - normalQualityText: { - color: '#065F46', - }, - lowQualityText: { - color: '#DC2626', - }, - stageCardProgress: { - height: 6, - backgroundColor: '#E5E7EB', - borderRadius: 3, - overflow: 'hidden', - }, - stageCardProgressBar: { - height: '100%', - borderRadius: 3, - }, - // Sleep Stages Modal 样式已移动到独立组件中 - // 睡眠时间标签样式 - sleepTimeLabels: { - flexDirection: 'row', - justifyContent: 'space-between', - marginBottom: 16, - }, - sleepTimeLabel: { - alignItems: 'center', - }, - sleepTimeText: { - fontSize: 12, - fontWeight: '500', - marginBottom: 4, - }, - sleepTimeValue: { - fontSize: 16, - fontWeight: '700', - letterSpacing: -0.2, - }, - // 调试信息样式 - debugContainer: { - marginHorizontal: 20, - marginBottom: 20, - padding: 16, - borderRadius: 12, - borderWidth: 1, - borderColor: 'rgba(0, 0, 0, 0.1)', - }, - debugTitle: { - fontSize: 14, - fontWeight: '600', - marginBottom: 8, - }, - debugText: { - fontSize: 12, - lineHeight: 16, - marginBottom: 4, - }, - // Raw Sleep Samples List 样式 - rawSamplesContainer: { - borderRadius: 16, - padding: 16, - marginBottom: 24, - marginHorizontal: 4, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, - shadowRadius: 8, + shadowRadius: 12, elevation: 3, }, - rawSamplesHeader: { - marginBottom: 16, - }, - rawSamplesTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 4, - }, - rawSamplesSubtitle: { - fontSize: 12, - fontWeight: '500', - }, - rawSamplesScrollView: { - maxHeight: 400, // 限制高度,避免列表过长 - }, - rawSampleItem: { - paddingVertical: 12, - paddingHorizontal: 16, - borderLeftWidth: 3, - borderLeftColor: 'transparent', - marginBottom: 8, - borderRadius: 8, - backgroundColor: 'rgba(248, 250, 252, 0.5)', - }, - sampleHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 4, - }, - sampleLeft: { + stageHeader: { flexDirection: 'row', alignItems: 'center', - flex: 1, + marginBottom: 12, }, stageDot: { width: 8, @@ -918,39 +564,87 @@ const styles = StyleSheet.create({ borderRadius: 4, marginRight: 8, }, - sampleStage: { - fontSize: 14, - fontWeight: '500', - flex: 1, - }, - sampleDuration: { - fontSize: 12, + stageTitle: { + fontSize: 13, fontWeight: '600', + fontFamily: 'AliBold', }, - sampleTimeRange: { + stageValue: { + fontSize: 18, + fontWeight: '700', + color: '#1c1f3a', + fontFamily: 'AliBold', + marginBottom: 8, + }, + stageProgressBg: { + height: 4, + backgroundColor: '#F0F2F9', + borderRadius: 2, + marginBottom: 8, + overflow: 'hidden', + }, + stageProgressFill: { + height: '100%', + borderRadius: 2, + }, + stagePercentage: { + fontSize: 12, + color: '#6f7ba7', + fontFamily: 'AliRegular', + }, + rawSamplesCard: { + marginTop: 24, + marginHorizontal: 20, + padding: 20, + borderRadius: 24, + backgroundColor: '#ffffff', + opacity: 0.8, + }, + rawSamplesHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', + marginBottom: 8, }, - sampleTime: { + rawSamplesTitle: { + fontSize: 15, + fontWeight: '600', + color: '#1c1f3a', + fontFamily: 'AliBold', + }, + rawSamplesSubtitle: { fontSize: 12, + color: '#6f7ba7', + fontFamily: 'AliRegular', }, - sampleIndex: { - fontSize: 10, - fontWeight: '500', - }, - gapIndicator: { - flexDirection: 'row', + missingContainer: { + flex: 1, alignItems: 'center', justifyContent: 'center', - paddingVertical: 8, - paddingHorizontal: 12, - marginVertical: 4, - borderRadius: 8, - gap: 6, + paddingHorizontal: 32, }, - gapText: { - fontSize: 12, - fontWeight: '600', + missingText: { + fontSize: 16, + color: '#6f7ba7', + marginTop: 16, + fontFamily: 'AliRegular', + }, + headerButtons: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + iconButton: { + width: 40, + height: 40, + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, + fallbackIconButton: { + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.3)', }, }); \ No newline at end of file diff --git a/app/steps/detail.tsx b/app/steps/detail.tsx index 7cbb7e4..022f4c1 100644 --- a/app/steps/detail.tsx +++ b/app/steps/detail.tsx @@ -1,5 +1,6 @@ import { DateSelector } from '@/components/DateSelector'; import { HeaderBar } from '@/components/ui/HeaderBar'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health'; @@ -17,6 +18,7 @@ import { } from 'react-native'; export default function StepsDetailScreen() { + const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() // 获取路由参数 @@ -169,11 +171,11 @@ export default function StepsDetailScreen() { // 活动等级配置 const activityLevels = useMemo(() => [ - { key: 'inactive', label: '不怎么动', minSteps: 0, maxSteps: 3000, color: '#B8C8D6' }, - { key: 'light', label: '轻度活跃', minSteps: 3000, maxSteps: 7500, color: '#93C5FD' }, - { key: 'moderate', label: '中等活跃', minSteps: 7500, maxSteps: 10000, color: '#FCD34D' }, - { key: 'very_active', label: '非常活跃', minSteps: 10000, maxSteps: Infinity, color: '#FB923C' } - ], []); + { key: 'inactive', label: t('stepsDetail.activityLevel.levels.inactive'), minSteps: 0, maxSteps: 3000, color: '#B8C8D6' }, + { key: 'light', label: t('stepsDetail.activityLevel.levels.light'), minSteps: 3000, maxSteps: 7500, color: '#93C5FD' }, + { key: 'moderate', label: t('stepsDetail.activityLevel.levels.moderate'), minSteps: 7500, maxSteps: 10000, color: '#FCD34D' }, + { key: 'very_active', label: t('stepsDetail.activityLevel.levels.very_active'), minSteps: 10000, maxSteps: Infinity, color: '#FB923C' } + ], [t]); // 计算当前活动等级 const currentActivityLevel = useMemo(() => { @@ -211,7 +213,7 @@ export default function StepsDetailScreen() { /> {isLoading ? ( - 加载中... + {t('stepsDetail.loading')} ) : ( {totalSteps.toLocaleString()} - 总步数 + {t('stepsDetail.stats.totalSteps')} {averageHourlySteps} - 平均每小时 + {t('stepsDetail.stats.averagePerHour')} {mostActiveHour ? `${mostActiveHour.hour}:00` : '--'} - 最活跃时段 + {t('stepsDetail.stats.mostActiveTime')} )} @@ -258,7 +260,7 @@ export default function StepsDetailScreen() { {/* 详细柱状图卡片 */} - 每小时步数分布 + {t('stepsDetail.chart.title')} {dayjs(currentSelectedDate).format('YYYY年MM月DD日')} @@ -290,7 +292,7 @@ export default function StepsDetailScreen() { ))} - 平均 {averageHourlySteps}步 + {t('stepsDetail.chart.averageLabel', { steps: averageHourlySteps })} )} @@ -354,9 +356,9 @@ export default function StepsDetailScreen() { {/* 底部时间轴标签 */} - 0:00 - 12:00 - 24:00 + {t('stepsDetail.timeLabels.midnight')} + {t('stepsDetail.timeLabels.noon')} + {t('stepsDetail.timeLabels.nextDay')} @@ -366,7 +368,7 @@ export default function StepsDetailScreen() { {/* 活动级别文本 */} - 你今天的活动量处于 + {t('stepsDetail.activityLevel.currentActivity')} {currentActivityLevel.label} {/* 进度条 */} @@ -388,14 +390,14 @@ export default function StepsDetailScreen() { {totalSteps.toLocaleString()} 步 - 当前 + {t('stepsDetail.activityLevel.progress.current')} {nextActivityLevel ? `${nextActivityLevel.minSteps.toLocaleString()} 步` : '--'} - {nextActivityLevel ? `下一级: ${nextActivityLevel.label}` : '已达最高级'} + {nextActivityLevel ? t('stepsDetail.activityLevel.progress.nextLevel', { level: nextActivityLevel.label }) : t('stepsDetail.activityLevel.progress.highestLevel')} diff --git a/app/water/detail.tsx b/app/water/detail.tsx index 77d0401..9230ae2 100644 --- a/app/water/detail.tsx +++ b/app/water/detail.tsx @@ -3,6 +3,7 @@ import { useColorScheme } from '@/hooks/useColorScheme'; import { useWaterDataByDate } from '@/hooks/useWaterData'; import { getQuickWaterAmount } from '@/utils/userPreferences'; import { Ionicons } from '@expo/vector-icons'; +import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { router, useLocalSearchParams } from 'expo-router'; @@ -20,6 +21,7 @@ import { import { Swipeable } from 'react-native-gesture-handler'; import { HeaderBar } from '@/components/ui/HeaderBar'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import dayjs from 'dayjs'; @@ -28,6 +30,7 @@ interface WaterDetailProps { } const WaterDetail: React.FC = () => { + const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>(); @@ -37,22 +40,14 @@ const WaterDetail: React.FC = () => { const [dailyGoal, setDailyGoal] = useState('2000'); const [quickAddAmount, setQuickAddAmount] = useState('250'); - // Remove modal states as they are now in separate settings page - // 使用新的 hook 来处理指定日期的饮水数据 const { waterRecords, dailyWaterGoal, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate); - - - // 处理设置按钮点击 - 跳转到设置页面 const handleSettingsPress = () => { router.push('/water/settings'); }; - // Remove all modal-related functions as they are now in separate settings page - - // 删除饮水记录 const handleDeleteRecord = async (recordId: string) => { await removeWaterRecord(recordId); @@ -70,13 +65,17 @@ const WaterDetail: React.FC = () => { setDailyGoal(dailyWaterGoal.toString()); } } catch (error) { - console.error('加载用户偏好设置失败:', error); + console.error(t('waterDetail.loadingUserPreferences'), error); } }; loadUserPreferences(); }, [dailyWaterGoal]); + const totalAmount = waterRecords?.reduce((sum, record) => sum + record.amount, 0) || 0; + const currentGoal = dailyWaterGoal || 2000; + const progress = Math.min(100, (totalAmount / currentGoal) * 100); + // 新增:饮水记录卡片组件 const WaterRecordCard = ({ record, onDelete }: { record: any; onDelete: () => void }) => { const swipeableRef = React.useRef(null); @@ -84,15 +83,15 @@ const WaterDetail: React.FC = () => { // 处理删除操作 const handleDelete = () => { Alert.alert( - '确认删除', - '确定要删除这条饮水记录吗?此操作无法撤销。', + t('waterDetail.deleteConfirm.title'), + t('waterDetail.deleteConfirm.message'), [ { - text: '取消', + text: t('waterDetail.deleteConfirm.cancel'), style: 'cancel', }, { - text: '删除', + text: t('waterDetail.deleteConfirm.confirm'), style: 'destructive', onPress: () => { onDelete(); @@ -112,7 +111,6 @@ const WaterDetail: React.FC = () => { activeOpacity={0.8} > - 删除 ); }; @@ -125,29 +123,29 @@ const WaterDetail: React.FC = () => { rightThreshold={40} overshootRight={false} > - + - + - + {t('waterDetail.water')} - - + + {dayjs(record.recordedAt || record.createdAt).format('HH:mm')} - {record.amount}ml + {record.amount}ml {record.note && ( - {record.note} + {record.note} )} @@ -157,32 +155,47 @@ const WaterDetail: React.FC = () => { return ( - {/* 背景渐变 */} + {/* 背景 */} + {/* 顶部装饰性渐变 - 模仿挑战页面的柔和背景感 */} + - {/* 装饰性圆圈 */} - - - { - // 这里会通过路由自动处理返回 - router.back(); - }} + title={t('waterDetail.title')} + onBack={() => router.back()} right={ - - - + isLiquidGlassAvailable() ? ( + + + + + + ) : ( + + + + ) } /> @@ -197,13 +210,37 @@ const WaterDetail: React.FC = () => { }]} showsVerticalScrollIndicator={false} > - - {/* 第二部分:饮水记录 */} - - - {selectedDate ? dayjs(selectedDate).format('MM月DD日') : '今日'}饮水记录 + + + {selectedDate ? dayjs(selectedDate).format('MM月DD日') : t('waterDetail.today')} + {t('waterDetail.waterRecord')} + + {/* 进度卡片 */} + + + + {t('waterDetail.total')} + {totalAmount}ml + + + {t('waterDetail.goal')} + {currentGoal}ml + + + + + + + + {/* 记录列表 */} + {waterRecords && waterRecords.length > 0 ? ( {waterRecords.map((record) => ( @@ -213,29 +250,20 @@ const WaterDetail: React.FC = () => { onDelete={() => handleDeleteRecord(record.id)} /> ))} - - {/* 总计显示 */} - - - 总计:{waterRecords.reduce((sum, record) => sum + record.amount, 0)}ml - - - 目标:{dailyWaterGoal}ml - - ) : ( - - 暂无饮水记录 - 点击"添加记录"开始记录饮水量 + + {t('waterDetail.noRecords')} + {t('waterDetail.noRecordsSubtitle')} )} - - {/* All modals have been moved to the separate water-settings page */} ); }; @@ -245,32 +273,12 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: '#f3f4fb', }, - gradientBackground: { + topGradient: { position: 'absolute', left: 0, right: 0, top: 0, - bottom: 0, - }, - decorativeCircle1: { - position: 'absolute', - top: 80, - right: 30, - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: '#4F5BD5', - opacity: 0.08, - }, - decorativeCircle2: { - position: 'absolute', - bottom: 100, - left: -20, - width: 60, - height: 60, - borderRadius: 30, - backgroundColor: '#4F5BD5', - opacity: 0.06, + height: 300, }, keyboardAvoidingView: { flex: 1, @@ -279,54 +287,107 @@ const styles = StyleSheet.create({ flex: 1, }, scrollContent: { + paddingBottom: 40, + }, + headerBlock: { paddingHorizontal: 24, - paddingTop: 20, - }, - section: { - marginBottom: 36, - }, - sectionTitle: { - fontSize: 20, - fontWeight: '700', + marginTop: 10, marginBottom: 24, - letterSpacing: -0.5, - color: '#1c1f3a', }, - subsectionTitle: { + pageTitle: { + fontSize: 28, + fontWeight: '800', + color: '#1c1f3a', + fontFamily: 'AliBold', + marginBottom: 4, + }, + pageSubtitle: { + fontSize: 16, + color: '#6f7ba7', + fontFamily: 'AliRegular', + }, + + // 进度卡片 + progressCard: { + marginHorizontal: 24, + marginBottom: 32, + padding: 24, + borderRadius: 28, + backgroundColor: '#ffffff', + shadowColor: 'rgba(30, 41, 59, 0.1)', + shadowOffset: { width: 0, height: 10 }, + shadowOpacity: 0.18, + shadowRadius: 20, + elevation: 8, + }, + progressInfo: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-end', + marginBottom: 16, + }, + progressLabel: { + fontSize: 14, + color: '#6f7ba7', + marginBottom: 6, + fontFamily: 'AliRegular', + }, + progressValue: { + fontSize: 32, + fontWeight: '800', + color: '#4F5BD5', + fontFamily: 'AliBold', + lineHeight: 32, + }, + progressGoalValue: { + fontSize: 24, + fontWeight: '700', + color: '#1c1f3a', + fontFamily: 'AliBold', + lineHeight: 32, + }, + progressUnit: { fontSize: 16, fontWeight: '600', - marginBottom: 16, - letterSpacing: -0.3, - color: '#1c1f3a', - }, - sectionSubtitle: { - fontSize: 14, - fontWeight: '500', - lineHeight: 20, color: '#6f7ba7', + marginLeft: 2, + fontFamily: 'AliRegular', }, - // 饮水记录相关样式 + progressBarBg: { + height: 12, + backgroundColor: '#F0F2F5', + borderRadius: 6, + overflow: 'hidden', + }, + progressBarFill: { + height: '100%', + borderRadius: 6, + }, + + section: { + paddingHorizontal: 24, + }, + + // 记录列表样式 recordsList: { gap: 16, }, recordCardContainer: { - // iOS 阴影效果 - 增强阴影效果 - shadowColor: 'rgba(30, 41, 59, 0.18)', + shadowColor: 'rgba(30, 41, 59, 0.08)', shadowOffset: { width: 0, height: 8 }, - shadowOpacity: 0.16, - shadowRadius: 16, - // Android 阴影效果 - elevation: 6, + shadowOpacity: 0.12, + shadowRadius: 12, + elevation: 4, + marginBottom: 2, }, recordCard: { - borderRadius: 20, + borderRadius: 24, padding: 18, backgroundColor: '#ffffff', }, recordMainContent: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'space-between', }, recordIconContainer: { width: 48, @@ -334,7 +395,7 @@ const styles = StyleSheet.create({ borderRadius: 16, alignItems: 'center', justifyContent: 'center', - backgroundColor: 'rgba(79, 91, 213, 0.08)', + backgroundColor: '#f5f6ff', }, recordIcon: { width: 24, @@ -345,15 +406,21 @@ const styles = StyleSheet.create({ marginLeft: 16, }, recordLabel: { - fontSize: 17, + fontSize: 16, fontWeight: '700', color: '#1c1f3a', - marginBottom: 6, + marginBottom: 4, + fontFamily: 'AliBold', }, recordTimeContainer: { flexDirection: 'row', alignItems: 'center', - gap: 6, + gap: 4, + }, + recordTimeText: { + fontSize: 13, + color: '#6f7ba7', + fontFamily: 'AliRegular', }, recordAmountContainer: { alignItems: 'flex-end', @@ -362,364 +429,74 @@ const styles = StyleSheet.create({ fontSize: 18, fontWeight: '700', color: '#4F5BD5', - }, - deleteSwipeButton: { - backgroundColor: '#EF4444', - justifyContent: 'center', - alignItems: 'center', - width: 80, - borderRadius: 16, - marginLeft: 12, - }, - deleteSwipeButtonText: { - color: '#FFFFFF', - fontSize: 12, - fontWeight: '600', - marginTop: 4, - }, - recordTimeText: { - fontSize: 13, - fontWeight: '500', - color: '#6f7ba7', + fontFamily: 'AliBold', }, recordNote: { - marginTop: 12, + marginTop: 14, padding: 12, - backgroundColor: 'rgba(79, 91, 213, 0.04)', + backgroundColor: '#F8F9FC', borderRadius: 12, - fontSize: 14, - fontStyle: 'normal', - lineHeight: 20, + fontSize: 13, + lineHeight: 18, color: '#5f6a97', + fontFamily: 'AliRegular', }, - recordsSummary: { - marginTop: 24, - padding: 20, - borderRadius: 20, - backgroundColor: '#ffffff', - shadowColor: 'rgba(30, 41, 59, 0.12)', - shadowOpacity: 0.16, - shadowRadius: 18, - shadowOffset: { width: 0, height: 10 }, - elevation: 6, - flexDirection: 'row', - justifyContent: 'space-between', + deleteSwipeButton: { + backgroundColor: '#FF6B6B', + justifyContent: 'center', alignItems: 'center', + width: 70, + height: '100%', + borderRadius: 24, + marginLeft: 12, }, - summaryText: { - fontSize: 16, - fontWeight: '700', - color: '#1c1f3a', - }, - summaryGoal: { - fontSize: 14, - fontWeight: '600', - color: '#6f7ba7', - }, + noRecordsContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, - gap: 20, + backgroundColor: '#ffffff', + borderRadius: 28, + shadowColor: 'rgba(30, 41, 59, 0.06)', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.1, + shadowRadius: 12, }, noRecordsText: { - fontSize: 17, + fontSize: 16, fontWeight: '600', - lineHeight: 24, - color: '#6f7ba7', + color: '#1c1f3a', + marginBottom: 8, + fontFamily: 'AliBold', }, noRecordsSubText: { fontSize: 14, - textAlign: 'center', - lineHeight: 20, color: '#9ba3c7', + fontFamily: 'AliRegular', }, - modalBackdrop: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0,0,0,0.4)', + + // Settings Button + settingsButtonWrapper: { + width: 40, + height: 40, + borderRadius: 20, + overflow: 'hidden', }, - modalSheet: { - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - padding: 16, - backgroundColor: '#FFFFFF', - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - // iOS 阴影效果 - shadowColor: '#000000', - shadowOffset: { width: 0, height: -2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - // Android 阴影效果 - elevation: 16, - }, - modalHandle: { - width: 36, - height: 4, - backgroundColor: '#E0E0E0', - borderRadius: 2, - alignSelf: 'center', - marginBottom: 20, - }, - modalTitle: { - fontSize: 20, - fontWeight: '600', - textAlign: 'center', - marginBottom: 20, - }, - pickerContainer: { - height: 200, - marginBottom: 20, - }, - picker: { - height: 200, - }, - modalActions: { - flexDirection: 'row', - justifyContent: 'flex-end', - gap: 12, - }, - modalBtn: { - paddingHorizontal: 14, - paddingVertical: 10, - borderRadius: 10, - minWidth: 80, + settingsButtonGlass: { + width: 40, + height: 40, alignItems: 'center', + justifyContent: 'center', }, - modalBtnPrimary: { - // backgroundColor will be set dynamically - }, - modalBtnText: { - fontSize: 16, - fontWeight: '600', - }, - modalBtnTextPrimary: { - // color will be set dynamically - }, - settingsButton: { + settingsButtonFallback: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', borderRadius: 20, - backgroundColor: 'rgba(255, 255, 255, 0.24)', + backgroundColor: '#ffffff', borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.45)', - }, - settingsModalSheet: { - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - padding: 16, - backgroundColor: '#FFFFFF', - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - shadowColor: '#000000', - shadowOffset: { width: 0, height: -2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 16, - }, - settingsModalTitle: { - fontSize: 18, - fontWeight: '600', - textAlign: 'center', - marginBottom: 20, - }, - settingsMenuContainer: { - backgroundColor: '#FFFFFF', - borderRadius: 12, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 4, - elevation: 2, - overflow: 'hidden', - }, - settingsMenuItem: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingVertical: 14, - paddingHorizontal: 16, - borderBottomWidth: 1, - borderBottomColor: '#F1F3F4', - }, - settingsMenuItemLeft: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - }, - settingsIconContainer: { - width: 32, - height: 32, - borderRadius: 6, - alignItems: 'center', - justifyContent: 'center', - marginRight: 12, - }, - settingsMenuItemContent: { - flex: 1, - }, - settingsMenuItemTitle: { - fontSize: 15, - fontWeight: '500', - marginBottom: 2, - }, - settingsMenuItemSubtitle: { - fontSize: 12, - marginBottom: 4, - }, - settingsMenuItemValue: { - fontSize: 14, - }, - // 喝水提醒配置弹窗样式 - waterReminderModalSheet: { - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - padding: 16, - backgroundColor: '#FFFFFF', - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - maxHeight: '80%', - shadowColor: '#000000', - shadowOffset: { width: 0, height: -2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 16, - }, - waterReminderContent: { - flex: 1, - marginBottom: 20, - }, - waterReminderSection: { - marginBottom: 24, - }, - waterReminderSectionHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: 8, - }, - waterReminderSectionTitleContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - waterReminderSectionTitle: { - fontSize: 16, - fontWeight: '600', - }, - waterReminderSectionDesc: { - fontSize: 14, - lineHeight: 20, - marginTop: 4, - }, - timeRangeContainer: { - flexDirection: 'row', - gap: 16, - marginTop: 16, - }, - timePickerContainer: { - flex: 1, - }, - timeLabel: { - fontSize: 14, - fontWeight: '500', - marginBottom: 8, - }, - timePicker: { - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 8, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 8, - }, - timePickerText: { - fontSize: 16, - fontWeight: '500', - }, - timePickerIcon: { - opacity: 0.6, - }, - intervalContainer: { - marginTop: 16, - }, - intervalPickerContainer: { - backgroundColor: '#F8F9FA', - borderRadius: 8, - overflow: 'hidden', - }, - intervalPicker: { - height: 120, - }, - // 时间选择器弹窗样式 - timePickerModalSheet: { - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - padding: 16, - backgroundColor: '#FFFFFF', - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - maxHeight: '60%', - shadowColor: '#000000', - shadowOffset: { width: 0, height: -2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 16, - }, - timePickerContent: { - flex: 1, - marginBottom: 20, - }, - timePickerSection: { - marginBottom: 20, - }, - timePickerLabel: { - fontSize: 16, - fontWeight: '600', - marginBottom: 12, - textAlign: 'center', - }, - hourPickerContainer: { - backgroundColor: '#F8F9FA', - borderRadius: 8, - overflow: 'hidden', - }, - hourPicker: { - height: 160, - }, - timeRangePreview: { - backgroundColor: '#F0F8FF', - borderRadius: 8, - padding: 16, - marginTop: 16, - alignItems: 'center', - }, - timeRangePreviewLabel: { - fontSize: 12, - fontWeight: '500', - marginBottom: 4, - }, - timeRangePreviewText: { - fontSize: 18, - fontWeight: '600', - marginBottom: 8, - }, - timeRangeWarning: { - fontSize: 12, - color: '#FF6B6B', - textAlign: 'center', - lineHeight: 18, + borderColor: 'rgba(0,0,0,0.05)', }, }); diff --git a/app/water/reminder-settings.tsx b/app/water/reminder-settings.tsx index 089cef0..2307dff 100644 --- a/app/water/reminder-settings.tsx +++ b/app/water/reminder-settings.tsx @@ -22,9 +22,11 @@ import { } from 'react-native'; import { HeaderBar } from '@/components/ui/HeaderBar'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; const WaterReminderSettings: React.FC = () => { + const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; @@ -71,9 +73,9 @@ const WaterReminderSettings: React.FC = () => { setStartTimePickerVisible(false); } else { Alert.alert( - '时间设置提示', - '开始时间不能晚于或等于结束时间,请重新选择', - [{ text: '确定' }] + t('waterReminderSettings.alerts.timeValidation.title'), + t('waterReminderSettings.alerts.timeValidation.startTimeInvalid'), + [{ text: t('waterReminderSettings.buttons.confirm') }] ); } }; @@ -91,9 +93,9 @@ const WaterReminderSettings: React.FC = () => { setEndTimePickerVisible(false); } else { Alert.alert( - '时间设置提示', - '结束时间不能早于或等于开始时间,请重新选择', - [{ text: '确定' }] + t('waterReminderSettings.alerts.timeValidation.title'), + t('waterReminderSettings.alerts.timeValidation.endTimeInvalid'), + [{ text: t('waterReminderSettings.buttons.confirm') }] ); } }; @@ -125,18 +127,28 @@ const WaterReminderSettings: React.FC = () => { if (waterReminderSettings.enabled) { const timeInfo = `${waterReminderSettings.startTime}-${waterReminderSettings.endTime}`; - const intervalInfo = `每${waterReminderSettings.interval}分钟`; + const intervalInfo = `${waterReminderSettings.interval}${t('waterReminderSettings.labels.minutes')}`; Alert.alert( - '设置成功', - `喝水提醒已开启\n\n时间段:${timeInfo}\n提醒间隔:${intervalInfo}\n\n我们将在指定时间段内定期提醒您喝水`, - [{ text: '确定', onPress: () => router.back() }] + t('waterReminderSettings.alerts.success.enabled'), + t('waterReminderSettings.alerts.success.enabledMessage', { + timeRange: timeInfo, + interval: intervalInfo + }), + [{ text: t('waterReminderSettings.buttons.confirm'), onPress: () => router.back() }] ); } else { - Alert.alert('设置成功', '喝水提醒已关闭', [{ text: '确定', onPress: () => router.back() }]); + Alert.alert( + t('waterReminderSettings.alerts.success.disabled'), + t('waterReminderSettings.alerts.success.disabledMessage'), + [{ text: t('waterReminderSettings.buttons.confirm'), onPress: () => router.back() }] + ); } } catch (error) { console.error('保存喝水提醒设置失败:', error); - Alert.alert('保存失败', '无法保存喝水提醒设置,请重试'); + Alert.alert( + t('waterReminderSettings.alerts.error.title'), + t('waterReminderSettings.alerts.error.message') + ); } }; @@ -176,7 +188,7 @@ const WaterReminderSettings: React.FC = () => { { router.back(); }} @@ -198,7 +210,7 @@ const WaterReminderSettings: React.FC = () => { - 推送提醒 + {t('waterReminderSettings.sections.notifications')} { /> - 开启后将在指定时间段内定期推送喝水提醒 + {t('waterReminderSettings.descriptions.notifications')} @@ -216,15 +228,15 @@ const WaterReminderSettings: React.FC = () => { {waterReminderSettings.enabled && ( <> - 提醒时间段 + {t('waterReminderSettings.sections.timeRange')} - 只在指定时间段内发送提醒,避免打扰您的休息 + {t('waterReminderSettings.descriptions.timeRange')} {/* 开始时间 */} - 开始时间 + {t('waterReminderSettings.labels.startTime')} { {/* 结束时间 */} - 结束时间 + {t('waterReminderSettings.labels.endTime')} { {/* 提醒间隔设置 */} - 提醒间隔 + {t('waterReminderSettings.sections.interval')} - 选择提醒的频率,建议30-120分钟为宜 + {t('waterReminderSettings.descriptions.interval')} @@ -263,7 +275,7 @@ const WaterReminderSettings: React.FC = () => { style={styles.intervalPicker} > {[30, 45, 60, 90, 120, 150, 180].map(interval => ( - + ))} @@ -279,7 +291,7 @@ const WaterReminderSettings: React.FC = () => { onPress={handleWaterReminderSave} activeOpacity={0.8} > - 保存设置 + {t('waterReminderSettings.labels.saveSettings')} @@ -295,11 +307,11 @@ const WaterReminderSettings: React.FC = () => { setStartTimePickerVisible(false)} /> - 选择开始时间 + {t('waterReminderSettings.labels.startTime')} - 小时 + {t('waterReminderSettings.labels.hours')} { - 时间段预览 + {t('waterReminderSettings.labels.timeRangePreview')} {String(tempStartHour).padStart(2, '0')}:00 - {waterReminderSettings.endTime} {!isValidTimeRange(`${String(tempStartHour).padStart(2, '0')}:00`, waterReminderSettings.endTime) && ( - ⚠️ 开始时间不能晚于或等于结束时间 + ⚠️ {t('waterReminderSettings.alerts.timeValidation.startTimeInvalid')} )} @@ -329,13 +341,13 @@ const WaterReminderSettings: React.FC = () => { onPress={() => setStartTimePickerVisible(false)} style={[styles.modalBtn, { backgroundColor: 'white' }]} > - 取消 + {t('waterReminderSettings.buttons.cancel')} - 确定 + {t('waterReminderSettings.buttons.confirm')} @@ -351,11 +363,11 @@ const WaterReminderSettings: React.FC = () => { setEndTimePickerVisible(false)} /> - 选择结束时间 + {t('waterReminderSettings.labels.endTime')} - 小时 + {t('waterReminderSettings.labels.hours')} { - 时间段预览 + {t('waterReminderSettings.labels.timeRangePreview')} {waterReminderSettings.startTime} - {String(tempEndHour).padStart(2, '0')}:00 {!isValidTimeRange(waterReminderSettings.startTime, `${String(tempEndHour).padStart(2, '0')}:00`) && ( - ⚠️ 结束时间不能早于或等于开始时间 + ⚠️ {t('waterReminderSettings.alerts.timeValidation.endTimeInvalid')} )} @@ -385,13 +397,13 @@ const WaterReminderSettings: React.FC = () => { onPress={() => setEndTimePickerVisible(false)} style={[styles.modalBtn, { backgroundColor: 'white' }]} > - 取消 + {t('waterReminderSettings.buttons.cancel')} - 确定 + {t('waterReminderSettings.buttons.confirm')} diff --git a/app/water/settings.tsx b/app/water/settings.tsx index 445689b..8248d78 100644 --- a/app/water/settings.tsx +++ b/app/water/settings.tsx @@ -21,9 +21,11 @@ import { } from 'react-native'; import { HeaderBar } from '@/components/ui/HeaderBar'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; const WaterSettings: React.FC = () => { + const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; @@ -74,7 +76,10 @@ const WaterSettings: React.FC = () => { setGoalModalVisible(false); // 这里可以添加保存到本地存储或发送到后端的逻辑 - Alert.alert('设置成功', `每日饮水目标已设置为 ${tempGoal}ml`); + Alert.alert( + t('waterSettings.alerts.goalSuccess.title'), + t('waterSettings.alerts.goalSuccess.message', { amount: tempGoal }) + ); }; // 处理快速添加默认值确认 @@ -84,9 +89,15 @@ const WaterSettings: React.FC = () => { try { await setQuickWaterAmount(tempQuickAdd); - Alert.alert('设置成功', `快速添加默认值已设置为 ${tempQuickAdd}ml`); + Alert.alert( + t('waterSettings.alerts.quickAddSuccess.title'), + t('waterSettings.alerts.quickAddSuccess.message', { amount: tempQuickAdd }) + ); } catch { - Alert.alert('设置失败', '无法保存快速添加默认值,请重试'); + Alert.alert( + t('waterSettings.alerts.quickAddFailed.title'), + t('waterSettings.alerts.quickAddFailed.message') + ); } }; @@ -101,7 +112,7 @@ const WaterSettings: React.FC = () => { const reminderSettings = await getWaterReminderSettings(); setWaterReminderSettings(reminderSettings); } catch (error) { - console.error('加载用户偏好设置失败:', error); + console.error('Failed to load user preferences:', error); } }, []); @@ -132,7 +143,7 @@ const WaterSettings: React.FC = () => { { router.back(); }} @@ -156,8 +167,8 @@ const WaterSettings: React.FC = () => { - 每日饮水目标 - {currentWaterGoal}ml + {t('waterSettings.sections.dailyGoal')} + {currentWaterGoal}{t('waterSettings.labels.ml')} @@ -169,11 +180,11 @@ const WaterSettings: React.FC = () => { - 快速添加默认值 + {t('waterSettings.sections.quickAdd')} - 设置点击"+"按钮时添加的默认饮水量 + {t('waterSettings.descriptions.quickAdd')} - {quickAddAmount}ml + {quickAddAmount}{t('waterSettings.labels.ml')} @@ -185,12 +196,19 @@ const WaterSettings: React.FC = () => { - 喝水提醒 + {t('waterSettings.sections.reminder')} - 设置定时提醒您补充水分 + {t('waterSettings.descriptions.reminder')} - {waterReminderSettings.enabled ? `${waterReminderSettings.startTime}-${waterReminderSettings.endTime}, 每${waterReminderSettings.interval}分钟` : '已关闭'} + {waterReminderSettings.enabled + ? t('waterSettings.status.reminderEnabled', { + startTime: waterReminderSettings.startTime, + endTime: waterReminderSettings.endTime, + interval: waterReminderSettings.interval + }) + : t('waterSettings.labels.disabled') + } @@ -211,7 +229,7 @@ const WaterSettings: React.FC = () => { setGoalModalVisible(false)} /> - 每日饮水目标 + {t('waterSettings.sections.dailyGoal')} { style={styles.picker} > {Array.from({ length: 96 }, (_, i) => 500 + i * 100).map(goal => ( - + ))} @@ -228,13 +246,13 @@ const WaterSettings: React.FC = () => { onPress={() => setGoalModalVisible(false)} style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]} > - 取消 + {t('waterSettings.buttons.cancel')} - 确定 + {t('waterSettings.buttons.confirm')} @@ -250,7 +268,7 @@ const WaterSettings: React.FC = () => { setQuickAddModalVisible(false)} /> - 快速添加默认值 + {t('waterSettings.sections.quickAdd')} { style={styles.picker} > {Array.from({ length: 41 }, (_, i) => 50 + i * 10).map(amount => ( - + ))} @@ -267,13 +285,13 @@ const WaterSettings: React.FC = () => { onPress={() => setQuickAddModalVisible(false)} style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]} > - 取消 + {t('waterSettings.buttons.cancel')} - 确定 + {t('waterSettings.buttons.confirm')} diff --git a/app/weight-records.tsx b/app/weight-records.tsx index 8d90a75..1300393 100644 --- a/app/weight-records.tsx +++ b/app/weight-records.tsx @@ -5,6 +5,7 @@ import { Colors } from '@/constants/Colors'; import { getTabBarBottomPadding } from '@/constants/TabBar'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { appStoreReviewService } from '@/services/appStoreReview'; import { deleteWeightRecord, fetchWeightHistory, updateUserProfile, updateWeightRecord, WeightHistoryItem } from '@/store/userSlice'; @@ -24,6 +25,7 @@ import { } from 'react-native'; export default function WeightRecordsPage() { + const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() const dispatch = useAppDispatch(); @@ -43,7 +45,7 @@ export default function WeightRecordsPage() { try { await dispatch(fetchWeightHistory() as any); } catch (error) { - console.error('加载体重历史失败:', error); + console.error(t('weightRecords.loadingHistory'), error); } }, [dispatch]); @@ -92,15 +94,15 @@ export default function WeightRecordsPage() { await dispatch(deleteWeightRecord(id) as any); await loadWeightHistory(); } catch (error) { - console.error('删除体重记录失败:', error); - Alert.alert('错误', '删除体重记录失败,请重试'); + console.error(t('weightRecords.alerts.deleteFailed'), error); + Alert.alert('错误', t('weightRecords.alerts.deleteFailed')); } }; const handleWeightSave = async () => { const weight = parseFloat(inputWeight); if (isNaN(weight) || weight <= 0 || weight > 500) { - alert('请输入有效的体重值(0-500kg)'); + alert(t('weightRecords.alerts.invalidWeight')); return; } @@ -130,8 +132,8 @@ export default function WeightRecordsPage() { setEditingRecord(null); await loadWeightHistory(); } catch (error) { - console.error('保存体重失败:', error); - Alert.alert('错误', '保存体重失败,请重试'); + console.error(t('weightRecords.alerts.saveFailed'), error); + Alert.alert('错误', t('weightRecords.alerts.saveFailed')); } }; @@ -190,7 +192,7 @@ export default function WeightRecordsPage() { /> } @@ -204,27 +206,27 @@ export default function WeightRecordsPage() { {totalWeightLoss.toFixed(1)}kg - 累计减重 + {t('weightRecords.stats.totalLoss')} {currentWeight.toFixed(1)}kg - 当前体重 + {t('weightRecords.stats.currentWeight')} {initialWeight.toFixed(1)}kg - 初始体重 + {t('weightRecords.stats.initialWeight')} - + {targetWeight.toFixed(1)}kg - 目标体重 + {t('weightRecords.stats.targetWeight')} - + @@ -282,8 +284,8 @@ export default function WeightRecordsPage() { ) : ( - 暂无体重记录 - 点击右上角添加按钮开始记录 + {t('weightRecords.empty.title')} + {t('weightRecords.empty.subtitle')} )} @@ -309,10 +311,10 @@ export default function WeightRecordsPage() { - {pickerType === 'current' && '记录体重'} - {pickerType === 'initial' && '编辑初始体重'} - {pickerType === 'target' && '编辑目标体重'} - {pickerType === 'edit' && '编辑体重记录'} + {pickerType === 'current' && t('weightRecords.modal.recordWeight')} + {pickerType === 'initial' && t('weightRecords.modal.editInitialWeight')} + {pickerType === 'target' && t('weightRecords.modal.editTargetWeight')} + {pickerType === 'edit' && t('weightRecords.modal.editRecord')} @@ -329,21 +331,21 @@ export default function WeightRecordsPage() { - {inputWeight || '输入体重'} + {inputWeight || t('weightRecords.modal.inputPlaceholder')} - kg + {t('weightRecords.modal.unit')} {/* Weight Range Hint */} - 请输入 0-500 之间的数值,支持小数 + {t('weightRecords.modal.inputHint')} {/* Quick Selection */} - 快速选择 + {t('weightRecords.modal.quickSelection')} {[50, 60, 70, 80, 90].map((weight) => ( - {weight}kg + {weight}{t('weightRecords.modal.unit')} ))} @@ -386,7 +388,7 @@ export default function WeightRecordsPage() { onPress={handleWeightSave} disabled={!inputWeight.trim()} > - 确定 + {t('weightRecords.modal.confirm')} @@ -461,7 +463,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, statValue: { - fontSize: 16, + fontSize: 14, fontWeight: '800', color: '#192126', marginBottom: 4, @@ -475,6 +477,7 @@ const styles = StyleSheet.create({ fontSize: 12, color: '#687076', marginRight: 4, + textAlign: 'center' }, editIcon: { padding: 2, diff --git a/app/workout/history.tsx b/app/workout/history.tsx index d6186b9..40b6752 100644 --- a/app/workout/history.tsx +++ b/app/workout/history.tsx @@ -16,6 +16,7 @@ import { import { HeaderBar } from '@/components/ui/HeaderBar'; import { IntensityBadge, WorkoutDetailModal } from '@/components/workout/WorkoutDetailModal'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { getWorkoutDetailMetrics, WorkoutDetailMetrics } from '@/services/workoutDetail'; import { @@ -233,23 +234,23 @@ function computeMonthlyStats(workouts: WorkoutData[]): MonthlyStatsInfo | null { }; } -function getIntensityBadge(totalCalories?: number, durationInSeconds?: number) { +function getIntensityBadge(t: (key: string, options?: any) => string, totalCalories?: number, durationInSeconds?: number): { label: string; color: string; background: string } { if (!totalCalories || !durationInSeconds) { - return { label: '低强度', color: '#7C85A3', background: '#E4E7F2' }; + return { label: t('workoutHistory.intensity.low'), color: '#7C85A3', background: '#E4E7F2' }; } const minutes = Math.max(durationInSeconds / 60, 1); const caloriesPerMinute = totalCalories / minutes; if (caloriesPerMinute >= 9) { - return { label: '高强度', color: '#F85959', background: '#FFE6E6' }; + return { label: t('workoutHistory.intensity.high'), color: '#F85959', background: '#FFE6E6' }; } if (caloriesPerMinute >= 5) { - return { label: '中强度', color: '#0EAF71', background: '#E4F6EF' }; + return { label: t('workoutHistory.intensity.medium'), color: '#0EAF71', background: '#E4F6EF' }; } - return { label: '低强度', color: '#5966FF', background: '#E7EBFF' }; + return { label: t('workoutHistory.intensity.low'), color: '#5966FF', background: '#E7EBFF' }; } function groupWorkouts(workouts: WorkoutData[]): WorkoutSection[] { @@ -265,13 +266,14 @@ function groupWorkouts(workouts: WorkoutData[]): WorkoutSection[] { return Object.keys(grouped) .sort((a, b) => dayjs(b).valueOf() - dayjs(a).valueOf()) .map((dateKey) => ({ - title: dayjs(dateKey).format('M月D日'), + title: dayjs(dateKey).format('M月D日'), // 保持中文格式,因为这是日期格式 data: grouped[dateKey] .sort((a, b) => dayjs(b.startDate || b.endDate).valueOf() - dayjs(a.startDate || a.endDate).valueOf()), })); } export default function WorkoutHistoryScreen() { + const { t } = useI18n(); const [sections, setSections] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -284,7 +286,7 @@ export default function WorkoutHistoryScreen() { const [monthOccurrenceText, setMonthOccurrenceText] = useState(null); const [monthlyStats, setMonthlyStats] = useState(null); - const safeAreaTop = useSafeAreaTop() + const safeAreaTop = useSafeAreaTop(); const loadHistory = useCallback(async () => { setIsLoading(true); @@ -302,7 +304,7 @@ export default function WorkoutHistoryScreen() { if (!hasPermission) { setSections([]); - setError('尚未授予健康数据权限'); + setError(t('workoutHistory.error.permissionDenied')); setMonthlyStats(null); return; } @@ -315,8 +317,8 @@ export default function WorkoutHistoryScreen() { setMonthlyStats(computeMonthlyStats(filteredWorkouts)); setSections(groupWorkouts(filteredWorkouts)); } catch (err) { - console.error('加载锻炼历史失败:', err); - setError('加载锻炼记录失败,请稍后再试'); + console.error('Failed to load workout history:', err); + setError(t('workoutHistory.error.loadFailed')); setSections([]); setMonthlyStats(null); } finally { @@ -350,9 +352,9 @@ export default function WorkoutHistoryScreen() { ? dayjs(monthlyStats.snapshotDate).format('M月D日') : dayjs().format('M月D日'); const overviewText = monthlyStats - ? `截至${snapshotLabel},你已完成${monthlyStats.totalCount}次锻炼,累计${formatDurationShort(monthlyStats.totalDuration)}。` - : '本月还没有锻炼记录,动起来收集第一条吧!'; - const periodText = `统计周期:1日 - ${monthEndDay}日(本月)`; + ? t('workoutHistory.monthlyStats.overviewWithStats', { date: snapshotLabel, count: monthlyStats.totalCount, duration: formatDurationShort(monthlyStats.totalDuration) }) + : t('workoutHistory.monthlyStats.overviewEmpty'); + const periodText = t('workoutHistory.monthlyStats.periodText', { day: monthEndDay }); const maxDuration = statsItems[0]?.duration || 1; return ( @@ -369,7 +371,7 @@ export default function WorkoutHistoryScreen() { end={{ x: 1, y: 1 }} style={styles.monthlyStatsCard} > - 锻炼时间 + {t('workoutHistory.monthlyStats.title')} {periodText} {overviewText} @@ -403,7 +405,7 @@ export default function WorkoutHistoryScreen() { ) : ( - 本月还没有锻炼数据 + {t('workoutHistory.monthlyStats.emptyData')} )} @@ -416,8 +418,8 @@ export default function WorkoutHistoryScreen() { const emptyComponent = useMemo(() => ( - 暂无锻炼记录 - 完成一次锻炼后即可在此查看详细历史 + {t('workoutHistory.empty.title')} + {t('workoutHistory.empty.subtitle')} ), []); @@ -453,7 +455,7 @@ export default function WorkoutHistoryScreen() { } const activityLabel = getWorkoutTypeDisplayName(workout.workoutActivityType); - return `这是你${workoutDate.format('M月')}的第 ${index + 1} 次${activityLabel}。`; + return t('workoutHistory.monthOccurrence', { month: workoutDate.format('M月'), index: index + 1, activity: activityLabel }); }, [sections]); const loadWorkoutDetail = useCallback(async (workout: WorkoutData) => { @@ -463,16 +465,16 @@ export default function WorkoutHistoryScreen() { const metrics = await getWorkoutDetailMetrics(workout); setDetailMetrics(metrics); } catch (err) { - console.error('加载锻炼详情失败:', err); + console.error('Failed to load workout details:', err); setDetailMetrics(null); - setDetailError('加载锻炼详情失败,请稍后再试'); + setDetailError(t('workoutHistory.error.detailLoadFailed')); } finally { setDetailLoading(false); } }, []); const handleWorkoutPress = useCallback((workout: WorkoutData) => { - const intensity = getIntensityBadge(workout.totalEnergyBurned, workout.duration || 0); + const intensity = getIntensityBadge(t, workout.totalEnergyBurned, workout.duration || 0); setSelectedIntensity(intensity); setSelectedWorkout(workout); setDetailMetrics(null); @@ -495,7 +497,7 @@ export default function WorkoutHistoryScreen() { const renderItem = useCallback(({ item }: { item: WorkoutData }) => { const calories = Math.round(item.totalEnergyBurned || 0); const minutes = Math.max(Math.round((item.duration || 0) / 60), 1); - const intensity = getIntensityBadge(item.totalEnergyBurned, item.duration || 0); + const intensity = getIntensityBadge(t, item.totalEnergyBurned, item.duration || 0); const iconName = ICON_MAP[item.workoutActivityType as WorkoutActivityType] || 'arm-flex'; const time = dayjs(item.startDate || item.endDate).format('HH:mm'); const activityLabel = getWorkoutTypeDisplayName(item.workoutActivityType); @@ -512,12 +514,12 @@ export default function WorkoutHistoryScreen() { - {calories}千卡 · {minutes}分钟 + {t('workoutHistory.historyCard.calories', { calories, minutes })} {intensity.label} - {activityLabel},{time} + {t('workoutHistory.historyCard.activityTime', { activity: activityLabel, time })} {/* */} @@ -535,11 +537,11 @@ export default function WorkoutHistoryScreen() { colors={["#F3F5FF", "#FFFFFF"]} style={StyleSheet.absoluteFill} /> - + {isLoading ? ( - 正在加载锻炼记录... + {t('workoutHistory.loading')} ) : ( {error} - 重试 + {t('workoutHistory.retry')} ) : emptyComponent} diff --git a/components/DateSelector.tsx b/components/DateSelector.tsx index e9ad549..413e62c 100644 --- a/components/DateSelector.tsx +++ b/components/DateSelector.tsx @@ -1,4 +1,5 @@ -import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date'; +import { useI18n } from '@/hooks/useI18n'; +import { getMonthDays, getMonthTitle, getTodayIndexInMonth } from '@/utils/date'; import { Ionicons } from '@expo/vector-icons'; import DateTimePicker from '@react-native-community/datetimepicker'; import dayjs from 'dayjs'; @@ -50,6 +51,8 @@ export const DateSelector: React.FC = ({ autoScrollToSelected = true, showCalendarIcon = true, }) => { + const { t, i18n } = useI18n(); + // 内部状态管理 const [internalSelectedIndex, setInternalSelectedIndex] = useState(getTodayIndexInMonth()); const [currentMonth, setCurrentMonth] = useState(dayjs()); // 当前显示的月份 @@ -59,8 +62,8 @@ export const DateSelector: React.FC = ({ const isGlassAvailable = isLiquidGlassAvailable(); // 获取日期数据 - const days = getMonthDaysZh(currentMonth); - const monthTitle = externalMonthTitle ?? getMonthTitleZh(currentMonth); + const days = getMonthDays(currentMonth, i18n.language as 'zh' | 'en'); + const monthTitle = externalMonthTitle ?? getMonthTitle(currentMonth, i18n.language as 'zh' | 'en'); // 判断当前选中的日期是否是今天 const isSelectedDateToday = () => { @@ -201,7 +204,7 @@ export const DateSelector: React.FC = ({ setCurrentMonth(selectedMonth); // 计算选中日期在新月份中的索引 - const newMonthDays = getMonthDaysZh(selectedMonth); + const newMonthDays = getMonthDays(selectedMonth, i18n.language as 'zh' | 'en'); const selectedDay = selectedMonth.date(); const newSelectedIndex = newMonthDays.findIndex(day => day.dayOfMonth === selectedDay); @@ -219,7 +222,7 @@ export const DateSelector: React.FC = ({ const handleGoToday = () => { const today = dayjs(); setCurrentMonth(today); - const todayDays = getMonthDaysZh(today); + const todayDays = getMonthDays(today, i18n.language as 'zh' | 'en'); const newSelectedIndex = todayDays.findIndex(day => day.dayOfMonth === today.date()); if (newSelectedIndex !== -1) { @@ -250,11 +253,11 @@ export const DateSelector: React.FC = ({ tintColor="rgba(124, 58, 237, 0.08)" isInteractive={true} > - 回到今天 + {t('dateSelector.backToToday')} ) : ( - 回到今天 + {t('dateSelector.backToToday')} )} @@ -379,7 +382,7 @@ export const DateSelector: React.FC = ({ display={Platform.OS === 'ios' ? 'inline' : 'calendar'} minimumDate={dayjs().subtract(6, 'month').toDate()} maximumDate={disableFutureDates ? new Date() : undefined} - {...(Platform.OS === 'ios' ? { locale: 'zh-CN' } : {})} + {...(Platform.OS === 'ios' ? { locale: i18n.language === 'zh' ? 'zh-CN' : 'en-US' } : {})} onChange={(event, date) => { if (Platform.OS === 'ios') { if (date) setPickerDate(date); @@ -395,12 +398,12 @@ export const DateSelector: React.FC = ({ {Platform.OS === 'ios' && ( - 取消 + {t('dateSelector.cancel')} { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}> - 确定 + {t('dateSelector.confirm')} )} @@ -413,7 +416,7 @@ export const DateSelector: React.FC = ({ display={Platform.OS === 'ios' ? 'inline' : 'calendar'} minimumDate={dayjs().subtract(6, 'month').toDate()} maximumDate={disableFutureDates ? new Date() : undefined} - {...(Platform.OS === 'ios' ? { locale: 'zh-CN' } : {})} + {...(Platform.OS === 'ios' ? { locale: i18n.language === 'zh' ? 'zh-CN' : 'en-US' } : {})} onChange={(event, date) => { if (Platform.OS === 'ios') { if (date) setPickerDate(date); @@ -429,12 +432,12 @@ export const DateSelector: React.FC = ({ {Platform.OS === 'ios' && ( - 取消 + {t('dateSelector.cancel')} { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}> - 确定 + {t('dateSelector.confirm')} )} diff --git a/components/challenges/ChallengeRankingItem.tsx b/components/challenges/ChallengeRankingItem.tsx index 1081789..ef545c3 100644 --- a/components/challenges/ChallengeRankingItem.tsx +++ b/components/challenges/ChallengeRankingItem.tsx @@ -1,3 +1,4 @@ +import { useI18n } from '@/hooks/useI18n'; import type { RankingItem } from '@/store/challengesSlice'; import { Ionicons } from '@expo/vector-icons'; import { Image } from 'expo-image'; @@ -18,34 +19,34 @@ const formatNumber = (value: number): string => { return value.toFixed(2).replace(/0+$/, '').replace(/\.$/, ''); }; -const formatMinutes = (value: number): string => { - const safeValue = Math.max(0, Math.round(value)); - const hours = safeValue / 60; - return `${hours.toFixed(1)} 小时`; -}; - -const formatValueWithUnit = (value: number | undefined, unit?: string): string | undefined => { - if (typeof value !== 'number' || Number.isNaN(value)) { - return undefined; - } - if (unit === 'min') { - return formatMinutes(value); - } - const formatted = formatNumber(value); - return unit ? `${formatted} ${unit}` : formatted; -}; - export function ChallengeRankingItem({ item, index, showDivider = false, unit }: ChallengeRankingItemProps) { - console.log('unit', unit); + const { t } = useI18n(); + + const formatMinutes = (value: number): string => { + const safeValue = Math.max(0, Math.round(value)); + const hours = safeValue / 60; + return `${hours.toFixed(1)} ${t('challengeDetail.ranking.hour')}`; + }; + + const formatValueWithUnit = (value: number | undefined, unit?: string): string | undefined => { + if (typeof value !== 'number' || Number.isNaN(value)) { + return undefined; + } + if (unit === 'min') { + return formatMinutes(value); + } + const formatted = formatNumber(value); + return unit ? `${formatted} ${unit}` : formatted; + }; const reportedLabel = formatValueWithUnit(item.todayReportedValue, unit); const targetLabel = formatValueWithUnit(item.todayTargetValue, unit); const progressLabel = reportedLabel && targetLabel - ? `今日 ${reportedLabel} / ${targetLabel}` + ? `${t('challengeDetail.ranking.today')} ${reportedLabel} / ${targetLabel}` : reportedLabel - ? `今日 ${reportedLabel}` + ? `${t('challengeDetail.ranking.today')} ${reportedLabel}` : targetLabel - ? `今日目标 ${targetLabel}` + ? `${t('challengeDetail.ranking.todayGoal')} ${targetLabel}` : undefined; return ( diff --git a/components/medications/MedicationPhotoGuideModal.tsx b/components/medications/MedicationPhotoGuideModal.tsx index 6c13bd9..c60d0b7 100644 --- a/components/medications/MedicationPhotoGuideModal.tsx +++ b/components/medications/MedicationPhotoGuideModal.tsx @@ -1,3 +1,4 @@ +import { useI18n } from '@/hooks/useI18n'; import { Ionicons } from '@expo/vector-icons'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; @@ -25,6 +26,8 @@ interface MedicationPhotoGuideModalProps { * 展示如何正确拍摄药品照片的说明和示例 */ export function MedicationPhotoGuideModal({ visible, onClose }: MedicationPhotoGuideModalProps) { + const { t } = useI18n(); + return ( {/* 标题部分 */} - 规范 - 拍摄图片清晰 + + {t('medications.aiCamera.guideModal.badge')} + + + {t('medications.aiCamera.guideModal.title')} + {/* 示例图片 */} @@ -99,10 +106,10 @@ export function MedicationPhotoGuideModal({ visible, onClose }: MedicationPhotoG {/* 说明文字 */} - 请拍摄药品正面\背面的产品名称\说明部分。 + {t('medications.aiCamera.guideModal.description1')} - 注意拍摄时光线充分,没有反光,文字部分清晰可见。照片的清晰度会影响识别的准确率。 + {t('medications.aiCamera.guideModal.description2')} @@ -124,7 +131,9 @@ export function MedicationPhotoGuideModal({ visible, onClose }: MedicationPhotoG end={{ x: 1, y: 0 }} style={styles.guideConfirmButtonGradient} > - 知道了! + + {t('medications.aiCamera.guideModal.button')} + ) : ( @@ -135,7 +144,9 @@ export function MedicationPhotoGuideModal({ visible, onClose }: MedicationPhotoG end={{ x: 1, y: 0 }} style={styles.guideConfirmButtonGradient} > - 知道了! + + {t('medications.aiCamera.guideModal.button')} + )} diff --git a/components/sleep/InfoModal.tsx b/components/sleep/InfoModal.tsx index 5005e3f..5c8e5d8 100644 --- a/components/sleep/InfoModal.tsx +++ b/components/sleep/InfoModal.tsx @@ -11,6 +11,7 @@ import { import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { useI18n } from '@/hooks/useI18n'; // 睡眠详情数据类型 export type SleepDetailData = { @@ -41,15 +42,22 @@ const SleepGradeCard = ({ range: string; isActive?: boolean; }) => { + const { t } = useI18n(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const getGradeColor = (grade: string) => { switch (grade) { - case '低': case '较差': return { bg: '#FECACA', text: '#DC2626' }; - case '正常': case '一般': return { bg: '#D1FAE5', text: '#065F46' }; - case '良好': return { bg: '#D1FAE5', text: '#065F46' }; - case '优秀': return { bg: '#FEF3C7', text: '#92400E' }; + case t('sleepDetail.sleepGrades.low'): + case t('sleepDetail.sleepGrades.poor'): + return { bg: '#FECACA', text: '#DC2626' }; + case t('sleepDetail.sleepGrades.normal'): + case t('sleepDetail.sleepGrades.fair'): + return { bg: '#D1FAE5', text: '#065F46' }; + case t('sleepDetail.sleepGrades.good'): + return { bg: '#D1FAE5', text: '#065F46' }; + case t('sleepDetail.sleepGrades.excellent'): + return { bg: '#FEF3C7', text: '#92400E' }; default: return { bg: colorTokens.pageBackgroundEmphasis, text: colorTokens.textSecondary }; } }; @@ -97,6 +105,7 @@ export const InfoModal = ({ type: 'sleep-time' | 'sleep-quality'; sleepData: SleepDetailData; }) => { + const { t } = useI18n(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const slideAnim = useState(new Animated.Value(0))[0]; @@ -153,26 +162,26 @@ export const InfoModal = ({ const currentSleepQualityGrade = getSleepQualityGrade(sleepData.sleepQualityPercentage || 94); // 默认94% const sleepTimeGrades = [ - { icon: 'alert-circle-outline', grade: '低', range: '< 6h', isActive: currentSleepTimeGrade === 0 }, - { icon: 'checkmark-circle-outline', grade: '正常', range: '6h - 7h or > 9h', isActive: currentSleepTimeGrade === 1 }, - { icon: 'checkmark-circle', grade: '良好', range: '7h - 8h', isActive: currentSleepTimeGrade === 2 }, - { icon: 'star', grade: '优秀', range: '8h - 9h', isActive: currentSleepTimeGrade === 3 }, + { icon: 'alert-circle-outline', grade: t('sleepDetail.sleepGrades.low'), range: '< 6h', isActive: currentSleepTimeGrade === 0 }, + { icon: 'checkmark-circle-outline', grade: t('sleepDetail.sleepGrades.normal'), range: '6h - 7h or > 9h', isActive: currentSleepTimeGrade === 1 }, + { icon: 'checkmark-circle', grade: t('sleepDetail.sleepGrades.good'), range: '7h - 8h', isActive: currentSleepTimeGrade === 2 }, + { icon: 'star', grade: t('sleepDetail.sleepGrades.excellent'), range: '8h - 9h', isActive: currentSleepTimeGrade === 3 }, ]; const sleepQualityGrades = [ - { icon: 'alert-circle-outline', grade: '较差', range: '< 55%', isActive: currentSleepQualityGrade === 0 }, - { icon: 'checkmark-circle-outline', grade: '一般', range: '55% - 69%', isActive: currentSleepQualityGrade === 1 }, - { icon: 'checkmark-circle', grade: '良好', range: '70% - 84%', isActive: currentSleepQualityGrade === 2 }, - { icon: 'star', grade: '优秀', range: '85% - 100%', isActive: currentSleepQualityGrade === 3 }, + { icon: 'alert-circle-outline', grade: t('sleepDetail.sleepGrades.poor'), range: '< 55%', isActive: currentSleepQualityGrade === 0 }, + { icon: 'checkmark-circle-outline', grade: t('sleepDetail.sleepGrades.fair'), range: '55% - 69%', isActive: currentSleepQualityGrade === 1 }, + { icon: 'checkmark-circle', grade: t('sleepDetail.sleepGrades.good'), range: '70% - 84%', isActive: currentSleepQualityGrade === 2 }, + { icon: 'star', grade: t('sleepDetail.sleepGrades.excellent'), range: '85% - 100%', isActive: currentSleepQualityGrade === 3 }, ]; const currentGrades = type === 'sleep-time' ? sleepTimeGrades : sleepQualityGrades; const getDescription = () => { if (type === 'sleep-time') { - return '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。'; + return t('sleepDetail.sleepTimeDescription'); } else { - return '睡眠质量综合评估您的睡眠效率、深度睡眠时长、REM睡眠比例等多个指标。高质量的睡眠不仅仅取决于时长,还包括睡眠的连续性和各睡眠阶段的平衡。'; + return t('sleepDetail.sleepQualityDescription'); } }; diff --git a/components/sleep/SleepStageTimeline.tsx b/components/sleep/SleepStageTimeline.tsx index 113e7cc..d2f178e 100644 --- a/components/sleep/SleepStageTimeline.tsx +++ b/components/sleep/SleepStageTimeline.tsx @@ -7,18 +7,24 @@ import React, { useMemo } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Svg, { Rect, Text as SvgText } from 'react-native-svg'; +import { StyleProp, ViewStyle } from 'react-native'; + export type SleepStageTimelineProps = { sleepSamples: SleepSample[]; bedtime: string; wakeupTime: string; onInfoPress?: () => void; + hideHeader?: boolean; + style?: StyleProp; }; export const SleepStageTimeline = ({ sleepSamples, bedtime, wakeupTime, - onInfoPress + onInfoPress, + hideHeader = false, + style }: SleepStageTimelineProps) => { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; @@ -130,15 +136,17 @@ export const SleepStageTimeline = ({ // 如果没有数据,显示空状态 if (timelineData.length === 0) { return ( - - - 睡眠阶段图 - {onInfoPress && ( - - - - )} - + + {!hideHeader && ( + + 睡眠阶段图 + {onInfoPress && ( + + + + )} + + )} 暂无睡眠阶段数据 @@ -149,16 +157,18 @@ export const SleepStageTimeline = ({ } return ( - + {/* 标题栏 */} - - 睡眠阶段图 - {onInfoPress && ( - - - - )} - + {!hideHeader && ( + + 睡眠阶段图 + {onInfoPress && ( + + + + )} + + )} {/* 睡眠时间范围 */} diff --git a/components/sleep/SleepStagesInfoModal.tsx b/components/sleep/SleepStagesInfoModal.tsx index 4e028c0..a89c3f5 100644 --- a/components/sleep/SleepStagesInfoModal.tsx +++ b/components/sleep/SleepStagesInfoModal.tsx @@ -13,6 +13,7 @@ import { import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { useI18n } from '@/hooks/useI18n'; // Sleep Stages Info Modal 组件 export const SleepStagesInfoModal = ({ @@ -22,6 +23,7 @@ export const SleepStagesInfoModal = ({ visible: boolean; onClose: () => void; }) => { + const { t } = useI18n(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const slideAnim = useState(new Animated.Value(0))[0]; @@ -82,7 +84,7 @@ export const SleepStagesInfoModal = ({ - 了解你的睡眠阶段 + {t('sleepDetail.sleepStagesInfo.title')} @@ -97,7 +99,7 @@ export const SleepStagesInfoModal = ({ scrollEnabled={true} > - 人们对睡眠阶段和睡眠质量有许多误解。有些人可能需要更多深度睡眠,其他人则不然。科学家和医生仍在探索不同睡眠阶段的作用及其对身体的影响。通过跟踪睡眠阶段并留意每天清晨的感受,你或许能深入了解自己的睡眠。 + {t('sleepDetail.sleepStagesInfo.description')} {/* 清醒时间 */} @@ -105,11 +107,11 @@ export const SleepStagesInfoModal = ({ - 清醒时间 + {t('sleepDetail.sleepStagesInfo.awake.title')} - 一次睡眠期间,你可能会醒来几次。偶尔醒来很正常。可能你会立刻再次入睡,并不记得曾在夜间醒来。 + {t('sleepDetail.sleepStagesInfo.awake.description')} @@ -118,11 +120,11 @@ export const SleepStagesInfoModal = ({ - 快速动眼睡眠 + {t('sleepDetail.sleepStagesInfo.rem.title')} - 这一睡眠阶段可能对学习和记忆产生一定影响。在此阶段,你的肌肉最为放松,眼球也会快速左右移动。这也是你大多数梦境出现的阶段。 + {t('sleepDetail.sleepStagesInfo.rem.description')} @@ -131,11 +133,11 @@ export const SleepStagesInfoModal = ({ - 核心睡眠 + {t('sleepDetail.sleepStagesInfo.core.title')} - 这一阶段有时也称为浅睡期,与其他阶段一样重要。此阶段通常占据你每晚大部分的睡眠时间。对于认知至关重要的脑电波会在这一阶段产生。 + {t('sleepDetail.sleepStagesInfo.core.description')} @@ -144,11 +146,11 @@ export const SleepStagesInfoModal = ({ - 深度睡眠 + {t('sleepDetail.sleepStagesInfo.deep.title')} - 因为脑电波的特征,这一阶段也称为慢波睡眠。在此阶段,身体组织得到修复,并释放重要荷尔蒙。它通常出现在睡眠的前半段,且持续时间较长。深度睡眠期间,身体非常放松,因此相较于其他阶段,你可能更难在此阶段醒来。 + {t('sleepDetail.sleepStagesInfo.deep.description')} diff --git a/components/weight/WeightRecordCard.tsx b/components/weight/WeightRecordCard.tsx index 4961cf7..fbfc2da 100644 --- a/components/weight/WeightRecordCard.tsx +++ b/components/weight/WeightRecordCard.tsx @@ -1,5 +1,6 @@ import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { useI18n } from '@/hooks/useI18n'; import { WeightHistoryItem } from '@/store/userSlice'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; @@ -20,6 +21,7 @@ export const WeightRecordCard: React.FC = ({ onDelete, weightChange = 0 }) => { + const { t } = useI18n(); const swipeableRef = useRef(null); const colorScheme = useColorScheme(); const themeColors = Colors[colorScheme ?? 'light']; @@ -27,15 +29,15 @@ export const WeightRecordCard: React.FC = ({ // 处理删除操作 const handleDelete = () => { Alert.alert( - '确认删除', - `确定要删除这条体重记录吗?此操作无法撤销。`, + t('weightRecords.card.deleteConfirmTitle'), + t('weightRecords.card.deleteConfirmMessage'), [ { - text: '取消', + text: t('weightRecords.card.cancelButton'), style: 'cancel', }, { - text: '删除', + text: t('weightRecords.card.deleteButton'), style: 'destructive', onPress: () => { const recordId = record.id || record.createdAt; @@ -56,7 +58,7 @@ export const WeightRecordCard: React.FC = ({ activeOpacity={0.8} > - 删除 + {t('weightRecords.card.deleteButton')} ); }; @@ -73,7 +75,7 @@ export const WeightRecordCard: React.FC = ({ > - {dayjs(record.createdAt).format('MM月DD日 HH:mm')} + {dayjs(record.createdAt).format('MM[月]DD[日] HH:mm')} = ({ - 体重: - {record.weight}kg + {t('weightRecords.card.weightLabel')}: + {record.weight}{t('weightRecords.modal.unit')} {Math.abs(weightChange) > 0 && ( - 正在加载锻炼详情... + {t('workoutDetail.loading')} ) : metrics ? ( <> - 体能训练时间 + {t('workoutDetail.metrics.duration')} {metrics.durationLabel} - 运动热量 + {t('workoutDetail.metrics.calories')} - {metrics.calories != null ? `${metrics.calories} 千卡` : '--'} + {metrics.calories != null ? `${metrics.calories} ${t('workoutDetail.metrics.caloriesUnit')}` : '--'} - 运动强度 + {t('workoutDetail.metrics.intensity')} setShowIntensityInfo(true)} style={styles.metricInfoButton} @@ -262,9 +264,9 @@ export function WorkoutDetailModal({ - 平均心率 + {t('workoutDetail.metrics.averageHeartRate')} - {metrics.averageHeartRate != null ? `${metrics.averageHeartRate} 次/分` : '--'} + {metrics.averageHeartRate != null ? `${metrics.averageHeartRate} ${t('workoutDetail.metrics.heartRateUnit')}` : '--'} @@ -275,11 +277,11 @@ export function WorkoutDetailModal({ ) : ( - {errorMessage || '未能获取到完整的锻炼详情'} + {errorMessage || t('workoutDetail.errors.loadFailed')} {onRetry ? ( - 重新加载 + {t('workoutDetail.retry')} ) : null} @@ -288,7 +290,7 @@ export function WorkoutDetailModal({ - 心率范围 + {t('workoutDetail.sections.heartRateRange')} {loading ? ( @@ -299,21 +301,21 @@ export function WorkoutDetailModal({ <> - 平均心率 + {t('workoutDetail.sections.averageHeartRate')} - {metrics.averageHeartRate != null ? `${metrics.averageHeartRate}次/分` : '--'} + {metrics.averageHeartRate != null ? `${metrics.averageHeartRate} ${t('workoutDetail.sections.heartRateUnit')}` : '--'} - 最高心率 + {t('workoutDetail.sections.maximumHeartRate')} - {metrics.maximumHeartRate != null ? `${metrics.maximumHeartRate}次/分` : '--'} + {metrics.maximumHeartRate != null ? `${metrics.maximumHeartRate} ${t('workoutDetail.sections.heartRateUnit')}` : '--'} - 最低心率 + {t('workoutDetail.sections.minimumHeartRate')} - {metrics.minimumHeartRate != null ? `${metrics.minimumHeartRate}次/分` : '--'} + {metrics.minimumHeartRate != null ? `${metrics.minimumHeartRate} ${t('workoutDetail.sections.heartRateUnit')}` : '--'} @@ -336,7 +338,7 @@ export function WorkoutDetailModal({ width={Dimensions.get('window').width - 72} height={220} fromZero={false} - yAxisSuffix="次/分" + yAxisSuffix={t('workoutDetail.sections.heartRateUnit')} withInnerLines={false} bezier chartConfig={{ @@ -360,20 +362,20 @@ export function WorkoutDetailModal({ ) : ( - 图表组件不可用,无法展示心率曲线 + {t('workoutDetail.chart.unavailable')} ) ) : ( - 暂无心率采样数据 + {t('workoutDetail.chart.noData')} )} ) : ( - {errorMessage || '未获取到心率数据'} + {errorMessage || t('workoutDetail.errors.noHeartRateData')} )} @@ -381,7 +383,7 @@ export function WorkoutDetailModal({ - 心率训练区间 + {t('workoutDetail.sections.heartRateZones')} {loading ? ( @@ -391,7 +393,7 @@ export function WorkoutDetailModal({ ) : metrics ? ( metrics.heartRateZones.map(renderHeartRateZone) ) : ( - 暂无区间统计 + {t('workoutDetail.errors.noZoneStats')} )} @@ -410,36 +412,36 @@ export function WorkoutDetailModal({ { }}> - 什么是运动强度? + {t('workoutDetail.intensityInfo.title')} - 运动强度是你完成一项任务所用的能量估算,是衡量锻炼和其他日常活动能耗强度的指标,单位为 MET(千卡/(千克·小时))。 + {t('workoutDetail.intensityInfo.description1')} - 因为每个人的代谢状况不同,MET 以身体的静息能耗作为参考,便于衡量不同活动的强度。 + {t('workoutDetail.intensityInfo.description2')} - 例如:散步(约 3 km/h)相当于 2 METs,意味着它需要消耗静息状态 2 倍的能量。 + {t('workoutDetail.intensityInfo.description3')} - 注:当设备未提供 METs 值时,系统会根据您的卡路里消耗和锻炼时长自动计算(使用70公斤估算体重)。 + {t('workoutDetail.intensityInfo.description4')} - 运动强度计算公式 - METs = 活动能耗(千卡/小时) ÷ 静息能耗(1 千卡/小时) + {t('workoutDetail.intensityInfo.formula.title')} + {t('workoutDetail.intensityInfo.formula.value')} - {'< 3'} - 低强度活动 + {t('workoutDetail.intensityInfo.legend.low')} + {t('workoutDetail.intensityInfo.legend.lowLabel')} - 3 - 6 - 中强度活动 + {t('workoutDetail.intensityInfo.legend.medium')} + {t('workoutDetail.intensityInfo.legend.mediumLabel')} - {'≥ 6'} - 高强度活动 + {t('workoutDetail.intensityInfo.legend.high')} + {t('workoutDetail.intensityInfo.legend.highLabel')} diff --git a/hooks/useAuthGuard.ts b/hooks/useAuthGuard.ts index f8e603d..fdfb46d 100644 --- a/hooks/useAuthGuard.ts +++ b/hooks/useAuthGuard.ts @@ -5,6 +5,7 @@ import { Alert } from 'react-native'; import { ROUTES } from '@/constants/Routes'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { useI18n } from '@/hooks/useI18n'; import { STORAGE_KEYS, api } from '@/services/api'; import { logout as logoutAction } from '@/store/userSlice'; @@ -21,6 +22,7 @@ export function useAuthGuard() { const dispatch = useAppDispatch(); const currentPath = usePathname(); const user = useAppSelector(state => state.user); + const { t } = useI18n(); // 判断登录状态:优先使用 token,因为 token 是登录的根本凭证 // profile.id 可能在初始化时还未加载,但 token 已经从 AsyncStorage 恢复 @@ -74,28 +76,28 @@ export function useAuthGuard() { router.push('/auth/login'); } catch (error) { console.error('退出登录失败:', error); - Alert.alert('错误', '退出登录失败,请稍后重试'); + Alert.alert(t('authGuard.logout.error'), t('authGuard.logout.errorMessage')); } - }, [dispatch, router]); + }, [dispatch, router, t]); // 带确认对话框的退出登录 const confirmLogout = useCallback(() => { Alert.alert( - '确认退出', - '确定要退出当前账号吗?', + t('authGuard.confirmLogout.title'), + t('authGuard.confirmLogout.message'), [ { - text: '取消', + text: t('authGuard.confirmLogout.cancelButton'), style: 'cancel', }, { - text: '确定', + text: t('authGuard.confirmLogout.confirmButton'), style: 'default', onPress: handleLogout, }, ] ); - }, [handleLogout]); + }, [handleLogout, t]); // 注销账号功能 const handleDeleteAccount = useCallback(async () => { @@ -109,38 +111,38 @@ export function useAuthGuard() { // 执行退出登录逻辑 await dispatch(logoutAction()).unwrap(); - Alert.alert('账号已注销', '您的账号已成功注销', [ + Alert.alert(t('authGuard.deleteAccount.successTitle'), t('authGuard.deleteAccount.successMessage'), [ { - text: '确定', + text: t('authGuard.deleteAccount.confirmButton'), onPress: () => router.push('/auth/login'), }, ]); } catch (error: any) { console.error('注销账号失败:', error); - const message = error?.message || '注销失败,请稍后重试'; - Alert.alert('注销失败', message); + const message = error?.message || t('authGuard.deleteAccount.errorMessage'); + Alert.alert(t('authGuard.deleteAccount.errorTitle'), message); } - }, [dispatch, router]); + }, [dispatch, router, t]); // 带确认对话框的注销账号 const confirmDeleteAccount = useCallback(() => { Alert.alert( - '确认注销账号', - '此操作不可恢复,将删除您的账号及相关数据。确定继续吗?', + t('authGuard.confirmDeleteAccount.title'), + t('authGuard.confirmDeleteAccount.message'), [ { - text: '取消', + text: t('authGuard.confirmDeleteAccount.cancelButton'), style: 'cancel', }, { - text: '确认注销', + text: t('authGuard.confirmDeleteAccount.confirmButton'), style: 'destructive', onPress: handleDeleteAccount, }, ], { cancelable: true } ); - }, [handleDeleteAccount]); + }, [handleDeleteAccount, t]); return { isLoggedIn, diff --git a/i18n/index.ts b/i18n/index.ts index 0eddb28..1ec6fa7 100644 --- a/i18n/index.ts +++ b/i18n/index.ts @@ -402,6 +402,38 @@ const statisticsResources = { calf: '小腿围', }, }, + circumferenceDetail: { + title: '围度统计', + loading: '加载中...', + error: '加载失败', + retry: '重试', + noData: '暂无数据', + noDataSelected: '请选择要显示的围度数据', + tabs: { + week: '按周', + month: '按月', + year: '按年', + }, + measurements: { + chest: '胸围', + waist: '腰围', + upperHip: '上臀围', + arm: '臂围', + thigh: '大腿围', + calf: '小腿围', + }, + modal: { + title: '设置{{label}}', + defaultTitle: '设置围度', + confirm: '确认', + }, + chart: { + weekLabel: '第{{week}}周', + monthLabel: '{{month}}月', + empty: '暂无数据', + noSelection: '请选择要显示的围度数据', + }, + }, workout: { title: '近期锻炼', minutes: '分钟', @@ -849,6 +881,62 @@ const medicationsResources = { confirm: '确定', }, }, + aiCamera: { + title: 'AI 用药识别', + steps: { + front: { + title: '正面', + subtitle: '保证药品名称清晰可见', + }, + side: { + title: '背面', + subtitle: '包含规格、成分等信息', + }, + aux: { + title: '侧面', + subtitle: '补充更多细节提升准确率', + }, + stepProgress: '步骤 {{current}} / {{total}}', + optional: '(可选)', + notTaken: '未拍摄', + }, + buttons: { + flip: '翻转', + capture: '拍照', + complete: '完成', + album: '从相册', + }, + permission: { + title: '需要相机权限', + description: '授权后即可快速拍摄药品包装,自动识别信息', + button: '授权访问相机', + }, + alerts: { + pickFailed: { + title: '选择失败', + message: '请重试或更换图片', + }, + captureFailed: { + title: '拍摄失败', + message: '请重试', + }, + insufficientPhotos: { + title: '照片不足', + message: '请至少完成正面和背面拍摄', + }, + taskFailed: { + title: '创建任务失败', + defaultMessage: '请检查网络后重试', + }, + }, + guideModal: { + badge: '规范', + title: '拍摄图片清晰', + description1: '请拍摄药品正面\\背面的产品名称\\说明部分。', + description2: '注意拍摄时光线充分,没有反光,文字部分清晰可见。照片的清晰度会影响识别的准确率。', + button: '知道了!', + }, + }, }; const challengeDetailResources = { @@ -950,6 +1038,18 @@ const challengeDetailResources = { title: '排行榜', description: '', empty: '榜单即将开启,快来抢占席位。', + today: '今日', + todayGoal: '今日目标', + hour: '小时', + }, + leaderboard: { + title: '排行榜', + loading: '加载榜单中…', + notFound: '未找到该挑战。', + loadFailed: '暂时无法加载榜单,请稍后再试。', + empty: '榜单即将开启,快来抢占席位。', + loadMore: '加载更多…', + loadMoreFailed: '加载更多失败,请下拉刷新重试', }, shareCard: { footer: 'Out Live · 超越生命', @@ -1068,6 +1168,18 @@ const challengeDetailResourcesEn = { title: 'Leaderboard', description: '', empty: 'Leaderboard opening soon, grab your spot.', + today: 'Today', + todayGoal: 'Today\'s Goal', + hour: 'hrs', + }, + leaderboard: { + title: 'Leaderboard', + loading: 'Loading leaderboard…', + notFound: 'Challenge not found.', + loadFailed: 'Unable to load leaderboard, please try again later.', + empty: 'Leaderboard opening soon, grab your spot.', + loadMore: 'Loading more…', + loadMoreFailed: 'Failed to load more, pull to refresh and retry', }, shareCard: { footer: 'Out Live · Beyond Life', @@ -1152,6 +1264,791 @@ const notificationSettingsResources = { }, }; +const sleepDetailResources = { + title: '睡眠详情', + loading: '加载睡眠数据中...', + today: '今天', + sleepScore: '睡眠评分', + noData: '暂无睡眠数据', + noDataRecommendation: '请确保在真实iOS设备上运行并授权访问健康数据,或等待有睡眠数据后再查看。', + sleepDuration: '睡眠时长', + sleepQuality: '睡眠质量', + sleepStages: '睡眠阶段', + learnMore: '了解更多', + awake: '清醒', + rem: '快速眼动', + core: '核心睡眠', + deep: '深度睡眠', + unknown: '未知', + rawData: '原始数据', + rawDataDescription: '包含 {{count}} 条 HealthKit 睡眠样本记录', + infoModalTitles: { + sleepTime: '睡眠时间', + sleepQuality: '睡眠质量', + }, + sleepGrades: { + low: '低', + normal: '正常', + good: '良好', + excellent: '优秀', + poor: '较差', + fair: '一般', + }, + sleepTimeDescription: '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。', + sleepQualityDescription: '睡眠质量综合评估您的睡眠效率、深度睡眠时长、REM睡眠比例等多个指标。高质量的睡眠不仅仅取决于时长,还包括睡眠的连续性和各睡眠阶段的平衡。', + sleepStagesInfo: { + title: '了解你的睡眠阶段', + description: '人们对睡眠阶段和睡眠质量有许多误解。有些人可能需要更多深度睡眠,其他人则不然。科学家和医生仍在探索不同睡眠阶段的作用及其对身体的影响。通过跟踪睡眠阶段并留意每天清晨的感受,你或许能深入了解自己的睡眠。', + awake: { + title: '清醒时间', + description: '一次睡眠期间,你可能会醒来几次。偶尔醒来很正常。可能你会立刻再次入睡,并不记得曾在夜间醒来。', + }, + rem: { + title: '快速动眼睡眠', + description: '这一睡眠阶段可能对学习和记忆产生一定影响。在此阶段,你的肌肉最为放松,眼球也会快速左右移动。这也是你大多数梦境出现的阶段。', + }, + core: { + title: '核心睡眠', + description: '这一阶段有时也称为浅睡期,与其他阶段一样重要。此阶段通常占据你每晚大部分的睡眠时间。对于认知至关重要的脑电波会在这一阶段产生。', + }, + deep: { + title: '深度睡眠', + description: '因为脑电波的特征,这一阶段也称为慢波睡眠。在此阶段,身体组织得到修复,并释放重要荷尔蒙。它通常出现在睡眠的前半段,且持续时间较长。深度睡眠期间,身体非常放松,因此相较于其他阶段,你可能更难在此阶段醒来。', + }, + }, +}; + +const sleepDetailResourcesEn = { + title: 'Sleep Details', + loading: 'Loading sleep data...', + today: 'Today', + sleepScore: 'Sleep Score', + noData: 'No sleep data available', + noDataRecommendation: 'Please ensure you are running on a real iOS device with authorized health data access, or wait until you have sleep data to view.', + sleepDuration: 'Sleep Duration', + sleepQuality: 'Sleep Quality', + sleepStages: 'Sleep Stages', + learnMore: 'Learn More', + awake: 'Awake', + rem: 'REM', + core: 'Core Sleep', + deep: 'Deep Sleep', + unknown: 'Unknown', + rawData: 'Raw Data', + rawDataDescription: 'Contains {{count}} HealthKit sleep sample records', + infoModalTitles: { + sleepTime: 'Sleep Time', + sleepQuality: 'Sleep Quality', + }, + sleepGrades: { + low: 'Low', + normal: 'Normal', + good: 'Good', + excellent: 'Excellent', + poor: 'Poor', + fair: 'Fair', + }, + sleepTimeDescription: 'Sleep is most important - it accounts for more than half of your sleep score. Longer sleep can reduce sleep debt, but regular sleep times are crucial for quality rest.', + sleepQualityDescription: 'Sleep quality comprehensively evaluates multiple indicators such as your sleep efficiency, deep sleep duration, REM sleep ratio, etc. High-quality sleep depends not only on duration but also on sleep continuity and balance of sleep stages.', + sleepStagesInfo: { + title: 'Understand Your Sleep Stages', + description: 'People have many misconceptions about sleep stages and sleep quality. Some people may need more deep sleep, while others may not. Scientists and doctors are still exploring the role of different sleep stages and their effects on the body. By tracking sleep stages and paying attention to how you feel each morning, you may gain deeper insights into your own sleep.', + awake: { + title: 'Awake Time', + description: 'During a sleep period, you may wake up several times. Occasional waking is normal. You may fall back asleep immediately and not remember waking up during the night.', + }, + rem: { + title: 'REM Sleep', + description: 'This sleep stage may have some impact on learning and memory. During this stage, your muscles are most relaxed and your eyes move rapidly left and right. This is also the stage where most of your dreams occur.', + }, + core: { + title: 'Core Sleep', + description: 'This stage is sometimes called light sleep and is as important as other stages. This stage usually occupies most of your sleep time each night. Brain waves that are crucial for cognition are generated during this stage.', + }, + deep: { + title: 'Deep Sleep', + description: 'Due to the characteristics of brain waves, this stage is also called slow-wave sleep. During this stage, body tissues are repaired and important hormones are released. It usually occurs in the first half of sleep and lasts longer. During deep sleep, the body is very relaxed, so you may find it harder to wake up during this stage compared to other stages.', + }, + }, +}; + +const stepsDetailResources = { + title: '步数详情', + loading: '加载中...', + stats: { + totalSteps: '总步数', + averagePerHour: '平均每小时', + mostActiveTime: '最活跃时段', + }, + chart: { + title: '每小时步数分布', + averageLabel: '平均 {{steps}}步', + }, + activityLevel: { + currentActivity: '你今天的活动量处于', + levels: { + inactive: '不怎么动', + light: '轻度活跃', + moderate: '中等活跃', + very_active: '非常活跃', + }, + progress: { + current: '当前', + nextLevel: '下一级: {{level}}', + highestLevel: '已达最高级', + }, + }, + timeLabels: { + midnight: '0:00', + noon: '12:00', + nextDay: '24:00', + }, +}; + +const stepsDetailResourcesEn = { + title: 'Steps Details', + loading: 'Loading...', + stats: { + totalSteps: 'Total Steps', + averagePerHour: 'Average Per Hour', + mostActiveTime: 'Most Active Time', + }, + chart: { + title: 'Hourly Steps Distribution', + averageLabel: 'Average {{steps}} steps', + }, + activityLevel: { + currentActivity: 'Your activity level today is', + levels: { + inactive: 'Inactive', + light: 'Lightly Active', + moderate: 'Moderately Active', + very_active: 'Very Active', + }, + progress: { + current: 'Current', + nextLevel: 'Next: {{level}}', + highestLevel: 'Highest Level', + }, + }, + timeLabels: { + midnight: '0:00', + noon: '12:00', + nextDay: '24:00', + }, +}; + +const fitnessRingsDetailResources = { + title: '健身圆环详情', + loading: '加载中...', + weekDays: { + monday: '周一', + tuesday: '周二', + wednesday: '周三', + thursday: '周四', + friday: '周五', + saturday: '周六', + sunday: '周日', + }, + cards: { + activeCalories: { + title: '活动热量', + unit: '千卡', + }, + exerciseMinutes: { + title: '锻炼分钟数', + unit: '分钟', + info: { + title: '锻炼分钟数:', + description: '进行强度不低于"快走"的运动锻炼,就会积累对应时长的锻炼分钟数。', + recommendation: '世卫组织推荐的成年人每天至少保持30分钟以上的中高强度运动。', + knowButton: '知道了', + }, + }, + standHours: { + title: '活动小时数', + unit: '小时', + }, + }, + stats: { + weeklyClosedRings: '周闭环天数', + daysUnit: '天', + }, + datePicker: { + cancel: '取消', + confirm: '确定', + }, + errors: { + loadExerciseInfoPreference: '加载锻炼分钟说明偏好失败', + saveExerciseInfoPreference: '保存锻炼分钟说明偏好失败', + }, +}; + +const fitnessRingsDetailResourcesEn = { + title: 'Fitness Rings Detail', + loading: 'Loading...', + weekDays: { + monday: 'Mon', + tuesday: 'Tue', + wednesday: 'Wed', + thursday: 'Thu', + friday: 'Fri', + saturday: 'Sat', + sunday: 'Sun', + }, + cards: { + activeCalories: { + title: 'Active Calories', + unit: 'kcal', + }, + exerciseMinutes: { + title: 'Exercise Minutes', + unit: 'minutes', + info: { + title: 'Exercise Minutes:', + description: 'Exercise at an intensity of at least "brisk walking" will accumulate corresponding exercise minutes.', + recommendation: 'WHO recommends adults to maintain at least 30 minutes of moderate to high-intensity exercise daily.', + knowButton: 'Got it', + }, + }, + standHours: { + title: 'Stand Hours', + unit: 'hours', + }, + }, + stats: { + weeklyClosedRings: 'Weekly Closed Rings', + daysUnit: 'days', + }, + datePicker: { + cancel: 'Cancel', + confirm: 'Confirm', + }, + errors: { + loadExerciseInfoPreference: 'Failed to load exercise minutes info preference', + saveExerciseInfoPreference: 'Failed to save exercise minutes info preference', + }, +}; + +const waterDetailResources = { + title: '饮水详情', + waterRecord: '饮水记录', + today: '今日', + total: '总计:', + goal: '目标:', + noRecords: '暂无饮水记录', + noRecordsSubtitle: '点击"添加记录"开始记录饮水量', + deleteConfirm: { + title: '确认删除', + message: '确定要删除这条饮水记录吗?此操作无法撤销。', + cancel: '取消', + confirm: '删除', + }, + deleteButton: '删除', + water: '水', + loadingUserPreferences: '加载用户偏好设置失败', +}; + +const basalMetabolismDetailResources = { + title: '基础代谢', + currentData: { + title: '{{date}} 基础代谢', + unit: '千卡', + normalRange: '正常范围: {{min}}-{{max}} 千卡', + noData: '--', + }, + stats: { + title: '基础代谢统计', + tabs: { + week: '按周', + month: '按月', + }, + }, + chart: { + loading: '加载中...', + loadingText: '加载中...', + error: { + text: '加载失败: {{error}}', + retry: '重试', + fetchFailed: '获取数据失败', + }, + empty: '暂无数据', + yAxisSuffix: '千卡', + weekLabel: '第{{week}}周', + }, + modal: { + title: '基础代谢', + closeButton: '×', + description: '基础代谢,也称基础代谢率(BMR),是指人体在完全静息状态下维持基本生命功能(心跳、呼吸、体温调节等)所需的最低能量消耗,通常以卡路里为单位。', + sections: { + importance: { + title: '为什么重要?', + content: '基础代谢占总能量消耗的60-75%,是能量平衡的基础。了解您的基础代谢有助于制定科学的营养计划、优化体重管理策略,以及评估代谢健康状态。', + }, + normalRange: { + title: '正常范围', + formulas: { + male: '男性:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 + 5', + female: '女性:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 - 161', + }, + userRange: '您的正常区间:{{min}}-{{max}}千卡/天', + rangeNote: '(在公式基础计算值上下浮动15%都属于正常范围)', + userInfo: '基于您的信息:{{gender}},{{age}}岁,{{height}}cm,{{weight}}kg', + incompleteInfo: '请完善基本信息以计算您的代谢率', + }, + strategies: { + title: '提高代谢率的策略', + subtitle: '科学研究支持以下方法:', + items: [ + '1.增加肌肉量 (每周2-3次力量训练)', + '2.高强度间歇训练 (HIIT)', + '3.充分蛋白质摄入 (体重每公斤1.6-2.2g)', + '4.保证充足睡眠 (7-9小时/晚)', + '5.避免过度热量限制 (不低于BMR的80%)', + ], + }, + }, + }, + gender: { + male: '男性', + female: '女性', + }, + comments: { + reloadData: '重新加载数据', + }, + dateSelector: { + backToToday: '回到今天', + cancel: '取消', + confirm: '确定', + }, +}; + +const waterDetailResourcesEn = { + title: 'Water Details', + waterRecord: 'Water Records', + today: 'Today', + total: 'Total: ', + goal: 'Goal: ', + noRecords: 'No water records', + noRecordsSubtitle: 'Tap "Add Record" to start tracking water intake', + deleteConfirm: { + title: 'Confirm Delete', + message: 'Are you sure you want to delete this water record? This action cannot be undone.', + cancel: 'Cancel', + confirm: 'Delete', + }, + deleteButton: 'Delete', + water: 'Water', + loadingUserPreferences: 'Failed to load user preferences', +}; + +const waterReminderSettingsResources = { + title: '喝水提醒', + sections: { + notifications: '推送提醒', + timeRange: '提醒时间段', + interval: '提醒间隔', + }, + descriptions: { + notifications: '开启后将在指定时间段内定期推送喝水提醒', + timeRange: '只在指定时间段内发送提醒,避免打扰您的休息', + interval: '选择提醒的频率,建议30-120分钟为宜', + }, + labels: { + startTime: '开始时间', + endTime: '结束时间', + interval: '提醒间隔', + saveSettings: '保存设置', + hours: '小时', + timeRangePreview: '时间段预览', + minutes: '分钟', + }, + alerts: { + timeValidation: { + title: '时间设置提示', + startTimeInvalid: '开始时间不能晚于或等于结束时间,请重新选择', + endTimeInvalid: '结束时间不能早于或等于开始时间,请重新选择', + }, + success: { + enabled: '设置成功', + enabledMessage: '喝水提醒已开启\n\n时间段:{{timeRange}}\n提醒间隔:{{interval}}\n\n我们将在指定时间段内定期提醒您喝水', + disabled: '设置成功', + disabledMessage: '喝水提醒已关闭', + }, + error: { + title: '保存失败', + message: '无法保存喝水提醒设置,请重试', + }, + }, + buttons: { + confirm: '确定', + cancel: '取消', + }, +}; + +const waterReminderSettingsResourcesEn = { + title: 'Water Reminder', + sections: { + notifications: 'Push Notifications', + timeRange: 'Reminder Time Range', + interval: 'Reminder Interval', + }, + descriptions: { + notifications: 'Enable to receive periodic water reminders during specified time periods', + timeRange: 'Only send reminders during specified time periods to avoid disturbing your rest', + interval: 'Choose the reminder frequency, recommended 30-120 minutes', + }, + labels: { + startTime: 'Start Time', + endTime: 'End Time', + interval: 'Reminder Interval', + saveSettings: 'Save Settings', + hours: 'Hours', + timeRangePreview: 'Time Range Preview', + minutes: 'Minutes', + }, + alerts: { + timeValidation: { + title: 'Time Setting Tip', + startTimeInvalid: 'Start time cannot be later than or equal to end time, please select again', + endTimeInvalid: 'End time cannot be earlier than or equal to start time, please select again', + }, + success: { + enabled: 'Settings Saved', + enabledMessage: 'Water reminder has been enabled\n\nTime range: {{timeRange}}\nReminder interval: {{interval}}\n\nWe will periodically remind you to drink water during the specified time period', + disabled: 'Settings Saved', + disabledMessage: 'Water reminder has been disabled', + }, + error: { + title: 'Save Failed', + message: 'Unable to save water reminder settings, please try again', + }, + }, + buttons: { + confirm: 'Confirm', + cancel: 'Cancel', + }, +}; + +const waterSettingsResources = { + title: '饮水设置', + sections: { + dailyGoal: '每日饮水目标', + quickAdd: '快速添加默认值', + reminder: '喝水提醒', + }, + descriptions: { + quickAdd: '设置点击"+"按钮时添加的默认饮水量', + reminder: '设置定时提醒您补充水分', + }, + labels: { + ml: 'ml', + disabled: '已关闭', + }, + alerts: { + goalSuccess: { + title: '设置成功', + message: '每日饮水目标已设置为 {{amount}}ml', + }, + quickAddSuccess: { + title: '设置成功', + message: '快速添加默认值已设置为 {{amount}}ml', + }, + quickAddFailed: { + title: '设置失败', + message: '无法保存快速添加默认值,请重试', + }, + }, + buttons: { + cancel: '取消', + confirm: '确定', + }, + status: { + reminderEnabled: '{{startTime}}-{{endTime}}, 每{{interval}}分钟', + }, +}; + +const waterSettingsResourcesEn = { + title: 'Water Settings', + sections: { + dailyGoal: 'Daily Water Goal', + quickAdd: 'Quick Add Default', + reminder: 'Water Reminder', + }, + descriptions: { + quickAdd: 'Set the default water amount when clicking the "+" button', + reminder: 'Set periodic reminders to replenish water', + }, + labels: { + ml: 'ml', + disabled: 'Disabled', + }, + alerts: { + goalSuccess: { + title: 'Settings Saved', + message: 'Daily water goal has been set to {{amount}}ml', + }, + quickAddSuccess: { + title: 'Settings Saved', + message: 'Quick add default has been set to {{amount}}ml', + }, + quickAddFailed: { + title: 'Save Failed', + message: 'Unable to save quick add default, please try again', + }, + }, + buttons: { + cancel: 'Cancel', + confirm: 'Confirm', + }, + status: { + reminderEnabled: '{{startTime}}-{{endTime}}, every {{interval}} minutes', + }, +}; + +const basalMetabolismDetailResourcesEn = { + title: 'Basal Metabolism', + currentData: { + title: '{{date}} Basal Metabolism', + unit: 'kcal', + normalRange: 'Normal range: {{min}}-{{max}} kcal', + noData: '--', + }, + stats: { + title: 'Basal Metabolism Statistics', + tabs: { + week: 'By Week', + month: 'By Month', + }, + }, + chart: { + loading: 'Loading...', + loadingText: 'Loading...', + error: { + text: 'Loading failed: {{error}}', + retry: 'Retry', + fetchFailed: 'Failed to fetch data', + }, + empty: 'No data available', + yAxisSuffix: 'kcal', + weekLabel: 'Week {{week}}', + }, + modal: { + title: 'Basal Metabolism', + closeButton: '×', + description: 'Basal metabolism, also known as Basal Metabolic Rate (BMR), refers to the minimum energy consumption required for the human body to maintain basic life functions (heartbeat, breathing, body temperature regulation, etc.) in a completely resting state, usually measured in calories.', + sections: { + importance: { + title: 'Why is it important?', + content: 'Basal metabolism accounts for 60-75% of total energy consumption and is the foundation of energy balance. Understanding your basal metabolism helps develop scientific nutrition plans, optimize weight management strategies, and assess metabolic health status.', + }, + normalRange: { + title: 'Normal Range', + formulas: { + male: 'Male: BMR = 10 × weight(kg) + 6.25 × height(cm) - 5 × age + 5', + female: 'Female: BMR = 10 × weight(kg) + 6.25 × height(cm) - 5 × age - 161', + }, + userRange: 'Your normal range: {{min}}-{{max}} kcal/day', + rangeNote: '(Within 15% above or below the calculated value is considered normal)', + userInfo: 'Based on your information: {{gender}}, {{age}} years old, {{height}}cm, {{weight}}kg', + incompleteInfo: 'Please complete basic information to calculate your metabolic rate', + }, + strategies: { + title: 'Strategies to Boost Metabolism', + subtitle: 'Scientific research supports the following methods:', + items: [ + '1. Increase muscle mass (2-3 strength training sessions per week)', + '2. High-intensity interval training (HIIT)', + '3. Adequate protein intake (1.6-2.2g per kg of body weight)', + '4. Ensure adequate sleep (7-9 hours per night)', + '5. Avoid excessive calorie restriction (not less than 80% of BMR)', + ], + }, + }, + }, + gender: { + male: 'Male', + female: 'Female', + }, + comments: { + reloadData: 'Reload data', + }, + dateSelector: { + backToToday: 'Back to Today', + cancel: 'Cancel', + confirm: 'Confirm', + }, +}; + +const workoutHistoryResources = { + title: '锻炼总结', + loading: '正在加载锻炼记录...', + error: { + permissionDenied: '尚未授予健康数据权限', + loadFailed: '加载锻炼记录失败,请稍后再试', + detailLoadFailed: '加载锻炼详情失败,请稍后再试', + }, + retry: '重试', + monthlyStats: { + title: '锻炼时间', + periodText: '统计周期:1日 - {{day}}日(本月)', + overviewWithStats: '截至{{date}},你已完成{{count}}次锻炼,累计{{duration}}。', + overviewEmpty: '本月还没有锻炼记录,动起来收集第一条吧!', + emptyData: '本月还没有锻炼数据', + }, + intensity: { + low: '低强度', + medium: '中强度', + high: '高强度', + }, + historyCard: { + calories: '{{calories}}千卡 · {{minutes}}分钟', + activityTime: '{{activity}},{{time}}', + }, + empty: { + title: '暂无锻炼记录', + subtitle: '完成一次锻炼后即可在此查看详细历史', + }, + monthOccurrence: '这是你{{month}}的第 {{index}} 次{{activity}}。', +}; + +const workoutHistoryResourcesEn = { + title: 'Workout Summary', + loading: 'Loading workout records...', + error: { + permissionDenied: 'Health data permission not granted', + loadFailed: 'Failed to load workout records, please try again later', + detailLoadFailed: 'Failed to load workout details, please try again later', + }, + retry: 'Retry', + monthlyStats: { + title: 'Workout Time', + periodText: 'Statistics period: 1st - {{day}} (This month)', + overviewWithStats: 'As of {{date}}, you have completed {{count}} workouts, totaling {{duration}}.', + overviewEmpty: 'No workout records this month yet, start moving to collect your first one!', + emptyData: 'No workout data this month', + }, + intensity: { + low: 'Low Intensity', + medium: 'Medium Intensity', + high: 'High Intensity', + }, + historyCard: { + calories: '{{calories}} kcal · {{minutes}} min', + activityTime: '{{activity}}, {{time}}', + }, + empty: { + title: 'No Workout Records', + subtitle: 'Complete a workout to view detailed history here', + }, + monthOccurrence: 'This is your {{index}} {{activity}} in {{month}}.', +}; +const loginScreenResources = { + title: '登录', + subtitle: '健康生活,自律让我更自由', + appleLogin: '使用 Apple 登录', + loggingIn: '登录中...', + agreement: { + readAndAgree: '我已阅读并同意', + privacyPolicy: '《隐私政策》', + and: '和', + userAgreement: '《用户协议》', + alert: { + title: '请先阅读并同意', + message: '继续登录前,请阅读并勾选《隐私政策》和《用户协议》。点击“同意并继续”将默认勾选并继续登录。', + cancel: '取消', + confirm: '同意并继续', + }, + }, + errors: { + appleIdentityTokenMissing: '未获取到 Apple 身份令牌', + loginFailed: '登录失败,请稍后再试', + loginFailedTitle: '登录失败', + }, + success: { + loginSuccess: '登录成功', + }, +}; + +const loginScreenResourcesEn = { + title: 'Log In', + subtitle: 'Healthy living, freedom through self-discipline', + appleLogin: 'Sign in with Apple', + loggingIn: 'Logging in...', + agreement: { + readAndAgree: 'I have read and agree to ', + privacyPolicy: 'Privacy Policy', + and: ' and ', + userAgreement: 'User Agreement', + alert: { + title: 'Please read and agree', + message: 'Please read and check the "Privacy Policy" and "User Agreement" before continuing. Clicking "Agree and Continue" will automatically check the box and proceed.', + cancel: 'Cancel', + confirm: 'Agree and Continue', + }, + }, + errors: { + appleIdentityTokenMissing: 'Failed to get Apple identity token', + loginFailed: 'Login failed, please try again later', + loginFailedTitle: 'Login Failed', + }, + success: { + loginSuccess: 'Login Successful', + }, +}; + +const authGuardResources = { + logout: { + error: '退出登录失败', + errorMessage: '退出登录失败,请稍后重试', + }, + confirmLogout: { + title: '确认退出', + message: '确定要退出当前账号吗?', + cancelButton: '取消', + confirmButton: '确定', + }, + deleteAccount: { + successTitle: '账号已注销', + successMessage: '您的账号已成功注销', + confirmButton: '确定', + errorTitle: '注销失败', + errorMessage: '注销失败,请稍后重试', + }, + confirmDeleteAccount: { + title: '确认注销账号', + message: '此操作不可恢复,将删除您的账号及相关数据。确定继续吗?', + cancelButton: '取消', + confirmButton: '确认注销', + }, +}; + +const authGuardResourcesEn = { + logout: { + error: 'Logout Failed', + errorMessage: 'Failed to logout, please try again later', + }, + confirmLogout: { + title: 'Confirm Logout', + message: 'Are you sure you want to logout of your current account?', + cancelButton: 'Cancel', + confirmButton: 'Confirm', + }, + deleteAccount: { + successTitle: 'Account Deleted', + successMessage: 'Your account has been successfully deleted', + confirmButton: 'OK', + errorTitle: 'Deletion Failed', + errorMessage: 'Failed to delete account, please try again later', + }, + confirmDeleteAccount: { + title: 'Confirm Account Deletion', + message: 'This action cannot be undone. Your account and all related data will be permanently deleted. Are you sure you want to continue?', + cancelButton: 'Cancel', + confirmButton: 'Confirm Deletion', + }, +}; + const resources = { zh: { translation: { @@ -1163,6 +2060,102 @@ const resources = { medications: medicationsResources, notificationSettings: notificationSettingsResources, challengeDetail: challengeDetailResources, + sleepDetail: sleepDetailResources, + stepsDetail: stepsDetailResources, + fitnessRingsDetail: fitnessRingsDetailResources, + waterDetail: waterDetailResources, + basalMetabolismDetail: basalMetabolismDetailResources, + waterReminderSettings: waterReminderSettingsResources, + waterSettings: waterSettingsResources, + workoutHistory: workoutHistoryResources, + login: loginScreenResources, + authGuard: authGuardResources, + weightRecords: { + title: '体重记录', + addButton: '记录体重', + stats: { + totalLoss: '累计减重', + currentWeight: '当前体重', + initialWeight: '初始体重', + targetWeight: '目标体重', + }, + empty: { + title: '暂无体重记录', + subtitle: '点击右上角添加按钮开始记录', + }, + modal: { + recordWeight: '记录体重', + editInitialWeight: '编辑初始体重', + editTargetWeight: '编辑目标体重', + editRecord: '编辑体重记录', + inputPlaceholder: '输入体重', + unit: 'kg', + inputHint: '请输入 0-500 之间的数值,支持小数', + quickSelection: '快速选择', + confirm: '确定', + }, + alerts: { + invalidWeight: '请输入有效的体重值(0-500kg)', + deleteFailed: '删除体重记录失败,请重试', + saveFailed: '保存体重失败,请重试', + }, + loadingHistory: '加载体重历史失败', + card: { + weightLabel: '体重', + deleteConfirmTitle: '确认删除', + deleteConfirmMessage: '确定要删除这条体重记录吗?此操作无法撤销。', + cancelButton: '取消', + deleteButton: '删除', + }, + }, + workoutDetail: { + loading: '正在加载锻炼详情...', + retry: '重新加载', + metrics: { + duration: '体能训练时间', + calories: '运动热量', + caloriesUnit: '千卡', + intensity: '运动强度', + averageHeartRate: '平均心率', + heartRateUnit: '次/分', + }, + sections: { + heartRateRange: '心率范围', + heartRateZones: '心率训练区间', + averageHeartRate: '平均心率', + maximumHeartRate: '最高心率', + minimumHeartRate: '最低心率', + heartRateUnit: '次/分', + }, + intensityInfo: { + title: '什么是运动强度?', + description1: '运动强度是你完成一项任务所用的能量估算,是衡量锻炼和其他日常活动能耗强度的指标,单位为 MET(千卡/(千克·小时))。', + description2: '因为每个人的代谢状况不同,MET 以身体的静息能耗作为参考,便于衡量不同活动的强度。', + description3: '例如:散步(约 3 km/h)相当于 2 METs,意味着它需要消耗静息状态 2 倍的能量。', + description4: '注:当设备未提供 METs 值时,系统会根据您的卡路里消耗和锻炼时长自动计算(使用70公斤估算体重)。', + formula: { + title: '运动强度计算公式', + value: 'METs = 活动能耗(千卡/小时) ÷ 静息能耗(1 千卡/小时)', + }, + legend: { + low: '< 3', + lowLabel: '低强度活动', + medium: '3 - 6', + mediumLabel: '中强度活动', + high: '≥ 6', + highLabel: '高强度活动', + }, + }, + chart: { + unavailable: '图表组件不可用,无法展示心率曲线', + noData: '暂无心率采样数据', + }, + errors: { + loadFailed: '未能获取到完整的锻炼详情', + noHeartRateData: '未获取到心率数据', + noZoneStats: '暂无区间统计', + }, + }, challenges: { title: '挑战', subtitle: '参与精选活动,保持每日动力', @@ -1371,8 +2364,23 @@ const resources = { }, }, }, + tabBarConfig: { + title: 'Tab Bar Settings', + subtitle: 'Customize your bottom navigation', + description: 'Use toggles to show or hide tabs', + resetButton: 'Reset', + cannotDisable: 'Cannot be disabled', + resetConfirm: { + title: 'Reset to Default?', + message: 'This will reset all tab bar settings and visibility', + cancel: 'Cancel', + confirm: 'Confirm', + }, + resetSuccess: 'Settings reset to default', + }, }, badges: badgesScreenResourcesEn, + login: loginScreenResourcesEn, editProfile: { title: 'Edit Profile', fields: { @@ -1600,6 +2608,38 @@ const resources = { calf: 'Calf', }, }, + circumferenceDetail: { + title: 'Circumference Statistics', + loading: 'Loading...', + error: 'Loading failed', + retry: 'Retry', + noData: 'No data available', + noDataSelected: 'Please select circumference data to display', + tabs: { + week: 'By Week', + month: 'By Month', + year: 'By Year', + }, + measurements: { + chest: 'Chest', + waist: 'Waist', + upperHip: 'Upper Hip', + arm: 'Arm', + thigh: 'Thigh', + calf: 'Calf', + }, + modal: { + title: 'Set {{label}}', + defaultTitle: 'Set Circumference', + confirm: 'Confirm', + }, + chart: { + weekLabel: 'Week {{week}}', + monthLabel: '{{month}}', + empty: 'No data available', + noSelection: 'Please select circumference data to display', + }, + }, workout: { title: 'Recent Workout', minutes: 'min', @@ -1897,6 +2937,62 @@ const resources = { reminderNotSet: 'Not set', unknownDate: 'Unknown date', }, + aiCamera: { + title: 'AI Scan', + steps: { + front: { + title: 'Front', + subtitle: 'Ensure medication name is clearly visible', + }, + side: { + title: 'Back', + subtitle: 'Include specs and ingredients info', + }, + aux: { + title: 'Side', + subtitle: 'Add more details to improve accuracy', + }, + stepProgress: 'Step {{current}} / {{total}}', + optional: '(Optional)', + notTaken: 'Empty', + }, + buttons: { + flip: 'Flip', + capture: 'Snap', + complete: 'Done', + album: 'Album', + }, + permission: { + title: 'Camera Permission Required', + description: 'Allow access to capture medication packaging for automatic recognition', + button: 'Allow Camera Access', + }, + alerts: { + pickFailed: { + title: 'Selection Failed', + message: 'Please try again or choose another image', + }, + captureFailed: { + title: 'Capture Failed', + message: 'Please try again', + }, + insufficientPhotos: { + title: 'Photos Missing', + message: 'Please capture at least front and back sides', + }, + taskFailed: { + title: 'Task Creation Failed', + defaultMessage: 'Please check network and try again', + }, + }, + guideModal: { + badge: 'Guide', + title: 'Keep Photos Clear', + description1: 'Please capture the product name and description on the front/back of the medication.', + description2: 'Ensure good lighting, avoid glare, and keep text legible. Photo clarity affects recognition accuracy.', + button: 'Got it!', + }, + }, // 药物详情页面翻译 detail: { title: 'Medication Details', @@ -2114,18 +3210,113 @@ const resources = { tabBarConfig: { title: 'Tab Bar Settings', subtitle: 'Customize your bottom navigation', - description: 'Use toggle to show or hide tabs', - resetButton: 'Reset to Default', - cannotDisable: 'This tab cannot be disabled', + description: 'Use toggles to show or hide tabs', + resetButton: 'Reset', + cannotDisable: 'Cannot be disabled', resetConfirm: { - title: 'Reset to default?', + title: 'Reset to Default?', message: 'This will reset all tab bar settings and visibility', cancel: 'Cancel', - confirm: 'Reset', + confirm: 'Confirm', }, resetSuccess: 'Settings reset to default', }, challengeDetail: challengeDetailResourcesEn, + sleepDetail: sleepDetailResourcesEn, + stepsDetail: stepsDetailResourcesEn, + fitnessRingsDetail: fitnessRingsDetailResourcesEn, + waterDetail: waterDetailResourcesEn, + basalMetabolismDetail: basalMetabolismDetailResourcesEn, + waterReminderSettings: waterReminderSettingsResourcesEn, + waterSettings: waterSettingsResourcesEn, + workoutHistory: workoutHistoryResourcesEn, + authGuard: authGuardResourcesEn, + weightRecords: { + title: 'Weight Records', + addButton: 'Record Weight', + stats: { + totalLoss: 'Total Weight Loss', + currentWeight: 'Curr. Weight', + initialWeight: 'Init. Weight', + targetWeight: 'Target Wt.', + }, + empty: { + title: 'No weight records', + subtitle: 'Tap the add button in the top right to start recording', + }, + modal: { + recordWeight: 'Record Weight', + editInitialWeight: 'Edit Initial Weight', + editTargetWeight: 'Edit Target Weight', + editRecord: 'Edit Weight Record', + inputPlaceholder: 'Enter weight', + unit: 'kg', + inputHint: 'Please enter a value between 0-500, decimals are supported', + quickSelection: 'Quick Selection', + confirm: 'Confirm', + }, + alerts: { + invalidWeight: 'Please enter a valid weight value (0-500kg)', + deleteFailed: 'Failed to delete weight record, please try again', + saveFailed: 'Failed to save weight, please try again', + }, + loadingHistory: 'Failed to load weight history', + card: { + weightLabel: 'Weight', + deleteConfirmTitle: 'Confirm Delete', + deleteConfirmMessage: 'Are you sure you want to delete this weight record? This action cannot be undone.', + cancelButton: 'Cancel', + deleteButton: 'Delete', + }, + }, + workoutDetail: { + loading: 'Loading workout details...', + retry: 'Retry', + metrics: { + duration: 'Exercise Duration', + calories: 'Exercise Calories', + caloriesUnit: 'kcal', + intensity: 'Exercise Intensity', + averageHeartRate: 'Average Heart Rate', + heartRateUnit: 'bpm', + }, + sections: { + heartRateRange: 'Heart Rate Range', + heartRateZones: 'Heart Rate Training Zones', + averageHeartRate: 'Average Heart Rate', + maximumHeartRate: 'Maximum Heart Rate', + minimumHeartRate: 'Minimum Heart Rate', + heartRateUnit: 'bpm', + }, + intensityInfo: { + title: 'What is Exercise Intensity?', + description1: 'Exercise intensity is an estimate of energy you expend to complete a task. It is a measure of intensity of exercise and other daily activities, in units of METs (kilocalories per kilogram per hour).', + description2: 'Because everyone\'s metabolism is different, METs use resting energy expenditure as a reference to facilitate measuring the intensity of different activities.', + description3: 'For example: Walking (about 3 km/h) is equivalent to 2 METs, which means it requires twice the energy of the resting state.', + description4: 'Note: When the device does not provide METs values, the system will automatically calculate based on your calorie consumption and exercise duration (using 70kg estimated weight).', + formula: { + title: 'Exercise Intensity Formula', + value: 'METs = Activity Energy Expenditure (kcal/hour) ÷ Resting Energy Expenditure (1 kcal/hour)', + }, + legend: { + low: '< 3', + lowLabel: 'Low-intensity Activity', + medium: '3 - 6', + mediumLabel: 'Moderate-intensity Activity', + high: '≥ 6', + highLabel: 'High-intensity Activity', + }, + }, + chart: { + unavailable: 'Chart component not available, unable to display heart rate curve', + noData: 'No heart rate sampling data available', + }, + errors: { + loadFailed: 'Failed to get complete workout details', + noHeartRateData: 'No heart rate data obtained', + noZoneStats: 'No zone statistics available', + }, + }, challenges: { title: 'Challenges', subtitle: 'Join curated activities, stay motivated daily', diff --git a/utils/date.ts b/utils/date.ts index c4a3b69..3303178 100644 --- a/utils/date.ts +++ b/utils/date.ts @@ -1,6 +1,8 @@ import dayjs, { Dayjs } from 'dayjs'; +import 'dayjs/locale/en'; import 'dayjs/locale/zh-cn'; +// 默认使用中文,可以通过参数切换 dayjs.locale('zh-cn'); /** @@ -61,9 +63,54 @@ export function getMonthDaysZh(date: Dayjs = dayjs()): MonthDay[] { }); } -/** 获取“今天”在当月的索引(0 基) */ +/** 获取"今天"在当月的索引(0 基) */ export function getTodayIndexInMonth(date: Dayjs = dayjs()): number { return date.date() - 1; } +/** 获取某月的所有日期(多语言版本) */ +export function getMonthDays(date: Dayjs = dayjs(), locale: 'zh' | 'en' = 'zh'): MonthDay[] { + const year = date.year(); + const monthIndex = date.month(); + const daysInMonth = date.daysInMonth(); + + // 根据语言选择星期显示 + const weekDays = locale === 'zh' + ? ['日', '一', '二', '三', '四', '五', '六'] + : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + + const today = dayjs(); + + return Array.from({ length: daysInMonth }, (_, i) => { + const d = dayjs(new Date(year, monthIndex, i + 1)); + const isToday = d.isSame(today, 'day'); + + return { + weekdayZh: weekDays[d.day()], // 保持原有字段名兼容性 + dayAbbr: weekDays[d.day()], + dayOfMonth: i + 1, + date: d, + isToday, + }; + }); +} + +/** 获取本地化的月份日期格式 */ +export function getLocalizedDateFormat(date: Dayjs, locale: 'zh' | 'en' = 'zh'): string { + if (locale === 'zh') { + return date.format('M月D日'); + } else { + return date.format('MMM D'); + } +} + +/** 获取本地化的月份标题 */ +export function getMonthTitle(date: Dayjs = dayjs(), locale: 'zh' | 'en' = 'zh'): string { + if (locale === 'zh') { + return date.format('YY年M月'); + } else { + return date.format('MMM YYYY'); + } +} +