From 2dca3253e68fce0eefc06e14f4fa2621c74f4345 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 13 Nov 2025 11:09:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E5=AE=9E=E7=8E=B0=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=9B=BD=E9=99=85=E5=8C=96=E6=94=AF=E6=8C=81=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=AD=E8=8B=B1=E6=96=87=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为所有UI组件添加国际化支持,替换硬编码文本 - 新增useI18n钩子函数统一管理翻译 - 完善中英文翻译资源,覆盖统计、用药、通知设置等模块 - 优化Tab布局使用翻译键值替代静态文本 - 更新药品管理、个人资料编辑等页面的多语言支持 --- app/(tabs)/_layout.tsx | 36 +- app/(tabs)/medications.tsx | 25 +- app/(tabs)/statistics.tsx | 10 +- app/medications/[medicationId].tsx | 171 +-- app/medications/manage-medications.tsx | 78 +- app/notification-settings.tsx | 55 +- app/profile/edit.tsx | 111 +- components/BasalMetabolismCard.tsx | 22 +- components/MoodCard.tsx | 6 +- components/NutritionRadarCard.tsx | 47 +- components/StepsCard.tsx | 10 +- components/StressMeter.tsx | 16 +- components/WaterIntakeCard.tsx | 14 +- components/WorkoutSummaryCard.tsx | 24 +- components/medication/MedicationCard.tsx | 28 +- components/statistic/CircumferenceCard.tsx | 20 +- components/statistic/OxygenSaturationCard.tsx | 12 +- components/statistic/SleepCard.tsx | 10 +- components/weight/WeightHistoryCard.tsx | 86 +- hooks/useI18n.ts | 10 + i18n/index.ts | 1244 ++++++++++++++++- 21 files changed, 1669 insertions(+), 366 deletions(-) create mode 100644 hooks/useI18n.ts diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index d7781c0..df122a3 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -3,6 +3,7 @@ import { GlassContainer, GlassView, isLiquidGlassAvailable } from 'expo-glass-ef import * as Haptics from 'expo-haptics'; import { Tabs, usePathname } from 'expo-router'; import { Icon, Label, NativeTabs } from 'expo-router/unstable-native-tabs'; +import { useTranslation } from 'react-i18next'; import React from 'react'; import { Text, TouchableOpacity, View, ViewStyle } from 'react-native'; @@ -16,18 +17,19 @@ import { useColorScheme } from '@/hooks/useColorScheme'; // Tab configuration type TabConfig = { icon: string; - title: string; + titleKey: string; }; const TAB_CONFIGS: Record = { - statistics: { icon: 'chart.pie.fill', title: '健康' }, - medications: { icon: 'pills.fill', title: '用药' }, - fasting: { icon: 'timer', title: '断食' }, - challenges: { icon: 'trophy.fill', title: '挑战' }, - personal: { icon: 'person.fill', title: '个人' }, + statistics: { icon: 'chart.pie.fill', titleKey: 'statistics.tabs.health' }, + medications: { icon: 'pills.fill', titleKey: 'statistics.tabs.medications' }, + fasting: { icon: 'timer', titleKey: 'statistics.tabs.fasting' }, + challenges: { icon: 'trophy.fill', titleKey: 'statistics.tabs.challenges' }, + personal: { icon: 'person.fill', titleKey: 'statistics.tabs.personal' }, }; export default function TabLayout() { + const { t } = useTranslation(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const pathname = usePathname(); @@ -96,7 +98,7 @@ export default function TabLayout() { }} numberOfLines={1} > - {tabConfig.title} + {t(tabConfig.titleKey)} )} @@ -175,24 +177,24 @@ export default function TabLayout() { if (glassEffectAvailable) { return - + - + - + - + - + } @@ -203,11 +205,11 @@ export default function TabLayout() { screenOptions={({ route }) => getScreenOptions(route.name)} > - - - - - + + + + + ); } diff --git a/app/(tabs)/medications.tsx b/app/(tabs)/medications.tsx index a63a88a..28ff1ae 100644 --- a/app/(tabs)/medications.tsx +++ b/app/(tabs)/medications.tsx @@ -1,5 +1,5 @@ -import { DateSelector } from '@/components/DateSelector'; import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation'; +import { DateSelector } from '@/components/DateSelector'; import { MedicationCard } from '@/components/medication/MedicationCard'; import { ThemedText } from '@/components/ThemedText'; import { IconSymbol } from '@/components/ui/IconSymbol'; @@ -17,6 +17,7 @@ import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { router } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { ScrollView, StyleSheet, @@ -32,6 +33,7 @@ type MedicationFilter = 'all' | 'taken' | 'missed'; type ThemeColors = (typeof Colors)[keyof typeof Colors]; export default function MedicationsScreen() { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const insets = useSafeAreaInsets(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; @@ -147,8 +149,8 @@ export default function MedicationsScreen() { const displayName = userProfile.name?.trim() || DEFAULT_MEMBER_NAME; const headerDateLabel = selectedDate.isSame(dayjs(), 'day') - ? `今天,${selectedDate.format('M月D日')}` - : selectedDate.format('M月D日 dddd'); + ? t('medications.dateFormats.today', { date: selectedDate.format('M月D日') }) + : t('medications.dateFormats.other', { date: selectedDate.format('M月D日 dddd') }); const emptyState = filteredMedications.length === 0; @@ -178,9 +180,9 @@ export default function MedicationsScreen() { > - 你好,{displayName} + {t('medications.greeting', { name: displayName })} - 欢迎来到用药助手! + {t('medications.welcome')} @@ -239,15 +241,10 @@ export default function MedicationsScreen() { - 今日用药 + {t('medications.todayMedications')} {(['all', 'taken', 'missed'] as MedicationFilter[]).map((filter) => { const isActive = activeFilter === filter; - const labelMap: Record = { - all: '全部', - taken: '已服用', - missed: '未服用', - }; return ( - {labelMap[filter]} + {t(`medications.filters.${filter}`)} - 今日暂无用药安排 + {t('medications.emptyState.title')} - 还未添加任何用药计划,快来补充吧。 + {t('medications.emptyState.subtitle')} ) : ( diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 4d82fba..ef27961 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -24,6 +24,7 @@ import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { debounce } from 'lodash'; import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { AppState, Image, @@ -55,6 +56,7 @@ const FloatingCard = ({ children, style }: { }; export default function ExploreScreen() { + const { t } = useTranslation(); const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000; const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard(); @@ -351,7 +353,7 @@ export default function ExploreScreen() { {/* 右边文字区域 */} - Out Live + {t('statistics.title')} {/* 开发环境调试按钮 */} @@ -360,7 +362,7 @@ export default function ExploreScreen() { { - console.log('🔧 手动触发后台任务测试...'); + console.log('🔧 Manual background task test...'); await BackgroundTaskManager.getInstance().triggerTaskForTesting(); }} > @@ -370,7 +372,7 @@ export default function ExploreScreen() { { - console.log('🫀 测试HRV数据获取...'); + console.log('🫀 Testing HRV data fetch...'); await testHRVDataFetch(); }} > @@ -407,7 +409,7 @@ export default function ExploreScreen() { {/* 身体指标section标题 */} - 身体指标 + {t('statistics.sections.bodyMetrics')} {/* 真正瀑布流布局 */} diff --git a/app/medications/[medicationId].tsx b/app/medications/[medicationId].tsx index f550ff9..11ff9e7 100644 --- a/app/medications/[medicationId].tsx +++ b/app/medications/[medicationId].tsx @@ -3,12 +3,13 @@ import { ConfirmationSheet } from '@/components/ui/ConfirmationSheet'; import { HeaderBar } from '@/components/ui/HeaderBar'; import InfoCard from '@/components/ui/InfoCard'; import { Colors } from '@/constants/Colors'; -import { DOSAGE_UNITS, DOSAGE_VALUES, FORM_LABELS, FORM_OPTIONS } from '@/constants/Medication'; +import { DOSAGE_UNITS, DOSAGE_VALUES, FORM_OPTIONS } from '@/constants/Medication'; import { ROUTES } from '@/constants/Routes'; import { useMembershipModal } from '@/contexts/MembershipModalContext'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { useI18n } from '@/hooks/useI18n'; import { useVipService } from '@/hooks/useVipService'; import { medicationNotificationService } from '@/services/medicationNotifications'; import { @@ -59,6 +60,7 @@ type RecordsSummary = { }; export default function MedicationDetailScreen() { + const { t } = useI18n(); const params = useLocalSearchParams<{ medicationId?: string }>(); const medicationId = Array.isArray(params.medicationId) ? params.medicationId[0] @@ -198,7 +200,7 @@ export default function MedicationDetailScreen() { console.error('加载药品详情失败', err); console.log('[MEDICATION_DETAIL] API call failed for medication', medicationId, err); if (isMounted) { - setError('暂时无法获取该药品的信息,请稍后重试。'); + setError(t('medications.detail.error.title')); } }) .finally(() => { @@ -295,7 +297,7 @@ export default function MedicationDetailScreen() { console.log('[MEDICATION_DETAIL] voice error', error); setDictationActive(false); setDictationLoading(false); - Alert.alert('语音识别不可用', '无法使用语音输入,请检查权限设置后重试'); + Alert.alert(t('medications.add.note.voiceError'), t('medications.add.note.voiceErrorMessage')); }; return () => { @@ -354,7 +356,7 @@ export default function MedicationDetailScreen() { } catch (error) { console.log('[MEDICATION_DETAIL] unable to start dictation', error); setDictationLoading(false); - Alert.alert('无法启动语音输入', '请检查麦克风与语音识别权限后重试'); + Alert.alert(t('medications.add.note.voiceStartError'), t('medications.add.note.voiceStartErrorMessage')); } }, [dictationActive, dictationLoading, isDictationSupported]); @@ -400,7 +402,7 @@ export default function MedicationDetailScreen() { } } catch (err) { console.error('切换药品状态失败', err); - Alert.alert('操作失败', '切换提醒状态时出现问题,请稍后重试。'); + Alert.alert(t('medications.detail.toggleError.title'), t('medications.detail.toggleError.message')); } finally { setUpdatePending(false); } @@ -430,13 +432,13 @@ export default function MedicationDetailScreen() { } } catch (error) { console.error('停用药物失败', error); - Alert.alert('操作失败', '停用药物时发生问题,请稍后重试。'); + Alert.alert(t('medications.detail.deactivate.error.title'), t('medications.detail.deactivate.error.message')); } finally { setDeactivateLoading(false); } }, [dispatch, medication, deactivateLoading]); - const formLabel = medication ? FORM_LABELS[medication.form] : ''; + const formLabel = medication ? t(`medications.manage.formLabels.${medication.form}`) : ''; const dosageLabel = medication ? `${medication.dosageValue} ${medication.dosageUnit}` : '--'; const startDateLabel = medication ? dayjs(medication.startDate).format('YYYY年M月D日') @@ -454,24 +456,24 @@ export default function MedicationDetailScreen() { return `${startDate} - ${endDate}`; } else { // 没有结束日期,显示长期 - return `${startDate} - 长期`; + return `${startDate} - ${t('medications.detail.plan.longTerm')}`; } - }, [medication]); + }, [medication, t]); const reminderTimes = medication?.medicationTimes?.length ? medication.medicationTimes.join('、') - : '尚未设置'; + : t('medications.manage.reminderNotSet'); const frequencyLabel = useMemo(() => { if (!medication) return '--'; switch (medication.repeatPattern) { case 'daily': - return `每日 ${medication.timesPerDay} 次`; + return `${t('medications.manage.frequency.daily')} ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`; case 'weekly': - return `每周 ${medication.timesPerDay} 次`; + return `${t('medications.manage.frequency.weekly')} ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`; default: - return `自定义 · ${medication.timesPerDay} 次/日`; + return `${t('medications.manage.frequency.custom')} · ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`; } - }, [medication]); + }, [medication, t]); const handleOpenNoteModal = useCallback(() => { setNoteDraft(medication?.note ?? ''); @@ -493,20 +495,20 @@ export default function MedicationDetailScreen() { closeNoteModal(); } catch (err) { console.error('保存备注失败', err); - Alert.alert('保存失败', '提交备注时出现问题,请稍后重试。'); + Alert.alert(t('medications.detail.note.saveError.title'), t('medications.detail.note.saveError.message')); } finally { setNoteSaving(false); } }, [closeNoteModal, dispatch, medication, noteDraft]); - const statusLabel = medication?.isActive ? '提醒已开启' : '提醒已关闭'; - const noteText = medication?.note?.trim() ? medication.note : '暂无备注信息'; + const statusLabel = medication?.isActive ? t('medications.detail.status.enabled') : t('medications.detail.status.disabled'); + const noteText = medication?.note?.trim() ? medication.note : t('medications.detail.note.noNote'); const dayStreakText = typeof summary.startedDays === 'number' - ? `已坚持 ${summary.startedDays} 天` + ? t('medications.detail.overview.startedDays', { days: summary.startedDays }) : medication - ? `开始于 ${dayjs(medication.startDate).format('YYYY年M月D日')}` - : '暂无开始日期'; + ? t('medications.detail.overview.startDate', { date: dayjs(medication.startDate).format('YYYY年M月D日') }) + : t('medications.detail.overview.noStartDate'); const handleDeleteMedication = useCallback(async () => { if (!medication || deleteLoading) { @@ -534,7 +536,7 @@ export default function MedicationDetailScreen() { router.back(); } catch (err) { console.error('删除药品失败', err); - Alert.alert('删除失败', '移除该药品时出现问题,请稍后再试。'); + Alert.alert(t('medications.detail.delete.error.title'), t('medications.detail.delete.error.message')); } finally { setDeleteLoading(false); } @@ -550,21 +552,26 @@ export default function MedicationDetailScreen() { if (!medication) return; const startDate = dayjs(medication.startDate).format('YYYY年M月D日'); - let message = `开始服药日期:${startDate}`; - + let message; if (medication.endDate) { const endDate = dayjs(medication.endDate).format('YYYY年M月D日'); - message += `\n结束服药日期:${endDate}`; + message = t('medications.detail.plan.periodMessage', { + startDate, + endDateInfo: t('medications.detail.plan.periodMessage', { endDate }) + }); } else { - message += `\n服药计划:长期服药`; + message = t('medications.detail.plan.periodMessage', { + startDate, + endDateInfo: t('medications.detail.plan.longTermPlan') + }); } - Alert.alert('服药周期', message); - }, [medication]); + Alert.alert(t('medications.detail.sections.plan'), message); + }, [medication, t]); const handleTimePress = useCallback(() => { - Alert.alert('服药时间', `设置的时间:${reminderTimes}`); - }, [reminderTimes]); + Alert.alert(t('medications.detail.plan.time'), t('medications.detail.plan.timeMessage', { times: reminderTimes })); + }, [reminderTimes, t]); const handleDosagePress = useCallback(() => { if (!medication) return; @@ -621,7 +628,7 @@ export default function MedicationDetailScreen() { } } catch (err) { console.error('更新剂量失败', err); - Alert.alert('更新失败', '更新剂量时出现问题,请稍后重试。'); + Alert.alert(t('medications.detail.updateErrors.dosage'), t('medications.detail.updateErrors.dosageMessage')); } finally { setUpdatePending(false); } @@ -656,7 +663,7 @@ export default function MedicationDetailScreen() { } } catch (err) { console.error('更新剂型失败', err); - Alert.alert('更新失败', '更新剂型时出现问题,请稍后重试。'); + Alert.alert(t('medications.detail.updateErrors.form'), t('medications.detail.updateErrors.formMessage')); } finally { setUpdatePending(false); } @@ -723,27 +730,27 @@ export default function MedicationDetailScreen() { onError: (error: any) => { console.error('[MEDICATION] AI 分析失败:', error); - let errorMessage = 'AI 分析失败,请稍后重试'; + let errorMessage = t('medications.detail.aiAnalysis.error.message'); // 解析服务端返回的错误信息 if (error?.message) { if (error.message.includes('[ERROR]')) { errorMessage = error.message.replace('[ERROR]', '').trim(); } else if (error.message.includes('无权访问')) { - errorMessage = '无权访问此药物'; + errorMessage = t('medications.detail.aiAnalysis.error.forbidden'); } else if (error.message.includes('不存在')) { - errorMessage = '药物不存在'; + errorMessage = t('medications.detail.aiAnalysis.error.notFound'); } } else if (error?.status === 401) { - errorMessage = '请先登录'; + errorMessage = t('medications.detail.aiAnalysis.error.unauthorized'); } else if (error?.status === 403) { - errorMessage = '无权访问此药物'; + errorMessage = t('medications.detail.aiAnalysis.error.forbidden'); } else if (error?.status === 404) { - errorMessage = '药物不存在'; + errorMessage = t('medications.detail.aiAnalysis.error.notFound'); } // 使用 Alert 弹窗显示错误 - Alert.alert('分析失败', errorMessage); + Alert.alert(t('medications.detail.aiAnalysis.error.title'), errorMessage); // 清空内容和加载状态 setAiAnalysisContent(''); @@ -756,7 +763,7 @@ export default function MedicationDetailScreen() { console.error('[MEDICATION] AI 分析异常:', error); // 使用 Alert 弹窗显示错误 - Alert.alert('分析失败', '发起分析请求失败,请检查网络连接'); + Alert.alert(t('medications.detail.aiAnalysis.error.title'), t('medications.detail.aiAnalysis.error.networkError')); // 清空内容和加载状态 setAiAnalysisContent(''); @@ -789,10 +796,10 @@ export default function MedicationDetailScreen() { - + - 未找到药品信息 - 请从用药列表重新进入此页面。 + {t('medications.detail.notFound.title')} + {t('medications.detail.notFound.subtitle')} ); @@ -815,17 +822,17 @@ export default function MedicationDetailScreen() { - + {isLoadingState ? ( - 正在载入... + {t('medications.detail.loading')} ) : error ? ( {error} - 请检查网络后重试,或返回上一页。 + {t('medications.detail.error.subtitle')} ) : medication ? ( @@ -878,10 +885,10 @@ export default function MedicationDetailScreen() { -
+
- 频率 + {t('medications.detail.plan.frequency')} {frequencyLabel} @@ -913,10 +920,10 @@ export default function MedicationDetailScreen() {
-
+
-
+
- 药品备注 + {t('medications.detail.note.label')}
-
+
- {summaryLoading ? '统计中...' : `累计服药 ${summary.takenCount} 次`} + {summaryLoading ? t('medications.detail.overview.calculating') : t('medications.detail.overview.takenCount', { count: summary.takenCount })} - {summaryLoading ? '正在计算坚持天数' : dayStreakText} + {summaryLoading ? t('medications.detail.overview.calculatingDays') : dayStreakText} @@ -974,13 +981,13 @@ export default function MedicationDetailScreen() { {/* AI 分析结果展示 - 移动到底部 */} {(aiAnalysisContent || aiAnalysisLoading) && ( -
+
{aiAnalysisLoading && !aiAnalysisContent && ( - 正在分析用药信息... + {t('medications.detail.aiAnalysis.analyzing')} )} @@ -1102,7 +1109,7 @@ export default function MedicationDetailScreen() { )} - {aiAnalysisLoading ? '分析中...' : 'AI 分析'} + {aiAnalysisLoading ? t('medications.detail.aiAnalysis.analyzingButton') : t('medications.detail.aiAnalysis.button')} ) : ( @@ -1113,7 +1120,7 @@ export default function MedicationDetailScreen() { )} - {aiAnalysisLoading ? '分析中...' : 'AI 分析'} + {aiAnalysisLoading ? t('medications.detail.aiAnalysis.analyzingButton') : t('medications.detail.aiAnalysis.button')} )} @@ -1161,7 +1168,7 @@ export default function MedicationDetailScreen() { - 编辑备注 + {t('medications.detail.note.edit')} @@ -1181,7 +1188,7 @@ export default function MedicationDetailScreen() { numberOfLines={6} value={noteDraft} onChangeText={setNoteDraft} - placeholder="记录注意事项、医生叮嘱或自定义提醒" + placeholder={t('medications.detail.note.placeholder')} placeholderTextColor={colors.textMuted} style={[styles.noteEditorInput, { color: colors.text }]} textAlignVertical="center" @@ -1213,7 +1220,7 @@ export default function MedicationDetailScreen() { {!isDictationSupported && ( - 当前设备暂不支持语音转文字,可直接输入备注 + {t('medications.detail.note.voiceNotSupported')} )} @@ -1232,7 +1239,7 @@ export default function MedicationDetailScreen() { {noteSaving ? ( ) : ( - 保存 + {t('medications.detail.note.save')} )} @@ -1253,12 +1260,12 @@ export default function MedicationDetailScreen() { /> - 选择剂量 + {t('medications.detail.dosage.selectDosage')} - 剂量值 + {t('medications.detail.dosage.dosageValue')} - 单位 + {t('medications.detail.dosage.unit')} - 取消 + {t('medications.detail.pickers.cancel')} - 确定 + {t('medications.detail.pickers.confirm')} @@ -1328,7 +1335,7 @@ export default function MedicationDetailScreen() { /> - 选择剂型 + {t('medications.detail.dosage.selectForm')} - 取消 + {t('medications.detail.pickers.cancel')} - 确定 + {t('medications.detail.pickers.confirm')} @@ -1370,10 +1377,10 @@ export default function MedicationDetailScreen() { visible={deleteSheetVisible} onClose={() => setDeleteSheetVisible(false)} onConfirm={handleDeleteMedication} - title={`删除 ${medication.name}?`} - description="删除后将清除与该药品相关的提醒与历史记录,且无法恢复。" - confirmText="删除" - cancelText="取消" + title={t('medications.detail.delete.title', { name: medication.name })} + description={t('medications.detail.delete.description')} + confirmText={t('medications.detail.delete.confirm')} + cancelText={t('medications.detail.delete.cancel')} destructive loading={deleteLoading} /> @@ -1384,10 +1391,10 @@ export default function MedicationDetailScreen() { visible={deactivateSheetVisible} onClose={() => setDeactivateSheetVisible(false)} onConfirm={handleDeactivateMedication} - title={`停用 ${medication.name}?`} - description="停用后,当天已生成的用药计划会一并删除,且无法恢复。" - confirmText="确认停用" - cancelText="取消" + title={t('medications.detail.deactivate.title', { name: medication.name })} + description={t('medications.detail.deactivate.description')} + confirmText={t('medications.detail.deactivate.confirm')} + cancelText={t('medications.detail.deactivate.cancel')} destructive loading={deactivateLoading} /> @@ -1415,7 +1422,7 @@ export default function MedicationDetailScreen() { style={styles.imageViewerFooterButton} onPress={() => setShowImagePreview(false)} > - 关闭 + {t('medications.detail.imageViewer.close')} )} diff --git a/app/medications/manage-medications.tsx b/app/medications/manage-medications.tsx index 4243cf5..60a2ce8 100644 --- a/app/medications/manage-medications.tsx +++ b/app/medications/manage-medications.tsx @@ -5,6 +5,7 @@ import { IconSymbol } from '@/components/ui/IconSymbol'; 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 { fetchMedications, @@ -12,6 +13,7 @@ import { selectMedicationsLoading, updateMedicationAction, } from '@/store/medicationsSlice'; +import { selectUserProfile } from '@/store/userSlice'; import type { Medication, MedicationForm } from '@/types/medication'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; @@ -33,25 +35,12 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; type FilterType = 'all' | 'active' | 'inactive'; -const FORM_LABELS: Record = { - capsule: '胶囊', - pill: '药片', - injection: '注射', - spray: '喷雾', - drop: '滴剂', - syrup: '糖浆', - other: '其他', -}; - -const FILTER_CONFIG: { key: FilterType; label: string }[] = [ - { key: 'all', label: '全部' }, - { key: 'active', label: '进行中' }, - { key: 'inactive', label: '已停用' }, -]; +// 这些常量将在组件内部定义,以便使用翻译函数 const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png'); export default function ManageMedicationsScreen() { + const { t } = useI18n(); const dispatch = useAppDispatch(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colors = Colors[theme]; @@ -59,6 +48,7 @@ export default function ManageMedicationsScreen() { const insets = useSafeAreaInsets(); const medications = useAppSelector(selectMedications); const loading = useAppSelector(selectMedicationsLoading); + const userProfile = useAppSelector(selectUserProfile); const [activeFilter, setActiveFilter] = useState('all'); const [pendingMedicationId, setPendingMedicationId] = useState(null); const [deactivateSheetVisible, setDeactivateSheetVisible] = useState(false); @@ -121,7 +111,7 @@ export default function ManageMedicationsScreen() { ).unwrap(); } catch (error) { console.error('更新药物状态失败', error); - Alert.alert('操作失败', '切换药物状态时发生问题,请稍后重试。'); + Alert.alert(t('medications.manage.toggleError.title'), t('medications.manage.toggleError.message')); } finally { setPendingMedicationId(null); } @@ -144,7 +134,7 @@ export default function ManageMedicationsScreen() { ).unwrap(); } catch (error) { console.error('停用药物失败', error); - Alert.alert('操作失败', '停用药物时发生问题,请稍后重试。'); + Alert.alert(t('medications.manage.deactivate.error.title'), t('medications.manage.deactivate.error.message')); } finally { setDeactivateLoading(false); setMedicationToDeactivate(null); @@ -153,14 +143,25 @@ export default function ManageMedicationsScreen() { // 创建独立的药品卡片组件,使用 React.memo 优化渲染 const MedicationCard = React.memo(({ medication, onPress }: { medication: Medication; onPress: () => void }) => { + // 使用翻译函数获取剂型标签 + const FORM_LABELS: Record = { + capsule: t('medications.manage.formLabels.capsule'), + pill: t('medications.manage.formLabels.pill'), + injection: t('medications.manage.formLabels.injection'), + spray: t('medications.manage.formLabels.spray'), + drop: t('medications.manage.formLabels.drop'), + syrup: t('medications.manage.formLabels.syrup'), + other: t('medications.manage.formLabels.other'), + }; + const dosageLabel = `${medication.dosageValue} ${medication.dosageUnit || ''} ${FORM_LABELS[medication.form] ?? ''}`.trim(); - const frequencyLabel = `${medication.repeatPattern === 'daily' ? '每日' : medication.repeatPattern === 'weekly' ? '每周' : '自定义'} | ${dosageLabel}`; + const frequencyLabel = `${medication.repeatPattern === 'daily' ? t('medications.manage.frequency.daily') : medication.repeatPattern === 'weekly' ? t('medications.manage.frequency.weekly') : t('medications.manage.frequency.custom')} | ${dosageLabel}`; const startDateLabel = dayjs(medication.startDate).isValid() ? dayjs(medication.startDate).format('M月D日') - : '未知日期'; + : t('medications.manage.unknownDate'); const reminderLabel = medication.medicationTimes?.length ? medication.medicationTimes.join('、') - : `${medication.timesPerDay} 次/日`; + : `${medication.timesPerDay} ${t('medications.manage.cardMeta.reminderNotSet')}`; return ( - {`开始于 ${startDateLabel} | 提醒:${reminderLabel}`} + {t('medications.manage.cardMeta', { date: startDateLabel, reminder: reminderLabel })} @@ -250,26 +251,27 @@ export default function ManageMedicationsScreen() { router.back()} variant="minimal" transparent /> - - - 我的用药 + {t('medications.greeting', { name: userProfile.name || '朋友' })} - 管理所有药品的状态与提醒 + {t('medications.manage.subtitle')} - {FILTER_CONFIG.map((filter) => { + {[ + { key: 'all' as FilterType, label: t('medications.manage.filters.all') }, + { key: 'active' as FilterType, label: t('medications.manage.filters.active') }, + { key: 'inactive' as FilterType, label: t('medications.manage.filters.inactive') }, + ].map((filter) => { const isActive = filter.key === activeFilter; return ( - 正在载入药品信息... + {t('medications.manage.loading')} ) : filteredMedications.length === 0 ? ( - 暂无药品 + {t('medications.manage.empty.title')} - 还没有相关药品记录,点击右上角添加 + {t('medications.manage.empty.subtitle')} ) : ( @@ -365,10 +371,10 @@ export default function ManageMedicationsScreen() { setMedicationToDeactivate(null); }} onConfirm={handleDeactivateMedication} - title={`停用 ${medicationToDeactivate.name}?`} - description="停用后,当天已生成的用药计划会一并删除,且无法恢复。" - confirmText="确认停用" - cancelText="取消" + title={t('medications.manage.deactivate.title', { name: medicationToDeactivate.name })} + description={t('medications.manage.deactivate.description')} + confirmText={t('medications.manage.deactivate.confirm')} + cancelText={t('medications.manage.deactivate.cancel')} destructive loading={deactivateLoading} /> @@ -419,7 +425,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, title: { - fontSize: 26, + fontSize: 24, fontWeight: '600', }, subtitle: { diff --git a/app/notification-settings.tsx b/app/notification-settings.tsx index ef9aab7..76e3fec 100644 --- a/app/notification-settings.tsx +++ b/app/notification-settings.tsx @@ -1,5 +1,6 @@ import { ThemedText } from '@/components/ThemedText'; import { useAuthGuard } from '@/hooks/useAuthGuard'; +import { useI18n } from '@/hooks/useI18n'; import { useNotifications } from '@/hooks/useNotifications'; import { getMedicationReminderEnabled, @@ -18,6 +19,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function NotificationSettingsScreen() { const insets = useSafeAreaInsets(); const { pushIfAuthedElseLogin } = useAuthGuard(); + const { t } = useI18n(); const { requestPermission, sendNotification } = useNotifications(); const isLgAvailable = isLiquidGlassAvailable(); @@ -36,7 +38,7 @@ export default function NotificationSettingsScreen() { setNotificationEnabledState(notification); setMedicationReminderEnabledState(medicationReminder); } catch (error) { - console.error('加载通知设置失败:', error); + console.error('Failed to load notification settings:', error); } finally { setIsLoading(false); } @@ -62,25 +64,25 @@ export default function NotificationSettingsScreen() { // 发送测试通知 await sendNotification({ - title: '通知已开启', - body: '您将收到应用通知和提醒', + title: t('notificationSettings.alerts.notificationsEnabled.title'), + body: t('notificationSettings.alerts.notificationsEnabled.body'), sound: true, priority: 'normal', }); } else { // 系统权限被拒绝,不更新用户偏好设置 Alert.alert( - '权限被拒绝', - '请在系统设置中开启通知权限,然后再尝试开启推送功能', + t('notificationSettings.alerts.permissionDenied.title'), + t('notificationSettings.alerts.permissionDenied.message'), [ - { text: '取消', style: 'cancel' }, - { text: '去设置', onPress: () => Linking.openSettings() } + { text: t('notificationSettings.alerts.permissionDenied.cancel'), style: 'cancel' }, + { text: t('notificationSettings.alerts.permissionDenied.goToSettings'), onPress: () => Linking.openSettings() } ] ); } } catch (error) { - console.error('开启推送通知失败:', error); - Alert.alert('错误', '请求通知权限失败'); + console.error('Failed to enable push notifications:', error); + Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.message')); } } else { try { @@ -91,8 +93,8 @@ export default function NotificationSettingsScreen() { await setMedicationReminderEnabled(false); setMedicationReminderEnabledState(false); } catch (error) { - console.error('关闭推送通知失败:', error); - Alert.alert('错误', '保存设置失败'); + console.error('Failed to disable push notifications:', error); + Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.saveFailed')); } } }; @@ -106,15 +108,15 @@ export default function NotificationSettingsScreen() { if (value) { // 发送测试通知 await sendNotification({ - title: '药品提醒已开启', - body: '您将在用药时间收到提醒通知', + title: t('notificationSettings.alerts.medicationReminderEnabled.title'), + body: t('notificationSettings.alerts.medicationReminderEnabled.body'), sound: true, priority: 'high', }); } } catch (error) { - console.error('设置药品提醒失败:', error); - Alert.alert('错误', '保存设置失败'); + console.error('Failed to set medication reminder:', error); + Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.medicationReminderFailed')); } }; @@ -183,7 +185,7 @@ export default function NotificationSettingsScreen() { end={{ x: 0, y: 1 }} /> - 加载中... + {t('notificationSettings.loading')} ); @@ -217,16 +219,16 @@ export default function NotificationSettingsScreen() { {/* 头部 */} - 通知设置 + {t('notificationSettings.title')} {/* 通知设置部分 */} - 通知设置 + {t('notificationSettings.sections.notifications')} @@ -235,11 +237,11 @@ export default function NotificationSettingsScreen() { {/* 药品提醒部分 */} - 药品提醒 + {t('notificationSettings.sections.medicationReminder')} - 说明 + {t('notificationSettings.sections.description')} - • 消息推送是所有通知的总开关{'\n'} - • 药品通知提醒需要在消息推送开启后才能使用{'\n'} - • 您可以在系统设置中管理通知权限{'\n'} - • 关闭消息推送将停止所有应用通知 + {t('notificationSettings.description.text')} diff --git a/app/profile/edit.tsx b/app/profile/edit.tsx index 79e819e..448435b 100644 --- a/app/profile/edit.tsx +++ b/app/profile/edit.tsx @@ -3,6 +3,7 @@ import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useCosUpload } from '@/hooks/useCosUpload'; +import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { fetchMyProfile, updateUserProfile } from '@/store/userSlice'; import { fetchMaximumHeartRate } from '@/utils/health'; @@ -46,6 +47,7 @@ interface UserProfile { const STORAGE_KEY = '@user_profile'; export default function EditProfileScreen() { + const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() const colorScheme = useColorScheme(); const colors = Colors[colorScheme ?? 'light']; @@ -189,7 +191,7 @@ export default function EditProfileScreen() { const handleSaveWithProfile = async (profileData: UserProfile) => { try { if (!userId) { - Alert.alert('未登录', '请先登录后再尝试保存'); + Alert.alert(t('editProfile.alerts.notLoggedIn.title'), t('editProfile.alerts.notLoggedIn.message')); return; } const next: UserProfile = { ...profileData }; @@ -215,7 +217,7 @@ export default function EditProfileScreen() { console.warn('更新用户信息失败', e?.message || e); } } catch (e) { - Alert.alert('保存失败', '请稍后重试'); + Alert.alert(t('editProfile.alerts.saveFailed.title'), t('editProfile.alerts.saveFailed.message')); } }; @@ -249,7 +251,7 @@ export default function EditProfileScreen() { const resp = await ImagePicker.requestMediaLibraryPermissionsAsync(); const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited'; if (!libGranted) { - Alert.alert('权限不足', '需要相册权限以选择头像'); + Alert.alert(t('editProfile.alerts.avatarPermissions.title'), t('editProfile.alerts.avatarPermissions.message')); return; } const result = await ImagePicker.launchImageLibraryAsync({ @@ -275,21 +277,21 @@ export default function EditProfileScreen() { setProfile((p) => ({ ...p, avatarUri: url })); // 保存更新后的 profile await handleSaveWithProfile({ ...profile, avatarUri: url }); - Alert.alert('成功', '头像更新成功'); + Alert.alert(t('editProfile.alerts.avatarSuccess.title'), t('editProfile.alerts.avatarSuccess.message')); } catch (e) { console.warn('上传头像失败', e); - Alert.alert('上传失败', '头像上传失败,请重试'); + Alert.alert(t('editProfile.alerts.avatarUploadFailed.title'), t('editProfile.alerts.avatarUploadFailed.message')); } } } catch (e) { - Alert.alert('发生错误', '选择头像失败,请重试'); + Alert.alert(t('editProfile.alerts.avatarError.title'), t('editProfile.alerts.avatarError.message')); } }; return ( router.back()} withSafeTop={false} transparent={true} @@ -321,8 +323,8 @@ export default function EditProfileScreen() { {/* 姓名 */} { setTempValue(profile.name || ''); setEditingField('name'); @@ -334,8 +336,8 @@ export default function EditProfileScreen() { icon="body" iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-sex.png" iconColor="#FF6B9D" - title="性别" - value={profile.gender === 'male' ? '男' : profile.gender === 'female' ? '女' : '未设置'} + title={t('editProfile.fields.gender')} + value={profile.gender === 'male' ? t('editProfile.gender.male') : profile.gender === 'female' ? t('editProfile.gender.female') : t('editProfile.gender.notSet')} onPress={() => { setEditingField('gender'); }} @@ -344,10 +346,10 @@ export default function EditProfileScreen() { {/* 身高 */} { - setTempValue(profile.height ? String(Math.round(profile.height)) : '170'); + setTempValue(profile.height ? String(Math.round(profile.height)) : String(t('editProfile.defaultValues.height'))); setEditingField('height'); }} /> @@ -355,10 +357,10 @@ export default function EditProfileScreen() { {/* 体重 */} { - setTempValue(profile.weight ? String(round(profile.weight, 1)) : '55'); + setTempValue(profile.weight ? String(round(profile.weight, 1)) : String(t('editProfile.defaultValues.weight'))); setEditingField('weight'); }} /> @@ -366,14 +368,14 @@ export default function EditProfileScreen() { {/* 活动水平 */} { switch (profile.activityLevel) { - case 1: return '久坐'; - case 2: return '轻度活跃'; - case 3: return '中度活跃'; - case 4: return '非常活跃'; - default: return '久坐'; + case 1: return t('editProfile.activityLevels.1'); + case 2: return t('editProfile.activityLevels.2'); + case 3: return t('editProfile.activityLevels.3'); + case 4: return t('editProfile.activityLevels.4'); + default: return t('editProfile.activityLevels.1'); } })()} onPress={() => { @@ -384,15 +386,20 @@ export default function EditProfileScreen() { {/* 出生日期 */} { try { const d = new Date(profile.birthDate); - return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`; + if (t('editProfile.birthDate.format').includes('{{year}}年')) { + return t('editProfile.birthDate.format', { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate() }); + } else { + const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + return t('editProfile.birthDate.format', { year: d.getFullYear(), month: monthNames[d.getMonth()], day: d.getDate() }); + } } catch { - return '1995年1月1日'; + return t('editProfile.birthDate.placeholder'); } - })() : '1995年1月1日'} + })() : t('editProfile.birthDate.placeholder')} onPress={() => { openDatePicker(); }} @@ -402,11 +409,11 @@ export default function EditProfileScreen() { { // 最大心率不可编辑,只显示 - Alert.alert('提示', '最大心率数据从健康应用自动获取'); + Alert.alert(t('editProfile.maxHeartRate.alert.title'), t('editProfile.maxHeartRate.alert.message')); }} disabled={true} hideArrow={true} @@ -432,6 +439,7 @@ export default function EditProfileScreen() { } else if (field === 'gender') { updatedProfile.gender = value as 'male' | 'female'; setProfile(p => ({ ...p, gender: value as 'male' | 'female' })); + } else if (field === 'height') { updatedProfile.height = parseFloat(value) || undefined; setProfile(p => ({ ...p, height: parseFloat(value) || undefined })); @@ -455,8 +463,8 @@ export default function EditProfileScreen() { colors={colors} textColor={textColor} placeholderColor={placeholderColor} + t={t} /> - {/* 出生日期选择器弹窗 */} - 取消 + {t('editProfile.modals.cancel')} { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}> - 确定 + {t('editProfile.modals.confirm')} )} @@ -536,7 +544,7 @@ function ProfileCard({ icon, iconUri, iconColor, title, value, onPress, disabled ); } -function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor }: { +function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor, t }: { visible: boolean; field: string | null; value: string; @@ -546,6 +554,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te colors: any; textColor: string; placeholderColor: string; + t: (key: string) => string; }) { const [inputValue, setInputValue] = useState(value); const [selectedGender, setSelectedGender] = useState(profile.gender || 'female'); @@ -563,10 +572,10 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te case 'name': return ( - 昵称 + {t('editProfile.fields.name')} - 性别 + {t('editProfile.fields.gender')} setSelectedGender('female')} > - 女性 + {t('editProfile.modals.female')} setSelectedGender('male')} > - 男性 + {t('editProfile.modals.male')} @@ -599,7 +608,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te case 'height': return ( - 身高 + {t('editProfile.fields.height')} {Array.from({ length: 101 }, (_, i) => 120 + i).map(height => ( - + ))} @@ -616,29 +625,29 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te case 'weight': return ( - 体重 + {t('editProfile.fields.weight')} - 公斤 (kg) + {t('editProfile.modals.input.weightUnit')} ); case 'activity': return ( - 活动水平 + {t('editProfile.fields.activityLevel')} {[ - { key: 1, label: '久坐', desc: '很少运动' }, - { key: 2, label: '轻度活跃', desc: '每周1-3次运动' }, - { key: 3, label: '中度活跃', desc: '每周3-5次运动' }, - { key: 4, label: '非常活跃', desc: '每周6-7次运动' }, + { key: 1, label: t('editProfile.activityLevels.1'), desc: t('editProfile.activityLevels.descriptions.1') }, + { key: 2, label: t('editProfile.activityLevels.2'), desc: t('editProfile.activityLevels.descriptions.2') }, + { key: 3, label: t('editProfile.activityLevels.3'), desc: t('editProfile.activityLevels.descriptions.3') }, + { key: 4, label: t('editProfile.activityLevels.4'), desc: t('editProfile.activityLevels.descriptions.4') }, ].map(item => ( - 取消 + {t('editProfile.modals.cancel')} { @@ -682,7 +691,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te }} style={[styles.modalSaveBtn, { backgroundColor: colors.primary }]} > - 保存 + {t('editProfile.modals.save')} diff --git a/components/BasalMetabolismCard.tsx b/components/BasalMetabolismCard.tsx index 4987b60..2d67fe3 100644 --- a/components/BasalMetabolismCard.tsx +++ b/components/BasalMetabolismCard.tsx @@ -6,6 +6,7 @@ import dayjs from 'dayjs'; import { Image } from 'expo-image'; import { router } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; interface BasalMetabolismCardProps { @@ -14,6 +15,7 @@ interface BasalMetabolismCardProps { } export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCardProps) { + const { t } = useTranslation(); const [basalMetabolism, setBasalMetabolism] = useState(null); const [loading, setLoading] = useState(false); @@ -90,7 +92,7 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard return result; } catch (error) { - console.error('BasalMetabolismCard: 获取基础代谢数据失败:', error); + console.error('BasalMetabolismCard: Failed to get basal metabolism data:', error); return null; } finally { // 清理请求记录 @@ -134,20 +136,20 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard // 使用 useMemo 优化状态描述计算 const status = useMemo(() => { if (basalMetabolism === null || basalMetabolism === 0) { - return { text: '未知', color: '#9AA3AE' }; + return { text: t('statistics.components.metabolism.status.unknown'), color: '#9AA3AE' }; } // 基于常见的基础代谢范围来判断状态 if (basalMetabolism >= 1800) { - return { text: '高代谢', color: '#10B981' }; + return { text: t('statistics.components.metabolism.status.high'), color: '#10B981' }; } else if (basalMetabolism >= 1400) { - return { text: '正常', color: '#3B82F6' }; + return { text: t('statistics.components.metabolism.status.normal'), color: '#3B82F6' }; } else if (basalMetabolism >= 1000) { - return { text: '偏低', color: '#F59E0B' }; + return { text: t('statistics.components.metabolism.status.low'), color: '#F59E0B' }; } else { - return { text: '较低', color: '#EF4444' }; + return { text: t('statistics.components.metabolism.status.veryLow'), color: '#EF4444' }; } - }, [basalMetabolism]); + }, [basalMetabolism, t]); return ( <> @@ -163,7 +165,7 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard source={require('@/assets/images/icons/icon-fire.png')} style={styles.titleIcon} /> - 基础代谢 + {t('statistics.components.metabolism.title')} {status.text} @@ -173,9 +175,9 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard {/* 数值显示区域 */} - {loading ? '加载中...' : (basalMetabolism != null && basalMetabolism > 0 ? Math.round(basalMetabolism).toString() : '--')} + {loading ? t('statistics.components.metabolism.loading') : (basalMetabolism != null && basalMetabolism > 0 ? Math.round(basalMetabolism).toString() : '--')} - 千卡/日 + {t('statistics.components.metabolism.unit')} diff --git a/components/MoodCard.tsx b/components/MoodCard.tsx index 2f24104..43de6c2 100644 --- a/components/MoodCard.tsx +++ b/components/MoodCard.tsx @@ -2,6 +2,7 @@ import { MoodCheckin, getMoodConfig } from '@/services/moodCheckins'; import dayjs from 'dayjs'; import LottieView from 'lottie-react-native'; import React, { useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; interface MoodCardProps { @@ -11,6 +12,7 @@ interface MoodCardProps { } export function MoodCard({ moodCheckin, onPress }: MoodCardProps) { + const { t } = useTranslation(); const moodConfig = moodCheckin ? getMoodConfig(moodCheckin.moodType) : null; const animationRef = useRef(null); @@ -28,7 +30,7 @@ export function MoodCard({ moodCheckin, onPress }: MoodCardProps) { source={require('@/assets/images/icons/icon-mood.png')} style={styles.titleIcon} /> - 心情 + {t('statistics.components.mood.title')} ) : ( - 点击记录心情 + {t('statistics.components.mood.empty')} )} ); diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index a89081c..4f676b9 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -9,6 +9,7 @@ import { calculateRemainingCalories } from '@/utils/nutrition'; import dayjs from 'dayjs'; import { router } from 'expo-router'; import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Animated, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Svg, { Circle } from 'react-native-svg'; @@ -25,10 +26,12 @@ export type NutritionRadarCardProps = { // 简化的圆环进度组件 const SimpleRingProgress = ({ remainingCalories, - totalAvailable + totalAvailable, + t }: { remainingCalories: number; totalAvailable: number; + t: any; }) => { const animatedProgress = useRef(new Animated.Value(0)).current; const radius = 32; @@ -82,7 +85,7 @@ const SimpleRingProgress = ({ {Math.round(remainingCalories)} - 还能吃 + {t('statistics.components.diet.remaining')} ); @@ -93,6 +96,7 @@ export function NutritionRadarCard({ style, resetToken, }: NutritionRadarCardProps) { + const { t } = useTranslation(); const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast'); const [loading, setLoading] = useState(false); @@ -122,7 +126,7 @@ export function NutritionRadarCard({ dispatch(fetchDailyBasalMetabolism(targetDate)).unwrap(), ]); } catch (error) { - console.error('NutritionRadarCard: 获取营养卡片数据失败:', error); + console.error('NutritionRadarCard: Failed to get nutrition card data:', error); } finally { setLoading(false); } @@ -133,14 +137,14 @@ export function NutritionRadarCard({ const nutritionStats = useMemo(() => { return [ - { label: '热量', value: nutritionSummary ? `${Math.round(nutritionSummary.totalCalories)} 千卡` : '0 千卡', color: '#FF6B6B' }, - { label: '蛋白质', value: nutritionSummary ? `${nutritionSummary.totalProtein.toFixed(1)} g` : '0.0 g', color: '#4ECDC4' }, - { label: '碳水', value: nutritionSummary ? `${nutritionSummary.totalCarbohydrate.toFixed(1)} g` : '0.0 g', color: '#45B7D1' }, - { label: '脂肪', value: nutritionSummary ? `${nutritionSummary.totalFat.toFixed(1)} g` : '0.0 g', color: '#FFA07A' }, - { label: '纤维', value: nutritionSummary ? `${nutritionSummary.totalFiber.toFixed(1)} g` : '0.0 g', color: '#98D8C8' }, - { label: '钠', value: nutritionSummary ? `${Math.round(nutritionSummary.totalSodium)} mg` : '0 mg', color: '#F7DC6F' }, + { label: t('statistics.components.diet.calories'), value: nutritionSummary ? `${Math.round(nutritionSummary.totalCalories)} ${t('statistics.components.diet.kcal')}` : `0 ${t('statistics.components.diet.kcal')}`, color: '#FF6B6B' }, + { label: t('statistics.components.diet.protein'), value: nutritionSummary ? `${nutritionSummary.totalProtein.toFixed(1)} g` : '0.0 g', color: '#4ECDC4' }, + { label: t('statistics.components.diet.carb'), value: nutritionSummary ? `${nutritionSummary.totalCarbohydrate.toFixed(1)} g` : '0.0 g', color: '#45B7D1' }, + { label: t('statistics.components.diet.fat'), value: nutritionSummary ? `${nutritionSummary.totalFat.toFixed(1)} g` : '0.0 g', color: '#FFA07A' }, + { label: t('statistics.components.diet.fiber'), value: nutritionSummary ? `${nutritionSummary.totalFiber.toFixed(1)} g` : '0.0 g', color: '#98D8C8' }, + { label: t('statistics.components.diet.sodium'), value: nutritionSummary ? `${Math.round(nutritionSummary.totalSodium)} mg` : '0 mg', color: '#F7DC6F' }, ]; - }, [nutritionSummary]); + }, [nutritionSummary, t]); // 计算还能吃的卡路里 const consumedCalories = nutritionSummary?.totalCalories || 0; @@ -168,10 +172,10 @@ export function NutritionRadarCard({ source={require('@/assets/images/icons/icon-healthy-diet.png')} style={styles.titleIcon} /> - 饮食分析 + {t('statistics.components.diet.title')} - {loading ? '加载中...' : `更新: ${dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}`} + {loading ? t('statistics.components.diet.loading') : t('statistics.components.diet.updated', { time: dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm') })} @@ -180,6 +184,7 @@ export function NutritionRadarCard({ @@ -199,7 +204,7 @@ export function NutritionRadarCard({ - 还能吃 + {t('statistics.components.diet.remaining')} (loading || activeCaloriesLoading) ? '--' : Math.round(v).toString()} /> - 千卡 + {t('statistics.components.diet.kcal')} = - 基代 + {t('statistics.components.diet.basal')} + - 运动 + {t('statistics.components.diet.exercise')} - - 饮食 + {t('statistics.components.diet.diet')} - AI识别 + {t('statistics.components.diet.aiRecognition')} - 食物库 + {t('statistics.components.diet.foodLibrary')} - 一句话记录 + {t('statistics.components.diet.voiceRecord')} - 成分表分析 + {t('statistics.components.diet.nutritionLabel')} diff --git a/components/StepsCard.tsx b/components/StepsCard.tsx index 043bfd9..c7249ff 100644 --- a/components/StepsCard.tsx +++ b/components/StepsCard.tsx @@ -14,6 +14,7 @@ import { logger } from '@/utils/logger'; import dayjs from 'dayjs'; import { Image } from 'expo-image'; import { useRouter } from 'expo-router'; +import { useTranslation } from 'react-i18next'; import { AnimatedNumber } from './AnimatedNumber'; // 使用原生View来替代SVG,避免导入问题 // import Svg, { Rect } from 'react-native-svg'; @@ -28,6 +29,7 @@ const StepsCard: React.FC = ({ curDate, style, }) => { + const { t } = useTranslation(); const router = useRouter(); const [stepCount, setStepCount] = useState(0) @@ -36,7 +38,7 @@ const StepsCard: React.FC = ({ const getStepData = useCallback(async (date: Date) => { try { - logger.info('获取步数数据...'); + logger.info('Getting step data...'); // 先获取步数,立即更新UI const [steps, hourly] = await Promise.all([ @@ -47,7 +49,7 @@ const StepsCard: React.FC = ({ setHourSteps(hourly); } catch (error) { - logger.error('获取步数数据失败:', error); + logger.error('Failed to get step data:', error); } }, []); @@ -122,7 +124,7 @@ const StepsCard: React.FC = ({ source={require('@/assets/images/icons/icon-step.png')} style={styles.titleIcon} /> - 步数 + {t('statistics.components.steps.title')} {/* 柱状图 */} @@ -190,7 +192,7 @@ const StepsCard: React.FC = ({ stepCount !== null ? `${Math.round(v)}` : '——'} + format={(v) => stepCount !== null ? `${Math.round(v)}` : '--'} resetToken={stepCount} /> diff --git a/components/StressMeter.tsx b/components/StressMeter.tsx index 0491685..c140fb3 100644 --- a/components/StressMeter.tsx +++ b/components/StressMeter.tsx @@ -2,6 +2,7 @@ import { fetchHRVWithStatus } from '@/utils/health'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { StressAnalysisModal } from './StressAnalysisModal'; @@ -10,6 +11,7 @@ interface StressMeterProps { } export function StressMeter({ curDate }: StressMeterProps) { + const { t } = useTranslation(); // 将HRV值转换为压力指数(0-100) // HRV值范围:30-110ms,映射到压力指数100-0 @@ -34,23 +36,23 @@ export function StressMeter({ curDate }: StressMeterProps) { const getHrvData = async () => { try { - console.log('StressMeter: 开始获取HRV数据...', curDate); + console.log('StressMeter: Starting to get HRV data...', curDate); // 使用智能HRV数据获取功能 const result = await fetchHRVWithStatus(curDate); - console.log('StressMeter: HRV数据获取结果:', result); + console.log('StressMeter: HRV data fetch result:', result); if (result.hrvData) { setHrvValue(Math.round(result.hrvData.value)); - console.log(`StressMeter: 使用${result.message},HRV值: ${result.hrvData.value}ms`); + console.log(`StressMeter: Using ${result.message}, HRV value: ${result.hrvData.value}ms`); } else { - console.log('StressMeter: 未获取到HRV数据'); + console.log('StressMeter: No HRV data obtained'); // 可以设置一个默认值或者显示无数据状态 setHrvValue(0); } } catch (error) { - console.error('StressMeter: 获取HRV数据失败:', error); + console.error('StressMeter: Failed to get HRV data:', error); setHrvValue(0); } } @@ -84,7 +86,7 @@ export function StressMeter({ curDate }: StressMeterProps) { source={require('@/assets/images/icons/icon-pressure.png')} style={styles.titleIcon} /> - 压力 + {t('statistics.components.stress.title')} {/* {updateTime && ( {formatUpdateTime(updateTime)} @@ -94,7 +96,7 @@ export function StressMeter({ curDate }: StressMeterProps) { {/* 数值显示区域 */} {hrvValue || '--'} - ms + {t('statistics.components.stress.unit')} {/* 进度条区域 */} diff --git a/components/WaterIntakeCard.tsx b/components/WaterIntakeCard.tsx index 7a4d320..011cf10 100644 --- a/components/WaterIntakeCard.tsx +++ b/components/WaterIntakeCard.tsx @@ -7,6 +7,7 @@ import { Image } from 'expo-image'; import { useRouter } from 'expo-router'; import LottieView from 'lottie-react-native'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Animated, StyleSheet, @@ -26,6 +27,7 @@ const WaterIntakeCard: React.FC = ({ style, selectedDate }) => { + const { t } = useTranslation(); const router = useRouter(); const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord, getWaterRecordsByDate } = useWaterDataByDate(selectedDate); const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载 @@ -89,7 +91,7 @@ const WaterIntakeCard: React.FC = ({ const targetDate = selectedDate || dayjs().format('YYYY-MM-DD'); await getWaterRecordsByDate(targetDate); } catch (error) { - console.error('页面聚焦时加载数据失败:', error); + console.error('Failed to load data on focus:', error); } }; @@ -185,11 +187,11 @@ const WaterIntakeCard: React.FC = ({ source={require('@/assets/images/icons/IconGlass.png')} style={styles.titleIcon} /> - 喝水 + {t('statistics.components.water.title')} {isToday && ( - + {quickWaterAmount}ml + {t('statistics.components.water.addButton', { amount: quickWaterAmount })} )} @@ -254,14 +256,14 @@ const WaterIntakeCard: React.FC = ({ `${Math.round(value)}ml`} + format={(value) => `${Math.round(value)}${t('statistics.components.water.unit')}`} resetToken={selectedDate} /> ) : ( - —— + -- )} - / {targetIntake}ml + / {targetIntake}{t('statistics.components.water.unit')} diff --git a/components/WorkoutSummaryCard.tsx b/components/WorkoutSummaryCard.tsx index 1fad8eb..f488c06 100644 --- a/components/WorkoutSummaryCard.tsx +++ b/components/WorkoutSummaryCard.tsx @@ -3,6 +3,7 @@ import dayjs from 'dayjs'; import { Image } from 'expo-image'; import { useRouter } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native'; import { AnimatedNumber } from '@/components/AnimatedNumber'; @@ -40,6 +41,7 @@ const DEFAULT_SUMMARY: WorkoutSummary = { export const WorkoutSummaryCard: React.FC = ({ date, style }) => { + const { t } = useTranslation(); const router = useRouter(); const [summary, setSummary] = useState(DEFAULT_SUMMARY); const [isLoading, setIsLoading] = useState(false); @@ -145,13 +147,13 @@ export const WorkoutSummaryCard: React.FC = ({ date, st const label = lastWorkout ? getWorkoutTypeDisplayName(lastWorkout.workoutActivityType) - : '尚无锻炼数据'; + : t('statistics.components.workout.noData'); const time = lastWorkout - ? `${dayjs(lastWorkout.endDate || lastWorkout.startDate).format('HH:mm')} 更新` - : '等待同步'; + ? `${dayjs(lastWorkout.endDate || lastWorkout.startDate).format('HH:mm')} ${t('statistics.components.workout.updated')}` + : t('statistics.components.workout.syncing'); - let source = '来源:等待同步'; + let source = t('statistics.components.workout.sourceWaiting'); if (hasWorkouts) { const sourceNames = summary.workouts .map((workout) => workout.source?.name?.trim() || workout.source?.bundleIdentifier?.trim()) @@ -160,9 +162,11 @@ export const WorkoutSummaryCard: React.FC = ({ date, st if (sourceNames.length) { const uniqueNames = Array.from(new Set(sourceNames)); const displayNames = uniqueNames.slice(0, 2).join('、'); - source = uniqueNames.length > 2 ? `来源:${displayNames} 等` : `来源:${displayNames}`; + source = uniqueNames.length > 2 + ? t('statistics.components.workout.sourceFormatMultiple', { source: displayNames }) + : t('statistics.components.workout.sourceFormat', { source: displayNames }); } else { - source = '来源:未知'; + source = t('statistics.components.workout.sourceUnknown'); } } @@ -184,7 +188,7 @@ export const WorkoutSummaryCard: React.FC = ({ date, st source, badges: uniqueBadges, }; - }, [summary]); + }, [summary, t]); return ( = ({ date, st - 近期锻炼 + {t('statistics.components.workout.title')} - 分钟 + {t('statistics.components.workout.minutes')} - 千卡 + {t('statistics.components.workout.kcal')} diff --git a/components/medication/MedicationCard.tsx b/components/medication/MedicationCard.tsx index 66cf5c0..861605b 100644 --- a/components/medication/MedicationCard.tsx +++ b/components/medication/MedicationCard.tsx @@ -1,5 +1,6 @@ import { ThemedText } from '@/components/ThemedText'; import { useAppDispatch } from '@/hooks/redux'; +import { useI18n } from '@/hooks/useI18n'; import { takeMedicationAction } from '@/store/medicationsSlice'; import type { MedicationDisplayItem } from '@/types/medication'; import { Ionicons } from '@expo/vector-icons'; @@ -19,6 +20,7 @@ export type MedicationCardProps = { export function MedicationCard({ medication, colors, selectedDate, onOpenDetails, onCelebrate }: MedicationCardProps) { const dispatch = useAppDispatch(); + const { t } = useI18n(); const [isSubmitting, setIsSubmitting] = useState(false); const [imageError, setImageError] = useState(false); @@ -43,11 +45,11 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails if (timeDiffMinutes > 60) { // 显示二次确认弹窗 Alert.alert( - '尚未到服药时间', - `该用药计划在 ${medication.scheduledTime},现在还早于1小时以上。\n\n是否确认已服用此药物?`, + t('medications.card.earlyTakeAlert.title'), + t('medications.card.earlyTakeAlert.message', { time: medication.scheduledTime }), [ { - text: '取消', + text: t('medications.card.earlyTakeAlert.cancel'), style: 'cancel', onPress: () => { // 用户取消,不执行任何操作 @@ -55,7 +57,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails }, }, { - text: '确认已服用', + text: t('medications.card.earlyTakeAlert.confirm'), style: 'default', onPress: () => { // 用户确认,执行服药逻辑 @@ -89,9 +91,9 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails } catch (error) { console.error('[MEDICATION_CARD] 服药操作失败', error); Alert.alert( - '操作失败', - error instanceof Error ? error.message : '记录服药时发生错误,请稍后重试', - [{ text: '确定' }] + t('medications.card.takeError.title'), + error instanceof Error ? error.message : t('medications.card.takeError.message'), + [{ text: t('medications.card.takeError.confirm') }] ); } finally { setIsSubmitting(false); @@ -102,7 +104,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails if (medication.status === 'missed') { return ( - 已错过 + {t('medications.card.status.missed')} ); } @@ -112,7 +114,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails return ( - 到服药时间 + {t('medications.card.status.timeToTake')} ); } @@ -125,7 +127,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails return ( - 剩余 {formatted} + {t('medications.card.status.remaining', { time: formatted })} ); } @@ -138,7 +140,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails return ( - 已服用 + {t('medications.card.action.taken')} ); } @@ -158,13 +160,13 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails isInteractive={!isSubmitting} > - {isSubmitting ? '提交中...' : '立即服用'} + {isSubmitting ? t('medications.card.action.submitting') : t('medications.card.action.takeNow')} ) : ( - {isSubmitting ? '提交中...' : '立即服用'} + {isSubmitting ? t('medications.card.action.submitting') : t('medications.card.action.takeNow')} )} diff --git a/components/statistic/CircumferenceCard.tsx b/components/statistic/CircumferenceCard.tsx index 8bf6e1e..09e059c 100644 --- a/components/statistic/CircumferenceCard.tsx +++ b/components/statistic/CircumferenceCard.tsx @@ -4,6 +4,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard'; import { selectUserProfile, updateUserBodyMeasurements, UserProfile } from '@/store/userSlice'; import { router } from 'expo-router'; import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; interface CircumferenceCardProps { @@ -11,6 +12,7 @@ interface CircumferenceCardProps { } const CircumferenceCard: React.FC = ({ style }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const userProfile = useAppSelector(selectUserProfile); @@ -30,32 +32,32 @@ const CircumferenceCard: React.FC = ({ style }) => { const measurements = [ { key: 'chestCircumference', - label: '胸围', + label: t('statistics.components.circumference.measurements.chest'), value: userProfile?.chestCircumference, }, { key: 'waistCircumference', - label: '腰围', + label: t('statistics.components.circumference.measurements.waist'), value: userProfile?.waistCircumference, }, { key: 'upperHipCircumference', - label: '上臀围', + label: t('statistics.components.circumference.measurements.hip'), value: userProfile?.upperHipCircumference, }, { key: 'armCircumference', - label: '臂围', + label: t('statistics.components.circumference.measurements.arm'), value: userProfile?.armCircumference, }, { key: 'thighCircumference', - label: '大腿围', + label: t('statistics.components.circumference.measurements.thigh'), value: userProfile?.thighCircumference, }, { key: 'calfCircumference', - label: '小腿围', + label: t('statistics.components.circumference.measurements.calf'), value: userProfile?.calfCircumference, }, ]; @@ -145,7 +147,7 @@ const CircumferenceCard: React.FC = ({ style }) => { onPress={handleCardPress} activeOpacity={0.8} > - 围度 (cm) + {t('statistics.components.circumference.title')} {measurements.map((measurement, index) => ( @@ -174,12 +176,12 @@ const CircumferenceCard: React.FC = ({ style }) => { setModalVisible(false); setSelectedMeasurement(null); }} - title={selectedMeasurement ? `设置${selectedMeasurement.label}` : '设置围度'} + title={selectedMeasurement ? t('statistics.components.circumference.setTitle', { label: selectedMeasurement.label }) : t('statistics.components.circumference.title')} items={circumferenceOptions} selectedValue={selectedMeasurement?.currentValue} onValueChange={() => { }} // Real-time update not needed onConfirm={handleUpdateMeasurement} - confirmButtonText="确认" + confirmButtonText={t('statistics.components.circumference.confirm')} pickerHeight={180} /> diff --git a/components/statistic/OxygenSaturationCard.tsx b/components/statistic/OxygenSaturationCard.tsx index b619fa2..a933538 100644 --- a/components/statistic/OxygenSaturationCard.tsx +++ b/components/statistic/OxygenSaturationCard.tsx @@ -1,8 +1,9 @@ -import React, { useState, useCallback, useRef } from 'react'; -import { useFocusEffect } from '@react-navigation/native'; -import HealthDataCard from './HealthDataCard'; import { fetchOxygenSaturation } from '@/utils/health'; +import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; +import React, { useCallback, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import HealthDataCard from './HealthDataCard'; interface OxygenSaturationCardProps { style?: object; @@ -13,6 +14,7 @@ const OxygenSaturationCard: React.FC = ({ style, selectedDate }) => { + const { t } = useTranslation(); const [oxygenSaturation, setOxygenSaturation] = useState(null); const [loading, setLoading] = useState(false); const loadingRef = useRef(false); @@ -38,7 +40,7 @@ const OxygenSaturationCard: React.FC = ({ const data = await fetchOxygenSaturation(options); setOxygenSaturation(data); } catch (error) { - console.error('OxygenSaturationCard: 获取血氧饱和度数据失败:', error); + console.error('OxygenSaturationCard: Failed to get blood oxygen data:', error); setOxygenSaturation(null); } finally { setLoading(false); @@ -52,7 +54,7 @@ const OxygenSaturationCard: React.FC = ({ return ( = ({ selectedDate, style, }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const challenges = useAppSelector(selectChallengeList); const [sleepDuration, setSleepDuration] = useState(null); @@ -39,7 +41,7 @@ const SleepCard: React.FC = ({ const data = await fetchCompleteSleepData(selectedDate); setSleepDuration(data?.totalSleepTime || null); } catch (error) { - console.error('SleepCard: 获取睡眠数据失败:', error); + console.error('SleepCard: Failed to get sleep data:', error); setSleepDuration(null); } finally { setLoading(false); @@ -75,7 +77,7 @@ const SleepCard: React.FC = ({ try { await dispatch(reportChallengeProgress({ id: sleepChallenge.id, value: sleepDuration })).unwrap(); } catch (error) { - logger.warn('SleepCard: 挑战进度上报失败', { error, challengeId: sleepChallenge.id }); + logger.warn('SleepCard: Challenge progress report failed', { error, challengeId: sleepChallenge.id }); } lastReportedRef.current = { date: dateKey, value: sleepDuration }; @@ -91,10 +93,10 @@ const SleepCard: React.FC = ({ source={require('@/assets/images/icons/icon-sleep.png')} style={styles.titleIcon} /> - 睡眠 + {t('statistics.components.sleep.title')} - {loading ? '加载中...' : (sleepDuration != null ? formatSleepTime(sleepDuration) : '--')} + {loading ? t('statistics.components.sleep.loading') : (sleepDuration != null ? formatSleepTime(sleepDuration) : '--')} ); diff --git a/components/weight/WeightHistoryCard.tsx b/components/weight/WeightHistoryCard.tsx index 83578c1..35a8052 100644 --- a/components/weight/WeightHistoryCard.tsx +++ b/components/weight/WeightHistoryCard.tsx @@ -10,6 +10,7 @@ import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Dimensions, Modal, @@ -22,13 +23,14 @@ import { import Svg, { Circle, Path } from 'react-native-svg'; const { width: screenWidth } = Dimensions.get('window'); -const CARD_WIDTH = screenWidth - 40; // 减去左右边距 -const CHART_WIDTH = CARD_WIDTH - 36; // 减去卡片内边距 +const CARD_WIDTH = screenWidth - 40; // Subtract left and right margins +const CHART_WIDTH = CARD_WIDTH - 36; // Subtract card padding const CHART_HEIGHT = 60; const PADDING = 10; export function WeightHistoryCard() { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const userProfile = useAppSelector((s) => s.user.profile); const weightHistory = useAppSelector((s) => s.user.weightHistory); @@ -53,7 +55,7 @@ export function WeightHistoryCard() { try { await dispatch(fetchWeightHistory() as any).unwrap(); } catch (error) { - console.error('加载体重历史失败:', error); + console.error('Failed to load weight history:', error); } }; @@ -70,20 +72,20 @@ export function WeightHistoryCard() { }; - // 处理体重历史数据 + // Process weight history data const sortedHistory = [...weightHistory] .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) - .slice(-7); // 只显示最近7条记录 + .slice(-7); // Show only the last 7 records // return ( // // - // 体重记录 + // {t('statistics.components.weight.title')} // // // - // 暂无体重记录,点击下方按钮开始记录 + // No weight records yet, click the button below to start recording // // // - // 记录体重 + // {t('statistics.components.weight.addButton')} // // // // ); // } - // 生成图表数据 + // Generate chart data const weights = sortedHistory.map(item => parseFloat(item.weight)); const minWeight = Math.min(...weights); const maxWeight = Math.max(...weights); @@ -110,18 +112,18 @@ export function WeightHistoryCard() { const points = sortedHistory.map((item, index) => { const x = PADDING + (index / Math.max(sortedHistory.length - 1, 1)) * (CHART_WIDTH - 2 * PADDING); const normalizedWeight = (parseFloat(item.weight) - minWeight) / weightRange; - // 减少顶部边距,压缩留白 + // Reduce top margin, compress whitespace const y = PADDING + 8 + (1 - normalizedWeight) * (CHART_HEIGHT - 2 * PADDING - 16); return { x, y, weight: item.weight, date: item.createdAt }; }); - // 生成路径 + // Generate path const pathData = points.map((point, index) => { if (index === 0) return `M ${point.x} ${point.y}`; return `L ${point.x} ${point.y}`; }).join(' '); - // 如果只有一个数据点,显示为水平线 + // If there's only one data point, display as a horizontal line const singlePointPath = points.length === 1 ? `M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}` : pathData; @@ -133,7 +135,7 @@ export function WeightHistoryCard() { source={require('@/assets/images/icons/icon-weight.png')} style={styles.iconSquare} /> - 体重记录 + {t('statistics.components.weight.title')} {isLgAvaliable ? ( { @@ -160,13 +162,13 @@ export function WeightHistoryCard() { )} - {/* 默认显示图表 */} + {/* Default chart display */} {sortedHistory.length > 0 && ( - {/* 背景网格线 */} + {/* Background grid lines */} - {/* 更抽象的折线 - 减小线宽和显示的细节 */} + {/* More abstract line - reduce line width and display details */} - {/* 简化的数据点 - 更小更精致 */} + {/* Simplified data points - smaller and more refined */} {points.map((point, index) => { const isLastPoint = index === points.length - 1; @@ -197,13 +199,13 @@ export function WeightHistoryCard() { - {/* 精简的图表信息 */} + {/* Concise chart information */} {userProfile.weight}kg - {sortedHistory.length}天 + {sortedHistory.length}{t('statistics.components.weight.days')} @@ -214,7 +216,7 @@ export function WeightHistoryCard() { )} - {/* BMI 信息弹窗 */} + {/* BMI information modal */} - {/* 标题 */} - BMI 指数说明 + {/* Title */} + {t('statistics.components.weight.bmiModal.title')} - {/* 介绍部分 */} + {/* Introduction section */} - BMI(身体质量指数)是评估体重与身高关系的国际通用健康指标 + {t('statistics.components.weight.bmiModal.description')} - 计算公式:体重(kg) ÷ 身高²(m) + {t('statistics.components.weight.bmiModal.formula')} - {/* BMI 分类标准 */} - BMI 分类标准 + {/* BMI classification standards */} + {t('statistics.components.weight.bmiModal.classificationTitle')} {BMI_CATEGORIES.map((category, index) => { const colors = [ - { bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 偏瘦 - { bg: '#E8F5E8', text: Colors.light.accentGreen, border: Colors.light.accentGreen }, // 正常 - { bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 超重 - { bg: '#FEE2E2', text: '#B91C1C', border: '#EF4444' } // 肥胖 + { bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // Underweight + { bg: '#E8F5E8', text: Colors.light.accentGreen, border: Colors.light.accentGreen }, // Normal + { bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // Overweight + { bg: '#FEE2E2', text: '#B91C1C', border: '#EF4444' } // Obese ][index]; return ( @@ -273,41 +275,41 @@ export function WeightHistoryCard() { })} - {/* 健康建议 */} - 健康建议 + {/* Health tips */} + {t('statistics.components.weight.bmiModal.healthTipsTitle')} - 保持均衡饮食,控制热量摄入 + {t('statistics.components.weight.bmiModal.tips.nutrition')} - 每周至少150分钟中等强度运动 + {t('statistics.components.weight.bmiModal.tips.exercise')} - 保证7-9小时充足睡眠 + {t('statistics.components.weight.bmiModal.tips.sleep')} - 定期监测体重变化,及时调整 + {t('statistics.components.weight.bmiModal.tips.monitoring')} - {/* 免责声明 */} + {/* Disclaimer */} - BMI 仅供参考,不能反映肌肉量、骨密度等指标。如有健康疑问,请咨询专业医生。 + {t('statistics.components.weight.bmiModal.disclaimer')} - {/* 底部继续按钮 */} + {/* Bottom continue button */} - 继续 + {t('statistics.components.weight.bmiModal.continueButton')} @@ -429,7 +431,7 @@ const styles = StyleSheet.create({ color: '#192126', }, - // BMI 弹窗样式 + // BMI modal styles bmiModalContainer: { flex: 1, }, diff --git a/hooks/useI18n.ts b/hooks/useI18n.ts new file mode 100644 index 0000000..055ecf0 --- /dev/null +++ b/hooks/useI18n.ts @@ -0,0 +1,10 @@ +import { useTranslation } from 'react-i18next'; + +export const useI18n = () => { + const { t, i18n } = useTranslation(); + + return { + t, + i18n, + }; +}; \ No newline at end of file diff --git a/i18n/index.ts b/i18n/index.ts index e7c5c3e..86ed8ec 100644 --- a/i18n/index.ts +++ b/i18n/index.ts @@ -69,6 +69,104 @@ const personalScreenResources = { }, }; +const editProfileResources = { + title: '编辑资料', + fields: { + name: '昵称', + gender: '性别', + height: '身高', + weight: '体重', + activityLevel: '活动水平', + birthDate: '出生日期', + maxHeartRate: '最大心率', + }, + gender: { + male: '男', + female: '女', + notSet: '未设置', + }, + height: { + unit: '厘米', + placeholder: '170厘米', + }, + weight: { + unit: '公斤', + placeholder: '55公斤', + }, + activityLevels: { + 1: '久坐', + 2: '轻度活跃', + 3: '中度活跃', + 4: '非常活跃', + descriptions: { + 1: '很少运动', + 2: '每周1-3次运动', + 3: '每周3-5次运动', + 4: '每周6-7次运动', + }, + }, + birthDate: { + placeholder: '1995年1月1日', + format: '{{year}}年{{month}}月{{day}}日', + }, + maxHeartRate: { + unit: '次/分钟', + notAvailable: '未获取', + alert: { + title: '提示', + message: '最大心率数据从健康应用自动获取', + }, + }, + alerts: { + notLoggedIn: { + title: '未登录', + message: '请先登录后再尝试保存', + }, + saveFailed: { + title: '保存失败', + message: '请稍后重试', + }, + avatarPermissions: { + title: '权限不足', + message: '需要相册权限以选择头像', + }, + avatarUploadFailed: { + title: '上传失败', + message: '头像上传失败,请重试', + }, + avatarError: { + title: '发生错误', + message: '选择头像失败,请重试', + }, + avatarSuccess: { + title: '成功', + message: '头像更新成功', + }, + }, + modals: { + cancel: '取消', + confirm: '确定', + save: '保存', + input: { + namePlaceholder: '输入昵称', + weightPlaceholder: '输入体重', + weightUnit: '公斤 (kg)', + }, + selectHeight: '选择身高', + selectGender: '选择性别', + selectActivityLevel: '选择活动水平', + female: '女性', + male: '男性', + }, + defaultValues: { + name: '今晚要吃肉', + height: 170, + weight: 55, + birthDate: '1995-01-01', + activityLevel: 1, + }, +}; + const healthPermissionsResources = { title: '健康数据授权说明', subtitle: '我们通过 Apple Health 的 HealthKit/CareKit 接口同步必要的数据,让训练、恢复和提醒更贴合你的身体状态。', @@ -121,11 +219,538 @@ const healthPermissionsResources = { }, }; +const statisticsResources = { + title: 'Out Live', + sections: { + bodyMetrics: '身体指标', + }, + components: { + diet: { + title: '饮食分析', + loading: '加载中...', + updated: '更新: {{time}}', + remaining: '还能吃', + calories: '热量', + protein: '蛋白质', + carb: '碳水', + fat: '脂肪', + fiber: '纤维', + sodium: '钠', + basal: '基代', + exercise: '运动', + diet: '饮食', + kcal: '千卡', + aiRecognition: 'AI识别', + foodLibrary: '食物库', + voiceRecord: '一句话记录', + nutritionLabel: '成分表分析', + }, + fitness: { + kcal: '千卡', + minutes: '分钟', + hours: '小时', + }, + steps: { + title: '步数', + }, + mood: { + title: '心情', + empty: '点击记录心情', + }, + stress: { + title: '压力', + unit: 'ms', + }, + water: { + title: '喝水', + unit: 'ml', + addButton: '+ {{amount}}ml', + }, + metabolism: { + title: '基础代谢', + loading: '加载中...', + unit: '千卡/日', + status: { + high: '高代谢', + normal: '正常', + low: '偏低', + veryLow: '较低', + unknown: '未知', + }, + }, + sleep: { + title: '睡眠', + loading: '加载中...', + }, + oxygen: { + title: '血氧饱和度', + }, + circumference: { + title: '围度 (cm)', + setTitle: '设置{{label}}', + confirm: '确认', + measurements: { + chest: '胸围', + waist: '腰围', + hip: '上臀围', + arm: '臂围', + thigh: '大腿围', + calf: '小腿围', + }, + }, + workout: { + title: '近期锻炼', + minutes: '分钟', + kcal: '千卡', + noData: '尚无锻炼数据', + syncing: '等待同步', + sourceWaiting: '来源:等待同步', + sourceUnknown: '来源:未知', + sourceFormat: '来源:{{source}}', + sourceFormatMultiple: '来源:{{source}} 等', + lastWorkout: '最近锻炼', + updated: '更新', + }, + weight: { + title: '体重记录', + addButton: '记录体重', + bmi: 'BMI', + weight: '体重', + days: '天', + range: '范围', + unit: 'kg', + bmiModal: { + title: 'BMI 指数说明', + description: 'BMI(身体质量指数)是评估体重与身高关系的国际通用健康指标', + formula: '计算公式:体重(kg) ÷ 身高²(m)', + classificationTitle: 'BMI 分类标准', + healthTipsTitle: '健康建议', + tips: { + nutrition: '保持均衡饮食,控制热量摄入', + exercise: '每周至少150分钟中等强度运动', + sleep: '保证7-9小时充足睡眠', + monitoring: '定期监测体重变化,及时调整', + }, + disclaimer: 'BMI 仅供参考,不能反映肌肉量、骨密度等指标。如有健康疑问,请咨询专业医生。', + continueButton: '继续', + }, + }, + fitnessRings: { + title: '健身圆环', + activeCalories: '活动卡路里', + exerciseMinutes: '锻炼分钟', + standHours: '站立小时', + goal: '/{{goal}}', + ringLabels: { + active: '活动', + exercise: '锻炼', + stand: '站立', + }, + }, + }, + tabs: { + health: '健康', + medications: '用药', + fasting: '断食', + challenges: '挑战', + personal: '个人', + }, +}; + +const medicationsResources = { + greeting: '你好,{{name}}', + welcome: '欢迎来到用药助手!', + todayMedications: '今日用药', + filters: { + all: '全部', + taken: '已服用', + missed: '未服用', + }, + emptyState: { + title: '今日暂无用药安排', + subtitle: '还未添加任何用药计划,快来补充吧。', + }, + dateFormats: { + today: '今天,{{date}}', + other: '{{date}}', + }, + // MedicationCard 组件翻译 + card: { + status: { + missed: '已错过', + timeToTake: '到服药时间', + remaining: '剩余 {{time}}', + }, + action: { + takeNow: '立即服用', + taken: '已服用', + submitting: '提交中...', + }, + earlyTakeAlert: { + title: '尚未到服药时间', + message: '该用药计划在 {{time}},现在还早于1小时以上。\n\n是否确认已服用此药物?', + cancel: '取消', + confirm: '确认已服用', + }, + takeError: { + title: '操作失败', + message: '记录服药时发生错误,请稍后重试', + confirm: '确定', + }, + }, + // 添加药物页面翻译 + add: { + title: '添加药物', + steps: { + name: '药品名称', + dosage: '剂型与剂量', + frequency: '服药频率', + time: '服药时间', + note: '备注', + }, + descriptions: { + name: '为药物命名并上传包装照片,方便识别', + dosage: '选择药片类型并填写每次的用药剂量', + frequency: '设置用药频率以及每日次数', + time: '添加并管理每天的提醒时间', + note: '填写备注或医生叮嘱(可选)', + }, + name: { + placeholder: '输入或搜索药品名称', + }, + photo: { + title: '上传药品图片', + subtitle: '拍照或从相册选择,辅助识别药品包装', + selectTitle: '选择图片', + selectMessage: '请选择图片来源', + camera: '拍照', + album: '从相册选择', + cancel: '取消', + retake: '重新选择', + uploading: '上传中…', + uploadingText: '正在上传', + remove: '删除', + cameraPermission: '需要相机权限以拍摄药品照片', + albumPermission: '需要相册权限以选择药品照片', + uploadFailed: '上传失败', + uploadFailedMessage: '图片上传失败,请稍后重试', + cameraFailed: '拍照失败', + cameraFailedMessage: '无法打开相机,请稍后再试', + selectFailed: '选择失败', + selectFailedMessage: '无法打开相册,请稍后再试', + }, + dosage: { + label: '每次剂量', + placeholder: '0.5', + type: '类型', + unitSelector: '选择剂量单位', + }, + frequency: { + label: '每日次数', + value: '{{count}} 次/日', + period: '用药周期', + start: '开始', + end: '结束', + longTerm: '长期', + startDateInvalid: '日期无效', + startDateInvalidMessage: '开始日期不能早于今天', + endDateInvalid: '日期无效', + endDateInvalidMessage: '结束日期不能早于开始日期', + }, + time: { + label: '每日提醒时间', + addTime: '添加时间', + editTime: '修改提醒时间', + addTimeButton: '添加时间', + }, + note: { + label: '备注', + placeholder: '记录注意事项、医生叮嘱或自定义提醒', + voiceNotSupported: '当前设备暂不支持语音转文字,可直接输入备注', + voiceError: '语音识别不可用', + voiceErrorMessage: '无法使用语音输入,请检查权限设置后重试', + voiceStartError: '无法启动语音输入', + voiceStartErrorMessage: '请检查麦克风与语音识别权限后重试', + }, + actions: { + previous: '上一步', + next: '下一步', + complete: '完成', + }, + success: { + title: '添加成功', + message: '已成功添加药物"{{name}}"', + confirm: '确定', + }, + error: { + title: '添加失败', + message: '创建药物时发生错误,请稍后重试', + confirm: '确定', + }, + datePickers: { + startDate: '选择开始日期', + endDate: '选择结束日期', + time: '选择时间', + cancel: '取消', + confirm: '确定', + }, + pickers: { + timesPerDay: '选择每日次数', + dosageUnit: '选择剂量单位', + cancel: '取消', + confirm: '确定', + }, + }, + // 药物管理页面翻译 + manage: { + title: '药品管理', + subtitle: '管理所有药品的状态与提醒', + filters: { + all: '全部', + active: '进行中', + inactive: '已停用', + }, + loading: '正在载入药品信息...', + empty: { + title: '暂无药品', + subtitle: '还没有相关药品记录,点击右上角添加', + }, + deactivate: { + title: '停用 {{name}}?', + description: '停用后,当天已生成的用药计划会一并删除,且无法恢复。', + confirm: '确认停用', + cancel: '取消', + error: { + title: '操作失败', + message: '停用药物时发生问题,请稍后重试。', + }, + }, + toggleError: { + title: '操作失败', + message: '切换药物状态时发生问题,请稍后重试。', + }, + formLabels: { + capsule: '胶囊', + pill: '药片', + injection: '注射', + spray: '喷雾', + drop: '滴剂', + syrup: '糖浆', + other: '其他', + }, + frequency: { + daily: '每日', + weekly: '每周', + custom: '自定义', + }, + cardMeta: '开始于 {{date}} | 提醒:{{reminder}}', + reminderNotSet: '尚未设置', + unknownDate: '未知日期', + }, + // 药物详情页面翻译 + detail: { + title: '药品详情', + notFound: { + title: '未找到药品信息', + subtitle: '请从用药列表重新进入此页面。', + }, + loading: '正在载入...', + error: { + title: '暂时无法获取该药品的信息,请稍后重试。', + subtitle: '请检查网络后重试,或返回上一页。', + }, + sections: { + plan: '服药计划', + dosage: '剂量与形式', + note: '备注', + overview: '服药概览', + aiAnalysis: 'AI 用药分析', + }, + plan: { + period: '服药周期', + time: '用药时间', + frequency: '频率', + longTerm: '长期', + periodMessage: '开始服药日期:{{startDate}}\n{{endDateInfo}}', + longTermPlan: '服药计划:长期服药', + timeMessage: '设置的时间:{{times}}', + }, + dosage: { + label: '每次剂量', + form: '剂型', + selectDosage: '选择剂量', + selectForm: '选择剂型', + dosageValue: '剂量值', + unit: '单位', + }, + note: { + label: '药品备注', + placeholder: '记录注意事项、医生叮嘱或自定义提醒', + edit: '编辑备注', + noNote: '暂无备注信息', + voiceNotSupported: '当前设备暂不支持语音转文字,可直接输入备注', + save: '保存', + saveError: { + title: '保存失败', + message: '提交备注时出现问题,请稍后重试。', + }, + }, + overview: { + calculating: '统计中...', + takenCount: '累计服药 {{count}} 次', + calculatingDays: '正在计算坚持天数', + startedDays: '已坚持 {{days}} 天', + startDate: '开始于 {{date}}', + noStartDate: '暂无开始日期', + }, + aiAnalysis: { + analyzing: '正在分析用药信息...', + analyzingButton: '分析中...', + button: 'AI 分析', + error: { + title: '分析失败', + message: 'AI 分析失败,请稍后重试', + networkError: '发起分析请求失败,请检查网络连接', + unauthorized: '请先登录', + forbidden: '无权访问此药物', + notFound: '药物不存在', + }, + }, + status: { + enabled: '提醒已开启', + disabled: '提醒已关闭', + }, + delete: { + title: '删除 {{name}}?', + description: '删除后将清除与该药品相关的提醒与历史记录,且无法恢复。', + confirm: '删除', + cancel: '取消', + error: { + title: '删除失败', + message: '移除该药品时出现问题,请稍后再试。', + }, + }, + deactivate: { + title: '停用 {{name}}?', + description: '停用后,当天已生成的用药计划会一并删除,且无法恢复。', + confirm: '确认停用', + cancel: '取消', + error: { + title: '操作失败', + message: '停用药物时发生问题,请稍后重试。', + }, + }, + toggleError: { + title: '操作失败', + message: '切换提醒状态时出现问题,请稍后重试。', + }, + updateErrors: { + dosage: '更新失败', + dosageMessage: '更新剂量时出现问题,请稍后重试。', + form: '更新失败', + formMessage: '更新剂型时出现问题,请稍后重试。', + }, + imageViewer: { + close: '关闭', + }, + pickers: { + cancel: '取消', + confirm: '确定', + }, + }, + // 编辑频率页面翻译 + editFrequency: { + title: '编辑服药频率', + missingParams: '缺少必要参数', + medicationName: '正在编辑:{{name}}', + sections: { + frequency: '服药频率', + frequencyDescription: '设置每日服药次数', + time: '每日提醒时间', + timeDescription: '添加并管理每天的提醒时间', + }, + frequency: { + repeatPattern: '重复模式', + timesPerDay: '每日次数', + daily: '每日', + weekly: '每周', + custom: '自定义', + timesLabel: '{{count}} 次', + summary: '{{pattern}} {{count}} 次', + }, + time: { + addTime: '添加时间', + editTime: '修改提醒时间', + addTimeButton: '添加时间', + }, + actions: { + save: '保存修改', + }, + error: { + title: '更新失败', + message: '更新服药频率时出现问题,请稍后重试。', + }, + pickers: { + cancel: '取消', + confirm: '确定', + }, + }, +}; + +const notificationSettingsResources = { + title: '通知设置', + loading: '加载中...', + sections: { + notifications: '通知设置', + medicationReminder: '药品提醒', + description: '说明', + }, + items: { + pushNotifications: { + title: '消息推送', + description: '开启后将接收应用通知', + }, + medicationReminder: { + title: '药品通知提醒', + description: '在用药时间接收提醒通知', + }, + }, + description: { + text: '• 消息推送是所有通知的总开关\n• 药品通知提醒需要在消息推送开启后才能使用\n• 您可以在系统设置中管理通知权限\n• 关闭消息推送将停止所有应用通知', + }, + alerts: { + permissionDenied: { + title: '权限被拒绝', + message: '请在系统设置中开启通知权限,然后再尝试开启推送功能', + cancel: '取消', + goToSettings: '去设置', + }, + error: { + title: '错误', + message: '请求通知权限失败', + saveFailed: '保存设置失败', + medicationReminderFailed: '设置药品提醒失败', + }, + notificationsEnabled: { + title: '通知已开启', + body: '您将收到应用通知和提醒', + }, + medicationReminderEnabled: { + title: '药品提醒已开启', + body: '您将在用药时间收到提醒通知', + }, + }, +}; + const resources = { zh: { translation: { personal: personalScreenResources, + editProfile: editProfileResources, healthPermissions: healthPermissionsResources, + statistics: statisticsResources, + medications: medicationsResources, + notificationSettings: notificationSettingsResources, }, }, en: { @@ -188,6 +813,103 @@ const resources = { }, }, }, + editProfile: { + title: 'Edit Profile', + fields: { + name: 'Nickname', + gender: 'Gender', + height: 'Height', + weight: 'Weight', + activityLevel: 'Activity Level', + birthDate: 'Birth Date', + maxHeartRate: 'Max Heart Rate', + }, + gender: { + male: 'Male', + female: 'Female', + notSet: 'Not set', + }, + height: { + unit: 'cm', + placeholder: '170cm', + }, + weight: { + unit: 'kg', + placeholder: '55kg', + }, + activityLevels: { + 1: 'Sedentary', + 2: 'Lightly active', + 3: 'Moderately active', + 4: 'Very active', + descriptions: { + 1: 'Rarely exercise', + 2: 'Exercise 1-3 times per week', + 3: 'Exercise 3-5 times per week', + 4: 'Exercise 6-7 times per week', + }, + }, + birthDate: { + placeholder: 'January 1, 1995', + format: '{{month}} {{day}}, {{year}}', + }, + maxHeartRate: { + unit: 'bpm', + notAvailable: 'Not available', + alert: { + title: 'Notice', + message: 'Max heart rate data is automatically retrieved from Health app', + }, + }, + alerts: { + notLoggedIn: { + title: 'Not logged in', + message: 'Please log in before trying to save', + }, + saveFailed: { + title: 'Save failed', + message: 'Please try again later', + }, + avatarPermissions: { + title: 'Insufficient permissions', + message: 'Photo album permission is required to select avatar', + }, + avatarUploadFailed: { + title: 'Upload failed', + message: 'Avatar upload failed, please try again', + }, + avatarError: { + title: 'Error occurred', + message: 'Failed to select avatar, please try again', + }, + avatarSuccess: { + title: 'Success', + message: 'Avatar updated successfully', + }, + }, + modals: { + cancel: 'Cancel', + confirm: 'Confirm', + save: 'Save', + input: { + namePlaceholder: 'Enter nickname', + weightPlaceholder: 'Enter weight', + weightUnit: 'kg', + }, + selectHeight: 'Select Height', + selectGender: 'Select Gender', + selectActivityLevel: 'Select Activity Level', + female: 'Female', + male: 'Male', + }, + defaultValues: { + name: 'TonightEatMeat', + height: 170, + weight: 55, + birthDate: '1995-01-01', + activityLevel: 1, + }, + }, healthPermissions: { title: 'Health data disclosure', subtitle: 'We integrate with Apple Health through HealthKit and CareKit to deliver precise training, recovery, and reminder experiences.', @@ -222,7 +944,7 @@ const resources = { items: [ 'Health data stays on your device — we do not upload it or share it with third parties.', 'Only aggregated, anonymized stats are synced when absolutely necessary.', - 'We follow Apple’s review requirements and will notify you before any changes.', + "We follow Apple's review requirements and will notify you before any changes.", ], }, }, @@ -239,6 +961,526 @@ const resources = { email: 'richardwei1995@gmail.com', }, }, + statistics: { + title: 'Out Live', + sections: { + bodyMetrics: 'Body Metrics', + }, + components: { + diet: { + title: 'Diet Analysis', + loading: 'Loading...', + updated: 'Updated: {{time}}', + remaining: 'Can Still Eat', + calories: 'Calories', + protein: 'Protein', + carb: 'Carbs', + fat: 'Fat', + fiber: 'Fiber', + sodium: 'Sodium', + basal: 'Basal', + exercise: 'Exercise', + diet: 'Diet', + kcal: 'kcal', + aiRecognition: 'AI Scan', + foodLibrary: 'Food Library', + voiceRecord: 'Voice Log', + nutritionLabel: 'Nutrition Label', + }, + fitness: { + kcal: 'kcal', + minutes: 'min', + hours: 'hrs', + }, + steps: { + title: 'Steps', + }, + mood: { + title: 'Mood', + empty: 'Tap to record mood', + }, + stress: { + title: 'Stress', + unit: 'ms', + }, + water: { + title: 'Water', + unit: 'ml', + addButton: '+ {{amount}}ml', + }, + metabolism: { + title: 'Basal Metabolism', + loading: 'Loading...', + unit: 'kcal/day', + status: { + high: 'High', + normal: 'Normal', + low: 'Low', + veryLow: 'Very Low', + unknown: 'Unknown', + }, + }, + sleep: { + title: 'Sleep', + loading: 'Loading...', + }, + oxygen: { + title: 'Blood Oxygen', + }, + circumference: { + title: 'Circumference (cm)', + setTitle: 'Set {{label}}', + confirm: 'Confirm', + measurements: { + chest: 'Chest', + waist: 'Waist', + hip: 'Hip', + arm: 'Arm', + thigh: 'Thigh', + calf: 'Calf', + }, + }, + workout: { + title: 'Recent Workout', + minutes: 'min', + kcal: 'kcal', + noData: 'No workout data', + syncing: 'Syncing...', + sourceWaiting: 'Source: Syncing...', + sourceUnknown: 'Source: Unknown', + sourceFormat: 'Source: {{source}}', + sourceFormatMultiple: 'Source: {{source}} et al.', + lastWorkout: 'Latest Workout', + updated: 'Updated', + }, + weight: { + title: 'Weight Records', + addButton: 'Record Weight', + bmi: 'BMI', + weight: 'Weight', + days: 'days', + range: 'Range', + unit: 'kg', + bmiModal: { + title: 'BMI Index Explanation', + description: 'BMI (Body Mass Index) is an internationally recognized health indicator for assessing weight relative to height', + formula: 'Formula: weight(kg) ÷ height²(m)', + classificationTitle: 'BMI Classification Standards', + healthTipsTitle: 'Health Tips', + tips: { + nutrition: 'Maintain a balanced diet and control calorie intake', + exercise: 'At least 150 minutes of moderate-intensity exercise per week', + sleep: 'Ensure 7-9 hours of adequate sleep', + monitoring: 'Regularly monitor weight changes and adjust promptly', + }, + disclaimer: 'BMI is for reference only and cannot reflect muscle mass, bone density, etc. If you have health concerns, please consult a professional doctor.', + continueButton: 'Continue', + }, + }, + fitnessRings: { + title: 'Fitness Rings', + activeCalories: 'Active Calories', + exerciseMinutes: 'Exercise Minutes', + standHours: 'Stand Hours', + goal: '/{{goal}}', + ringLabels: { + active: 'Active', + exercise: 'Exercise', + stand: 'Stand', + }, + }, + }, + tabs: { + health: 'Health', + medications: 'Meds', + fasting: 'Fasting', + challenges: 'Challenges', + personal: 'Me', + }, + }, + medications: { + greeting: 'Hello, {{name}}', + welcome: 'Welcome to Medication Assistant!', + todayMedications: 'Today\'s Medications', + filters: { + all: 'All', + taken: 'Taken', + missed: 'Missed', + }, + emptyState: { + title: 'No medications scheduled for today', + subtitle: 'No medication plans added yet. Let\'s add some.', + }, + dateFormats: { + today: 'Today, {{date}}', + other: '{{date}}', + }, + // MedicationCard 组件翻译 + card: { + status: { + missed: 'Missed', + timeToTake: 'Time to take', + remaining: '{{time}} remaining', + }, + action: { + takeNow: 'Take Now', + taken: 'Taken', + submitting: 'Submitting...', + }, + earlyTakeAlert: { + title: 'Not yet time to take medication', + message: 'This medication is scheduled for {{time}}, which is more than 1 hour from now.\n\nHave you already taken this medication?', + cancel: 'Cancel', + confirm: 'Confirm Taken', + }, + takeError: { + title: 'Operation Failed', + message: 'An error occurred while recording medication, please try again later', + confirm: 'OK', + }, + }, + // 添加药物页面翻译 + add: { + title: 'Add Medication', + steps: { + name: 'Medication Name', + dosage: 'Dosage & Form', + frequency: 'Frequency', + time: 'Reminder Time', + note: 'Notes', + }, + descriptions: { + name: 'Name the medication and upload package photo for easy identification', + dosage: 'Select tablet type and fill in dosage per administration', + frequency: 'Set medication frequency and daily times', + time: 'Add and manage daily reminder times', + note: 'Fill in notes or doctor instructions (optional)', + }, + name: { + placeholder: 'Enter or search medication name', + }, + photo: { + title: 'Upload Medication Photo', + subtitle: 'Take a photo or select from album to help identify medication packaging', + selectTitle: 'Select Image', + selectMessage: 'Please select image source', + camera: 'Camera', + album: 'From Album', + cancel: 'Cancel', + retake: 'Retake', + uploading: 'Uploading...', + uploadingText: 'Uploading', + remove: 'Remove', + cameraPermission: 'Camera permission is required to take medication photos', + albumPermission: 'Album permission is required to select medication photos', + uploadFailed: 'Upload Failed', + uploadFailedMessage: 'Image upload failed, please try again later', + cameraFailed: 'Camera Failed', + cameraFailedMessage: 'Unable to open camera, please try again later', + selectFailed: 'Selection Failed', + selectFailedMessage: 'Unable to open album, please try again later', + }, + dosage: { + label: 'Dosage per administration', + placeholder: '0.5', + type: 'Type', + unitSelector: 'Select dosage unit', + }, + frequency: { + label: 'Times per day', + value: '{{count}} times/day', + period: 'Medication period', + start: 'Start', + end: 'End', + longTerm: 'Long-term', + startDateInvalid: 'Invalid date', + startDateInvalidMessage: 'Start date cannot be earlier than today', + endDateInvalid: 'Invalid date', + endDateInvalidMessage: 'End date cannot be earlier than start date', + }, + time: { + label: 'Daily reminder times', + addTime: 'Add Time', + editTime: 'Edit Reminder Time', + addTimeButton: 'Add Time', + }, + note: { + label: 'Notes', + placeholder: 'Record precautions, doctor instructions or custom reminders', + voiceNotSupported: 'Voice-to-text is not supported on this device, you can type notes directly', + voiceError: 'Voice recognition unavailable', + voiceErrorMessage: 'Unable to use voice input, please check permission settings and try again', + voiceStartError: 'Unable to start voice input', + voiceStartErrorMessage: 'Please check microphone and voice recognition permissions and try again', + }, + actions: { + previous: 'Previous', + next: 'Next', + complete: 'Complete', + }, + success: { + title: 'Added Successfully', + message: 'Successfully added medication "{{name}}"', + confirm: 'OK', + }, + error: { + title: 'Add Failed', + message: 'An error occurred while creating medication, please try again later', + confirm: 'OK', + }, + datePickers: { + startDate: 'Select Start Date', + endDate: 'Select End Date', + time: 'Select Time', + cancel: 'Cancel', + confirm: 'Confirm', + }, + pickers: { + timesPerDay: 'Select Times Per Day', + dosageUnit: 'Select Dosage Unit', + cancel: 'Cancel', + confirm: 'Confirm', + }, + }, + // 药物管理页面翻译 + manage: { + title: 'Medication Management', + subtitle: 'Manage status and reminders for all medications', + filters: { + all: 'All', + active: 'Active', + inactive: 'Inactive', + }, + loading: 'Loading medication information...', + empty: { + title: 'No Medications', + subtitle: 'No medication records yet, click the top right to add', + }, + deactivate: { + title: 'Deactivate {{name}}?', + description: 'After deactivation, medication plans generated for the day will be deleted and cannot be recovered.', + confirm: 'Confirm Deactivation', + cancel: 'Cancel', + error: { + title: 'Operation Failed', + message: 'An error occurred while deactivating medication, please try again later.', + }, + }, + toggleError: { + title: 'Operation Failed', + message: 'An error occurred while toggling medication status, please try again later.', + }, + formLabels: { + capsule: 'Capsule', + pill: 'Tablet', + injection: 'Injection', + spray: 'Spray', + drop: 'Drops', + syrup: 'Syrup', + other: 'Other', + }, + frequency: { + daily: 'Daily', + weekly: 'Weekly', + custom: 'Custom', + }, + cardMeta: 'Started {{date}} | Reminder: {{reminder}}', + reminderNotSet: 'Not set', + unknownDate: 'Unknown date', + }, + // 药物详情页面翻译 + detail: { + title: 'Medication Details', + notFound: { + title: 'Medication information not found', + subtitle: 'Please re-enter this page from the medication list.', + }, + loading: 'Loading...', + error: { + title: 'Unable to retrieve medication information at this time, please try again later.', + subtitle: 'Please check your network and try again, or return to the previous page.', + }, + sections: { + plan: 'Medication Plan', + dosage: 'Dosage & Form', + note: 'Notes', + overview: 'Medication Overview', + aiAnalysis: 'AI Medication Analysis', + }, + plan: { + period: 'Medication Period', + time: 'Medication Time', + frequency: 'Frequency', + longTerm: 'Long-term', + periodMessage: 'Start date: {{startDate}}\n{{endDateInfo}}', + longTermPlan: 'Medication plan: Long-term medication', + timeMessage: 'Set times: {{times}}', + }, + dosage: { + label: 'Dosage per administration', + form: 'Form', + selectDosage: 'Select Dosage', + selectForm: 'Select Form', + dosageValue: 'Dosage Value', + unit: 'Unit', + }, + note: { + label: 'Medication Notes', + placeholder: 'Record precautions, doctor instructions or custom reminders', + edit: 'Edit Notes', + noNote: 'No notes', + voiceNotSupported: 'Voice-to-text is not supported on this device, you can type notes directly', + save: 'Save', + saveError: { + title: 'Save Failed', + message: 'An error occurred while submitting notes, please try again later.', + }, + }, + overview: { + calculating: 'Calculating...', + takenCount: 'Taken {{count}} times in total', + calculatingDays: 'Calculating adherence days', + startedDays: 'Adhered for {{days}} days', + startDate: 'Started {{date}}', + noStartDate: 'No start date', + }, + aiAnalysis: { + analyzing: 'Analyzing medication information...', + analyzingButton: 'Analyzing...', + button: 'AI Analysis', + error: { + title: 'Analysis Failed', + message: 'AI analysis failed, please try again later', + networkError: 'Failed to initiate analysis request, please check network connection', + unauthorized: 'Please log in first', + forbidden: 'No access to this medication', + notFound: 'Medication not found', + }, + }, + status: { + enabled: 'Reminders Enabled', + disabled: 'Reminders Disabled', + }, + delete: { + title: 'Delete {{name}}?', + description: 'After deletion, reminders and history related to this medication will be cleared and cannot be recovered.', + confirm: 'Delete', + cancel: 'Cancel', + error: { + title: 'Delete Failed', + message: 'An error occurred while removing this medication, please try again later.', + }, + }, + deactivate: { + title: 'Deactivate {{name}}?', + description: 'After deactivation, medication plans generated for the day will be deleted and cannot be recovered.', + confirm: 'Confirm Deactivation', + cancel: 'Cancel', + error: { + title: 'Operation Failed', + message: 'An error occurred while deactivating medication, please try again later.', + }, + }, + toggleError: { + title: 'Operation Failed', + message: 'An error occurred while toggling reminder status, please try again later.', + }, + updateErrors: { + dosage: 'Update Failed', + dosageMessage: 'An error occurred while updating dosage, please try again later.', + form: 'Update Failed', + formMessage: 'An error occurred while updating form, please try again later.', + }, + imageViewer: { + close: 'Close', + }, + pickers: { + cancel: 'Cancel', + confirm: 'Confirm', + }, + }, + // 编辑频率页面翻译 + editFrequency: { + title: 'Edit Medication Frequency', + missingParams: 'Missing required parameters', + medicationName: 'Editing: {{name}}', + sections: { + frequency: 'Medication Frequency', + frequencyDescription: 'Set daily medication frequency', + time: 'Daily Reminder Times', + timeDescription: 'Add and manage daily reminder times', + }, + frequency: { + repeatPattern: 'Repeat Pattern', + timesPerDay: 'Times Per Day', + daily: 'Daily', + weekly: 'Weekly', + custom: 'Custom', + timesLabel: '{{count}} times', + summary: '{{pattern}} {{count}} times', + }, + time: { + addTime: 'Add Time', + editTime: 'Edit Reminder Time', + addTimeButton: 'Add Time', + }, + actions: { + save: 'Save Changes', + }, + error: { + title: 'Update Failed', + message: 'An error occurred while updating medication frequency, please try again later.', + }, + pickers: { + cancel: 'Cancel', + confirm: 'Confirm', + }, + }, + }, + notificationSettings: { + title: 'Notification Settings', + loading: 'Loading...', + sections: { + notifications: 'Notification Settings', + medicationReminder: 'Medication Reminder', + description: 'Description', + }, + items: { + pushNotifications: { + title: 'Push Notifications', + description: 'Receive app notifications when enabled', + }, + medicationReminder: { + title: 'Medication Reminder', + description: 'Receive reminder notifications at medication time', + }, + }, + description: { + text: '• Push notifications is the master switch for all notifications\n• Medication reminder requires push notifications to be enabled\n• You can manage notification permissions in system settings\n• Disabling push notifications will stop all app notifications', + }, + alerts: { + permissionDenied: { + title: 'Permission Denied', + message: 'Please enable notification permission in system settings, then try to enable push notifications', + cancel: 'Cancel', + goToSettings: 'Go to Settings', + }, + error: { + title: 'Error', + message: 'Failed to request notification permission', + saveFailed: 'Failed to save settings', + medicationReminderFailed: 'Failed to set medication reminder', + }, + notificationsEnabled: { + title: 'Notifications Enabled', + body: 'You will receive app notifications and reminders', + }, + medicationReminderEnabled: { + title: 'Medication Reminder Enabled', + body: 'You will receive reminder notifications at medication time', + }, + }, + }, }, }, };