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

@@ -15,6 +15,7 @@ import {
View,
} from 'react-native';
import { useI18n } from '@/hooks/useI18n';
import {
HeartRateZoneStat,
WorkoutDetailMetrics,
@@ -59,6 +60,7 @@ export function WorkoutDetailModal({
onRetry,
errorMessage,
}: WorkoutDetailModalProps) {
const { t } = useI18n();
const animation = useRef(new Animated.Value(visible ? 1 : 0)).current;
const [isMounted, setIsMounted] = useState(visible);
const [showIntensityInfo, setShowIntensityInfo] = useState(false);
@@ -229,26 +231,26 @@ export function WorkoutDetailModal({
{loading ? (
<View style={styles.loadingBlock}>
<ActivityIndicator color="#5C55FF" />
<Text style={styles.loadingLabel}>...</Text>
<Text style={styles.loadingLabel}>{t('workoutDetail.loading')}</Text>
</View>
) : metrics ? (
<>
<View style={styles.metricsRow}>
<View style={styles.metricItem}>
<Text style={styles.metricTitle}></Text>
<Text style={styles.metricTitle}>{t('workoutDetail.metrics.duration')}</Text>
<Text style={styles.metricValue}>{metrics.durationLabel}</Text>
</View>
<View style={styles.metricItem}>
<Text style={styles.metricTitle}></Text>
<Text style={styles.metricTitle}>{t('workoutDetail.metrics.calories')}</Text>
<Text style={styles.metricValue}>
{metrics.calories != null ? `${metrics.calories} 千卡` : '--'}
{metrics.calories != null ? `${metrics.calories} ${t('workoutDetail.metrics.caloriesUnit')}` : '--'}
</Text>
</View>
</View>
<View style={styles.metricsRow}>
<View style={styles.metricItem}>
<View style={styles.metricTitleRow}>
<Text style={styles.metricTitle}></Text>
<Text style={styles.metricTitle}>{t('workoutDetail.metrics.intensity')}</Text>
<TouchableOpacity
onPress={() => setShowIntensityInfo(true)}
style={styles.metricInfoButton}
@@ -262,9 +264,9 @@ export function WorkoutDetailModal({
</Text>
</View>
<View style={styles.metricItem}>
<Text style={styles.metricTitle}></Text>
<Text style={styles.metricTitle}>{t('workoutDetail.metrics.averageHeartRate')}</Text>
<Text style={styles.metricValue}>
{metrics.averageHeartRate != null ? `${metrics.averageHeartRate} 次/分` : '--'}
{metrics.averageHeartRate != null ? `${metrics.averageHeartRate} ${t('workoutDetail.metrics.heartRateUnit')}` : '--'}
</Text>
</View>
</View>
@@ -275,11 +277,11 @@ export function WorkoutDetailModal({
) : (
<View style={styles.errorBlock}>
<Text style={styles.errorText}>
{errorMessage || '未能获取到完整的锻炼详情'}
{errorMessage || t('workoutDetail.errors.loadFailed')}
</Text>
{onRetry ? (
<TouchableOpacity style={styles.retryButton} onPress={onRetry}>
<Text style={styles.retryButtonText}></Text>
<Text style={styles.retryButtonText}>{t('workoutDetail.retry')}</Text>
</TouchableOpacity>
) : null}
</View>
@@ -288,7 +290,7 @@ export function WorkoutDetailModal({
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}></Text>
<Text style={styles.sectionTitle}>{t('workoutDetail.sections.heartRateRange')}</Text>
</View>
{loading ? (
@@ -299,21 +301,21 @@ export function WorkoutDetailModal({
<>
<View style={styles.heartRateSummaryRow}>
<View style={styles.heartRateStat}>
<Text style={styles.statLabel}></Text>
<Text style={styles.statLabel}>{t('workoutDetail.sections.averageHeartRate')}</Text>
<Text style={styles.statValue}>
{metrics.averageHeartRate != null ? `${metrics.averageHeartRate}次/分` : '--'}
{metrics.averageHeartRate != null ? `${metrics.averageHeartRate} ${t('workoutDetail.sections.heartRateUnit')}` : '--'}
</Text>
</View>
<View style={styles.heartRateStat}>
<Text style={styles.statLabel}></Text>
<Text style={styles.statLabel}>{t('workoutDetail.sections.maximumHeartRate')}</Text>
<Text style={styles.statValue}>
{metrics.maximumHeartRate != null ? `${metrics.maximumHeartRate}次/分` : '--'}
{metrics.maximumHeartRate != null ? `${metrics.maximumHeartRate} ${t('workoutDetail.sections.heartRateUnit')}` : '--'}
</Text>
</View>
<View style={styles.heartRateStat}>
<Text style={styles.statLabel}></Text>
<Text style={styles.statLabel}>{t('workoutDetail.sections.minimumHeartRate')}</Text>
<Text style={styles.statValue}>
{metrics.minimumHeartRate != null ? `${metrics.minimumHeartRate}次/分` : '--'}
{metrics.minimumHeartRate != null ? `${metrics.minimumHeartRate} ${t('workoutDetail.sections.heartRateUnit')}` : '--'}
</Text>
</View>
</View>
@@ -336,7 +338,7 @@ export function WorkoutDetailModal({
width={Dimensions.get('window').width - 72}
height={220}
fromZero={false}
yAxisSuffix="次/分"
yAxisSuffix={t('workoutDetail.sections.heartRateUnit')}
withInnerLines={false}
bezier
chartConfig={{
@@ -360,20 +362,20 @@ export function WorkoutDetailModal({
) : (
<View style={styles.chartEmpty}>
<MaterialCommunityIcons name="chart-line-variant" size={32} color="#C5CBE2" />
<Text style={styles.chartEmptyText}>线</Text>
<Text style={styles.chartEmptyText}>{t('workoutDetail.chart.unavailable')}</Text>
</View>
)
) : (
<View style={styles.chartEmpty}>
<MaterialCommunityIcons name="heart-off-outline" size={32} color="#C5CBE2" />
<Text style={styles.chartEmptyText}></Text>
<Text style={styles.chartEmptyText}>{t('workoutDetail.chart.noData')}</Text>
</View>
)}
</>
) : (
<View style={styles.sectionError}>
<Text style={styles.errorTextSmall}>
{errorMessage || '未获取到心率数据'}
{errorMessage || t('workoutDetail.errors.noHeartRateData')}
</Text>
</View>
)}
@@ -381,7 +383,7 @@ export function WorkoutDetailModal({
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}></Text>
<Text style={styles.sectionTitle}>{t('workoutDetail.sections.heartRateZones')}</Text>
</View>
{loading ? (
@@ -391,7 +393,7 @@ export function WorkoutDetailModal({
) : metrics ? (
metrics.heartRateZones.map(renderHeartRateZone)
) : (
<Text style={styles.errorTextSmall}></Text>
<Text style={styles.errorTextSmall}>{t('workoutDetail.errors.noZoneStats')}</Text>
)}
</View>
@@ -410,36 +412,36 @@ export function WorkoutDetailModal({
<TouchableWithoutFeedback onPress={() => { }}>
<View style={styles.intensityInfoSheet}>
<View style={styles.intensityHandle} />
<Text style={styles.intensityInfoTitle}></Text>
<Text style={styles.intensityInfoTitle}>{t('workoutDetail.intensityInfo.title')}</Text>
<Text style={styles.intensityInfoText}>
MET/·
{t('workoutDetail.intensityInfo.description1')}
</Text>
<Text style={styles.intensityInfoText}>
MET 便
{t('workoutDetail.intensityInfo.description2')}
</Text>
<Text style={styles.intensityInfoText}>
3 km/h 2 METs 2
{t('workoutDetail.intensityInfo.description3')}
</Text>
<Text style={styles.intensityInfoText}>
METs 使70
{t('workoutDetail.intensityInfo.description4')}
</Text>
<View style={styles.intensityFormula}>
<Text style={styles.intensityFormulaLabel}></Text>
<Text style={styles.intensityFormulaValue}>METs = / ÷ 1 /</Text>
<Text style={styles.intensityFormulaLabel}>{t('workoutDetail.intensityInfo.formula.title')}</Text>
<Text style={styles.intensityFormulaValue}>{t('workoutDetail.intensityInfo.formula.value')}</Text>
</View>
<View style={styles.intensityLegend}>
<View style={styles.intensityLegendRow}>
<Text style={styles.intensityLegendRange}>{'< 3'}</Text>
<Text style={[styles.intensityLegendLabel, styles.intensityLow]}></Text>
<Text style={styles.intensityLegendRange}>{t('workoutDetail.intensityInfo.legend.low')}</Text>
<Text style={[styles.intensityLegendLabel, styles.intensityLow]}>{t('workoutDetail.intensityInfo.legend.lowLabel')}</Text>
</View>
<View style={styles.intensityLegendRow}>
<Text style={styles.intensityLegendRange}>3 - 6</Text>
<Text style={[styles.intensityLegendLabel, styles.intensityMedium]}></Text>
<Text style={styles.intensityLegendRange}>{t('workoutDetail.intensityInfo.legend.medium')}</Text>
<Text style={[styles.intensityLegendLabel, styles.intensityMedium]}>{t('workoutDetail.intensityInfo.legend.mediumLabel')}</Text>
</View>
<View style={styles.intensityLegendRow}>
<Text style={styles.intensityLegendRange}>{'≥ 6'}</Text>
<Text style={[styles.intensityLegendLabel, styles.intensityHigh]}></Text>
<Text style={styles.intensityLegendRange}>{t('workoutDetail.intensityInfo.legend.high')}</Text>
<Text style={[styles.intensityLegendLabel, styles.intensityHigh]}>{t('workoutDetail.intensityInfo.legend.highLabel')}</Text>
</View>
</View>
</View>