feat(i18n): 全面实现应用核心功能模块的国际化支持

- 新增 i18n 翻译资源,覆盖睡眠、饮水、体重、锻炼、用药 AI 识别、步数、健身圆环、基础代谢及设置等核心模块
- 重构相关页面及组件(如 SleepDetail, WaterDetail, WorkoutHistory 等)使用 `useI18n` 钩子替换硬编码文本
- 升级 `utils/date` 工具库与 `DateSelector` 组件,支持基于语言环境的日期格式化与显示
- 完善登录页、注销流程及权限申请弹窗的双语提示信息
- 优化部分页面的 UI 细节与字体样式以适配多语言显示
This commit is contained in:
richarjiang
2025-11-27 17:54:36 +08:00
parent 08adf0f20d
commit fbe0c92f0f
26 changed files with 2508 additions and 1622 deletions

View File

@@ -16,6 +16,7 @@ import {
import { HeaderBar } from '@/components/ui/HeaderBar';
import { IntensityBadge, WorkoutDetailModal } from '@/components/workout/WorkoutDetailModal';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { getWorkoutDetailMetrics, WorkoutDetailMetrics } from '@/services/workoutDetail';
import {
@@ -233,23 +234,23 @@ function computeMonthlyStats(workouts: WorkoutData[]): MonthlyStatsInfo | null {
};
}
function getIntensityBadge(totalCalories?: number, durationInSeconds?: number) {
function getIntensityBadge(t: (key: string, options?: any) => string, totalCalories?: number, durationInSeconds?: number): { label: string; color: string; background: string } {
if (!totalCalories || !durationInSeconds) {
return { label: '低强度', color: '#7C85A3', background: '#E4E7F2' };
return { label: t('workoutHistory.intensity.low'), color: '#7C85A3', background: '#E4E7F2' };
}
const minutes = Math.max(durationInSeconds / 60, 1);
const caloriesPerMinute = totalCalories / minutes;
if (caloriesPerMinute >= 9) {
return { label: '高强度', color: '#F85959', background: '#FFE6E6' };
return { label: t('workoutHistory.intensity.high'), color: '#F85959', background: '#FFE6E6' };
}
if (caloriesPerMinute >= 5) {
return { label: '中强度', color: '#0EAF71', background: '#E4F6EF' };
return { label: t('workoutHistory.intensity.medium'), color: '#0EAF71', background: '#E4F6EF' };
}
return { label: '低强度', color: '#5966FF', background: '#E7EBFF' };
return { label: t('workoutHistory.intensity.low'), color: '#5966FF', background: '#E7EBFF' };
}
function groupWorkouts(workouts: WorkoutData[]): WorkoutSection[] {
@@ -265,13 +266,14 @@ function groupWorkouts(workouts: WorkoutData[]): WorkoutSection[] {
return Object.keys(grouped)
.sort((a, b) => dayjs(b).valueOf() - dayjs(a).valueOf())
.map((dateKey) => ({
title: dayjs(dateKey).format('M月D日'),
title: dayjs(dateKey).format('M月D日'), // 保持中文格式,因为这是日期格式
data: grouped[dateKey]
.sort((a, b) => dayjs(b.startDate || b.endDate).valueOf() - dayjs(a.startDate || a.endDate).valueOf()),
}));
}
export default function WorkoutHistoryScreen() {
const { t } = useI18n();
const [sections, setSections] = useState<WorkoutSection[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -284,7 +286,7 @@ export default function WorkoutHistoryScreen() {
const [monthOccurrenceText, setMonthOccurrenceText] = useState<string | null>(null);
const [monthlyStats, setMonthlyStats] = useState<MonthlyStatsInfo | null>(null);
const safeAreaTop = useSafeAreaTop()
const safeAreaTop = useSafeAreaTop();
const loadHistory = useCallback(async () => {
setIsLoading(true);
@@ -302,7 +304,7 @@ export default function WorkoutHistoryScreen() {
if (!hasPermission) {
setSections([]);
setError('尚未授予健康数据权限');
setError(t('workoutHistory.error.permissionDenied'));
setMonthlyStats(null);
return;
}
@@ -315,8 +317,8 @@ export default function WorkoutHistoryScreen() {
setMonthlyStats(computeMonthlyStats(filteredWorkouts));
setSections(groupWorkouts(filteredWorkouts));
} catch (err) {
console.error('加载锻炼历史失败:', err);
setError('加载锻炼记录失败,请稍后再试');
console.error('Failed to load workout history:', err);
setError(t('workoutHistory.error.loadFailed'));
setSections([]);
setMonthlyStats(null);
} finally {
@@ -350,9 +352,9 @@ export default function WorkoutHistoryScreen() {
? dayjs(monthlyStats.snapshotDate).format('M月D日')
: dayjs().format('M月D日');
const overviewText = monthlyStats
? `截至${snapshotLabel},你已完成${monthlyStats.totalCount}次锻炼,累计${formatDurationShort(monthlyStats.totalDuration)}`
: '本月还没有锻炼记录,动起来收集第一条吧!';
const periodText = `统计周期1日 - ${monthEndDay}日(本月)`;
? t('workoutHistory.monthlyStats.overviewWithStats', { date: snapshotLabel, count: monthlyStats.totalCount, duration: formatDurationShort(monthlyStats.totalDuration) })
: t('workoutHistory.monthlyStats.overviewEmpty');
const periodText = t('workoutHistory.monthlyStats.periodText', { day: monthEndDay });
const maxDuration = statsItems[0]?.duration || 1;
return (
@@ -369,7 +371,7 @@ export default function WorkoutHistoryScreen() {
end={{ x: 1, y: 1 }}
style={styles.monthlyStatsCard}
>
<Text style={styles.statSectionLabel}></Text>
<Text style={styles.statSectionLabel}>{t('workoutHistory.monthlyStats.title')}</Text>
<Text style={styles.statPeriodText}>{periodText}</Text>
<Text style={styles.statDescription}>{overviewText}</Text>
@@ -403,7 +405,7 @@ export default function WorkoutHistoryScreen() {
) : (
<View style={styles.statEmptyState}>
<MaterialCommunityIcons name="calendar-blank" size={20} color="#7C85A3" />
<Text style={styles.statEmptyText}></Text>
<Text style={styles.statEmptyText}>{t('workoutHistory.monthlyStats.emptyData')}</Text>
</View>
)}
</LinearGradient>
@@ -416,8 +418,8 @@ export default function WorkoutHistoryScreen() {
const emptyComponent = useMemo(() => (
<View style={styles.emptyContainer}>
<MaterialCommunityIcons name="calendar-blank" size={40} color="#9AA4C4" />
<Text style={styles.emptyText}></Text>
<Text style={styles.emptySubText}></Text>
<Text style={styles.emptyText}>{t('workoutHistory.empty.title')}</Text>
<Text style={styles.emptySubText}>{t('workoutHistory.empty.subtitle')}</Text>
</View>
), []);
@@ -453,7 +455,7 @@ export default function WorkoutHistoryScreen() {
}
const activityLabel = getWorkoutTypeDisplayName(workout.workoutActivityType);
return `这是你${workoutDate.format('M月')}的第 ${index + 1}${activityLabel}`;
return t('workoutHistory.monthOccurrence', { month: workoutDate.format('M月'), index: index + 1, activity: activityLabel });
}, [sections]);
const loadWorkoutDetail = useCallback(async (workout: WorkoutData) => {
@@ -463,16 +465,16 @@ export default function WorkoutHistoryScreen() {
const metrics = await getWorkoutDetailMetrics(workout);
setDetailMetrics(metrics);
} catch (err) {
console.error('加载锻炼详情失败:', err);
console.error('Failed to load workout details:', err);
setDetailMetrics(null);
setDetailError('加载锻炼详情失败,请稍后再试');
setDetailError(t('workoutHistory.error.detailLoadFailed'));
} finally {
setDetailLoading(false);
}
}, []);
const handleWorkoutPress = useCallback((workout: WorkoutData) => {
const intensity = getIntensityBadge(workout.totalEnergyBurned, workout.duration || 0);
const intensity = getIntensityBadge(t, workout.totalEnergyBurned, workout.duration || 0);
setSelectedIntensity(intensity);
setSelectedWorkout(workout);
setDetailMetrics(null);
@@ -495,7 +497,7 @@ export default function WorkoutHistoryScreen() {
const renderItem = useCallback(({ item }: { item: WorkoutData }) => {
const calories = Math.round(item.totalEnergyBurned || 0);
const minutes = Math.max(Math.round((item.duration || 0) / 60), 1);
const intensity = getIntensityBadge(item.totalEnergyBurned, item.duration || 0);
const intensity = getIntensityBadge(t, item.totalEnergyBurned, item.duration || 0);
const iconName = ICON_MAP[item.workoutActivityType as WorkoutActivityType] || 'arm-flex';
const time = dayjs(item.startDate || item.endDate).format('HH:mm');
const activityLabel = getWorkoutTypeDisplayName(item.workoutActivityType);
@@ -512,12 +514,12 @@ export default function WorkoutHistoryScreen() {
<View style={styles.cardContent}>
<View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{calories} · {minutes}</Text>
<Text style={styles.cardTitle}>{t('workoutHistory.historyCard.calories', { calories, minutes })}</Text>
<View style={[styles.intensityBadge, { backgroundColor: intensity.background }]}>
<Text style={[styles.intensityText, { color: intensity.color }]}>{intensity.label}</Text>
</View>
</View>
<Text style={styles.cardSubtitle}>{activityLabel}{time}</Text>
<Text style={styles.cardSubtitle}>{t('workoutHistory.historyCard.activityTime', { activity: activityLabel, time })}</Text>
</View>
{/* <Ionicons name="chevron-forward" size={20} color="#9AA4C4" /> */}
@@ -535,11 +537,11 @@ export default function WorkoutHistoryScreen() {
colors={["#F3F5FF", "#FFFFFF"]}
style={StyleSheet.absoluteFill}
/>
<HeaderBar title="锻炼总结" variant="minimal" transparent={true} />
<HeaderBar title={t('workoutHistory.title')} variant="minimal" transparent={true} />
{isLoading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#5C55FF" />
<Text style={styles.loadingText}>...</Text>
<Text style={styles.loadingText}>{t('workoutHistory.loading')}</Text>
</View>
) : (
<SectionList
@@ -556,7 +558,7 @@ export default function WorkoutHistoryScreen() {
<MaterialCommunityIcons name="alert-circle" size={40} color="#F85959" />
<Text style={[styles.emptyText, { color: '#F85959' }]}>{error}</Text>
<TouchableOpacity style={styles.retryButton} onPress={loadHistory}>
<Text style={styles.retryText}></Text>
<Text style={styles.retryText}>{t('workoutHistory.retry')}</Text>
</TouchableOpacity>
</View>
) : emptyComponent}