feat(i18n): 实现应用国际化支持,添加中英文翻译

- 为所有UI组件添加国际化支持,替换硬编码文本
- 新增useI18n钩子函数统一管理翻译
- 完善中英文翻译资源,覆盖统计、用药、通知设置等模块
- 优化Tab布局使用翻译键值替代静态文本
- 更新药品管理、个人资料编辑等页面的多语言支持
This commit is contained in:
richarjiang
2025-11-13 11:09:55 +08:00
parent 416d144387
commit 2dca3253e6
21 changed files with 1669 additions and 366 deletions

View File

@@ -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<WorkoutSummaryCardProps> = ({ date, style }) => {
const { t } = useTranslation();
const router = useRouter();
const [summary, setSummary] = useState<WorkoutSummary>(DEFAULT_SUMMARY);
const [isLoading, setIsLoading] = useState(false);
@@ -145,13 +147,13 @@ export const WorkoutSummaryCard: React.FC<WorkoutSummaryCardProps> = ({ 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<WorkoutSummaryCardProps> = ({ 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<WorkoutSummaryCardProps> = ({ date, st
source,
badges: uniqueBadges,
};
}, [summary]);
}, [summary, t]);
return (
<TouchableOpacity
@@ -195,18 +199,18 @@ export const WorkoutSummaryCard: React.FC<WorkoutSummaryCardProps> = ({ date, st
<View style={styles.headerRow}>
<View style={styles.titleRow}>
<Image source={require('@/assets/images/icons/icon-fitness.png')} style={styles.titleIcon} />
<Text style={styles.titleText}></Text>
<Text style={styles.titleText}>{t('statistics.components.workout.title')}</Text>
</View>
</View>
<View style={styles.metricsRow}>
<View style={styles.metricItem}>
<AnimatedNumber value={summary.totalMinutes} resetToken={resetToken} style={styles.metricValue} />
<Text style={styles.metricLabel}></Text>
<Text style={styles.metricLabel}>{t('statistics.components.workout.minutes')}</Text>
</View>
<View style={styles.metricItem}>
<AnimatedNumber value={summary.totalCalories} resetToken={resetToken} style={styles.metricValue} />
<Text style={styles.metricLabel}></Text>
<Text style={styles.metricLabel}>{t('statistics.components.workout.kcal')}</Text>
</View>
</View>