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

@@ -11,6 +11,7 @@ import {
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
// 睡眠详情数据类型
export type SleepDetailData = {
@@ -41,15 +42,22 @@ const SleepGradeCard = ({
range: string;
isActive?: boolean;
}) => {
const { t } = useI18n();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const getGradeColor = (grade: string) => {
switch (grade) {
case '低': case '较差': return { bg: '#FECACA', text: '#DC2626' };
case '正常': case '一般': return { bg: '#D1FAE5', text: '#065F46' };
case '良好': return { bg: '#D1FAE5', text: '#065F46' };
case '优秀': return { bg: '#FEF3C7', text: '#92400E' };
case t('sleepDetail.sleepGrades.low'):
case t('sleepDetail.sleepGrades.poor'):
return { bg: '#FECACA', text: '#DC2626' };
case t('sleepDetail.sleepGrades.normal'):
case t('sleepDetail.sleepGrades.fair'):
return { bg: '#D1FAE5', text: '#065F46' };
case t('sleepDetail.sleepGrades.good'):
return { bg: '#D1FAE5', text: '#065F46' };
case t('sleepDetail.sleepGrades.excellent'):
return { bg: '#FEF3C7', text: '#92400E' };
default: return { bg: colorTokens.pageBackgroundEmphasis, text: colorTokens.textSecondary };
}
};
@@ -97,6 +105,7 @@ export const InfoModal = ({
type: 'sleep-time' | 'sleep-quality';
sleepData: SleepDetailData;
}) => {
const { t } = useI18n();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const slideAnim = useState(new Animated.Value(0))[0];
@@ -153,26 +162,26 @@ export const InfoModal = ({
const currentSleepQualityGrade = getSleepQualityGrade(sleepData.sleepQualityPercentage || 94); // 默认94%
const sleepTimeGrades = [
{ icon: 'alert-circle-outline', grade: '低', range: '< 6h', isActive: currentSleepTimeGrade === 0 },
{ icon: 'checkmark-circle-outline', grade: '正常', range: '6h - 7h or > 9h', isActive: currentSleepTimeGrade === 1 },
{ icon: 'checkmark-circle', grade: '良好', range: '7h - 8h', isActive: currentSleepTimeGrade === 2 },
{ icon: 'star', grade: '优秀', range: '8h - 9h', isActive: currentSleepTimeGrade === 3 },
{ icon: 'alert-circle-outline', grade: t('sleepDetail.sleepGrades.low'), range: '< 6h', isActive: currentSleepTimeGrade === 0 },
{ icon: 'checkmark-circle-outline', grade: t('sleepDetail.sleepGrades.normal'), range: '6h - 7h or > 9h', isActive: currentSleepTimeGrade === 1 },
{ icon: 'checkmark-circle', grade: t('sleepDetail.sleepGrades.good'), range: '7h - 8h', isActive: currentSleepTimeGrade === 2 },
{ icon: 'star', grade: t('sleepDetail.sleepGrades.excellent'), range: '8h - 9h', isActive: currentSleepTimeGrade === 3 },
];
const sleepQualityGrades = [
{ icon: 'alert-circle-outline', grade: '较差', range: '< 55%', isActive: currentSleepQualityGrade === 0 },
{ icon: 'checkmark-circle-outline', grade: '一般', range: '55% - 69%', isActive: currentSleepQualityGrade === 1 },
{ icon: 'checkmark-circle', grade: '良好', range: '70% - 84%', isActive: currentSleepQualityGrade === 2 },
{ icon: 'star', grade: '优秀', range: '85% - 100%', isActive: currentSleepQualityGrade === 3 },
{ icon: 'alert-circle-outline', grade: t('sleepDetail.sleepGrades.poor'), range: '< 55%', isActive: currentSleepQualityGrade === 0 },
{ icon: 'checkmark-circle-outline', grade: t('sleepDetail.sleepGrades.fair'), range: '55% - 69%', isActive: currentSleepQualityGrade === 1 },
{ icon: 'checkmark-circle', grade: t('sleepDetail.sleepGrades.good'), range: '70% - 84%', isActive: currentSleepQualityGrade === 2 },
{ icon: 'star', grade: t('sleepDetail.sleepGrades.excellent'), range: '85% - 100%', isActive: currentSleepQualityGrade === 3 },
];
const currentGrades = type === 'sleep-time' ? sleepTimeGrades : sleepQualityGrades;
const getDescription = () => {
if (type === 'sleep-time') {
return '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。';
return t('sleepDetail.sleepTimeDescription');
} else {
return '睡眠质量综合评估您的睡眠效率、深度睡眠时长、REM睡眠比例等多个指标。高质量的睡眠不仅仅取决于时长还包括睡眠的连续性和各睡眠阶段的平衡。';
return t('sleepDetail.sleepQualityDescription');
}
};

View File

@@ -7,18 +7,24 @@ import React, { useMemo } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Svg, { Rect, Text as SvgText } from 'react-native-svg';
import { StyleProp, ViewStyle } from 'react-native';
export type SleepStageTimelineProps = {
sleepSamples: SleepSample[];
bedtime: string;
wakeupTime: string;
onInfoPress?: () => void;
hideHeader?: boolean;
style?: StyleProp<ViewStyle>;
};
export const SleepStageTimeline = ({
sleepSamples,
bedtime,
wakeupTime,
onInfoPress
onInfoPress,
hideHeader = false,
style
}: SleepStageTimelineProps) => {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
@@ -130,15 +136,17 @@ export const SleepStageTimeline = ({
// 如果没有数据,显示空状态
if (timelineData.length === 0) {
return (
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
<View style={styles.header}>
<Text style={[styles.title, { color: colorTokens.text }]}></Text>
{onInfoPress && (
<TouchableOpacity style={styles.infoButton} onPress={onInfoPress}>
<Ionicons name="help-circle-outline" size={20} color={colorTokens.textSecondary} />
</TouchableOpacity>
)}
</View>
<View style={[styles.container, { backgroundColor: colorTokens.background }, style]}>
{!hideHeader && (
<View style={styles.header}>
<Text style={[styles.title, { color: colorTokens.text }]}></Text>
{onInfoPress && (
<TouchableOpacity style={styles.infoButton} onPress={onInfoPress}>
<Ionicons name="help-circle-outline" size={20} color={colorTokens.textSecondary} />
</TouchableOpacity>
)}
</View>
)}
<View style={styles.emptyState}>
<Text style={[styles.emptyText, { color: colorTokens.textSecondary }]}>
@@ -149,16 +157,18 @@ export const SleepStageTimeline = ({
}
return (
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
<View style={[styles.container, { backgroundColor: colorTokens.background }, style]}>
{/* 标题栏 */}
<View style={styles.header}>
<Text style={[styles.title, { color: colorTokens.text }]}></Text>
{onInfoPress && (
<TouchableOpacity style={styles.infoButton} onPress={onInfoPress}>
<Ionicons name="help-circle-outline" size={20} color={colorTokens.textSecondary} />
</TouchableOpacity>
)}
</View>
{!hideHeader && (
<View style={styles.header}>
<Text style={[styles.title, { color: colorTokens.text }]}></Text>
{onInfoPress && (
<TouchableOpacity style={styles.infoButton} onPress={onInfoPress}>
<Ionicons name="help-circle-outline" size={20} color={colorTokens.textSecondary} />
</TouchableOpacity>
)}
</View>
)}
{/* 睡眠时间范围 */}
<View style={styles.timeRange}>

View File

@@ -13,6 +13,7 @@ import {
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
// Sleep Stages Info Modal 组件
export const SleepStagesInfoModal = ({
@@ -22,6 +23,7 @@ export const SleepStagesInfoModal = ({
visible: boolean;
onClose: () => void;
}) => {
const { t } = useI18n();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const slideAnim = useState(new Animated.Value(0))[0];
@@ -82,7 +84,7 @@ export const SleepStagesInfoModal = ({
<View style={styles.sleepStagesModalHeader}>
<Text style={[styles.sleepStagesModalTitle, { color: colorTokens.text }]}>
{t('sleepDetail.sleepStagesInfo.title')}
</Text>
<TouchableOpacity onPress={onClose} style={styles.infoModalCloseButton}>
<Ionicons name="close" size={24} color={colorTokens.textSecondary} />
@@ -97,7 +99,7 @@ export const SleepStagesInfoModal = ({
scrollEnabled={true}
>
<Text style={[styles.sleepStagesDescription, { color: colorTokens.textSecondary }]}>
{t('sleepDetail.sleepStagesInfo.description')}
</Text>
{/* 清醒时间 */}
@@ -105,11 +107,11 @@ export const SleepStagesInfoModal = ({
<View style={[styles.sleepStageInfoHeader, { borderBottomColor: colorTokens.border }]}>
<View style={styles.sleepStageInfoTitleContainer}>
<View style={[styles.sleepStageDot, { backgroundColor: '#F59E0B' }]} />
<Text style={[styles.sleepStageInfoTitle, { color: colorTokens.text }]}></Text>
<Text style={[styles.sleepStageInfoTitle, { color: colorTokens.text }]}>{t('sleepDetail.sleepStagesInfo.awake.title')}</Text>
</View>
</View>
<Text style={[styles.sleepStageInfoContent, { color: colorTokens.textSecondary }]}>
{t('sleepDetail.sleepStagesInfo.awake.description')}
</Text>
</View>
@@ -118,11 +120,11 @@ export const SleepStagesInfoModal = ({
<View style={[styles.sleepStageInfoHeader, { borderBottomColor: colorTokens.border }]}>
<View style={styles.sleepStageInfoTitleContainer}>
<View style={[styles.sleepStageDot, { backgroundColor: '#EC4899' }]} />
<Text style={[styles.sleepStageInfoTitle, { color: colorTokens.text }]}></Text>
<Text style={[styles.sleepStageInfoTitle, { color: colorTokens.text }]}>{t('sleepDetail.sleepStagesInfo.rem.title')}</Text>
</View>
</View>
<Text style={[styles.sleepStageInfoContent, { color: colorTokens.textSecondary }]}>
{t('sleepDetail.sleepStagesInfo.rem.description')}
</Text>
</View>
@@ -131,11 +133,11 @@ export const SleepStagesInfoModal = ({
<View style={[styles.sleepStageInfoHeader, { borderBottomColor: colorTokens.border }]}>
<View style={styles.sleepStageInfoTitleContainer}>
<View style={[styles.sleepStageDot, { backgroundColor: '#8B5CF6' }]} />
<Text style={[styles.sleepStageInfoTitle, { color: colorTokens.text }]}></Text>
<Text style={[styles.sleepStageInfoTitle, { color: colorTokens.text }]}>{t('sleepDetail.sleepStagesInfo.core.title')}</Text>
</View>
</View>
<Text style={[styles.sleepStageInfoContent, { color: colorTokens.textSecondary }]}>
{t('sleepDetail.sleepStagesInfo.core.description')}
</Text>
</View>
@@ -144,11 +146,11 @@ export const SleepStagesInfoModal = ({
<View style={[styles.sleepStageInfoHeader, { borderBottomColor: colorTokens.border }]}>
<View style={styles.sleepStageInfoTitleContainer}>
<View style={[styles.sleepStageDot, { backgroundColor: '#3B82F6' }]} />
<Text style={[styles.sleepStageInfoTitle, { color: colorTokens.text }]}></Text>
<Text style={[styles.sleepStageInfoTitle, { color: colorTokens.text }]}>{t('sleepDetail.sleepStagesInfo.deep.title')}</Text>
</View>
</View>
<Text style={[styles.sleepStageInfoContent, { color: colorTokens.textSecondary }]}>
{t('sleepDetail.sleepStagesInfo.deep.description')}
</Text>
</View>
</ScrollView>