feat(i18n): 实现应用国际化支持,添加中英文翻译
- 为所有UI组件添加国际化支持,替换硬编码文本 - 新增useI18n钩子函数统一管理翻译 - 完善中英文翻译资源,覆盖统计、用药、通知设置等模块 - 优化Tab布局使用翻译键值替代静态文本 - 更新药品管理、个人资料编辑等页面的多语言支持
This commit is contained in:
@@ -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 (
|
||||
// <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||||
// <View style={styles.cardHeader}>
|
||||
// <Text style={styles.cardTitle}>体重记录</Text>
|
||||
// <Text style={styles.cardTitle}>{t('statistics.components.weight.title')}</Text>
|
||||
// </View>
|
||||
|
||||
// <View style={styles.emptyContent}>
|
||||
// <Text style={styles.emptyDescription}>
|
||||
// 暂无体重记录,点击下方按钮开始记录
|
||||
// No weight records yet, click the button below to start recording
|
||||
// </Text>
|
||||
// <TouchableOpacity
|
||||
// style={styles.recordButton}
|
||||
@@ -94,14 +96,14 @@ export function WeightHistoryCard() {
|
||||
// activeOpacity={0.8}
|
||||
// >
|
||||
// <Ionicons name="add" size={18} color="#FFFFFF" />
|
||||
// <Text style={styles.recordButtonText}>记录体重</Text>
|
||||
// <Text style={styles.recordButtonText}>{t('statistics.components.weight.addButton')}</Text>
|
||||
// </TouchableOpacity>
|
||||
// </View>
|
||||
// </TouchableOpacity>
|
||||
// );
|
||||
// }
|
||||
|
||||
// 生成图表数据
|
||||
// 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}
|
||||
/>
|
||||
<Text style={styles.cardTitle}>体重记录</Text>
|
||||
<Text style={styles.cardTitle}>{t('statistics.components.weight.title')}</Text>
|
||||
{isLgAvaliable ? (
|
||||
<TouchableOpacity
|
||||
onPress={(e) => {
|
||||
@@ -160,13 +162,13 @@ export function WeightHistoryCard() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 默认显示图表 */}
|
||||
{/* Default chart display */}
|
||||
{sortedHistory.length > 0 && (
|
||||
<View style={styles.chartContainer}>
|
||||
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
|
||||
{/* 背景网格线 */}
|
||||
{/* Background grid lines */}
|
||||
|
||||
{/* 更抽象的折线 - 减小线宽和显示的细节 */}
|
||||
{/* More abstract line - reduce line width and display details */}
|
||||
<Path
|
||||
d={singlePointPath}
|
||||
stroke={Colors.light.accentGreen}
|
||||
@@ -177,7 +179,7 @@ export function WeightHistoryCard() {
|
||||
opacity={0.8}
|
||||
/>
|
||||
|
||||
{/* 简化的数据点 - 更小更精致 */}
|
||||
{/* Simplified data points - smaller and more refined */}
|
||||
{points.map((point, index) => {
|
||||
const isLastPoint = index === points.length - 1;
|
||||
|
||||
@@ -197,13 +199,13 @@ export function WeightHistoryCard() {
|
||||
|
||||
</Svg>
|
||||
|
||||
{/* 精简的图表信息 */}
|
||||
{/* Concise chart information */}
|
||||
<View style={styles.chartInfo}>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>{userProfile.weight}kg</Text>
|
||||
</View>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>{sortedHistory.length}天</Text>
|
||||
<Text style={styles.infoLabel}>{sortedHistory.length}{t('statistics.components.weight.days')}</Text>
|
||||
</View>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>
|
||||
@@ -214,7 +216,7 @@ export function WeightHistoryCard() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* BMI 信息弹窗 */}
|
||||
{/* BMI information modal */}
|
||||
<Modal
|
||||
visible={showBMIModal}
|
||||
animationType="slide"
|
||||
@@ -228,31 +230,31 @@ export function WeightHistoryCard() {
|
||||
end={{ x: 0, y: 1 }}
|
||||
>
|
||||
<ScrollView style={styles.bmiModalContent} showsVerticalScrollIndicator={false}>
|
||||
{/* 标题 */}
|
||||
<Text style={styles.bmiModalTitle}>BMI 指数说明</Text>
|
||||
{/* Title */}
|
||||
<Text style={styles.bmiModalTitle}>{t('statistics.components.weight.bmiModal.title')}</Text>
|
||||
|
||||
{/* 介绍部分 */}
|
||||
{/* Introduction section */}
|
||||
<View style={styles.bmiModalIntroSection}>
|
||||
<Text style={styles.bmiModalDescription}>
|
||||
BMI(身体质量指数)是评估体重与身高关系的国际通用健康指标
|
||||
{t('statistics.components.weight.bmiModal.description')}
|
||||
</Text>
|
||||
<View style={styles.bmiModalFormulaContainer}>
|
||||
<Text style={styles.bmiModalFormulaText}>
|
||||
计算公式:体重(kg) ÷ 身高²(m)
|
||||
{t('statistics.components.weight.bmiModal.formula')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* BMI 分类标准 */}
|
||||
<Text style={styles.bmiModalSectionTitle}>BMI 分类标准</Text>
|
||||
{/* BMI classification standards */}
|
||||
<Text style={styles.bmiModalSectionTitle}>{t('statistics.components.weight.bmiModal.classificationTitle')}</Text>
|
||||
|
||||
<View style={styles.bmiModalStatsCard}>
|
||||
{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() {
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* 健康建议 */}
|
||||
<Text style={styles.bmiModalSectionTitle}>健康建议</Text>
|
||||
{/* Health tips */}
|
||||
<Text style={styles.bmiModalSectionTitle}>{t('statistics.components.weight.bmiModal.healthTipsTitle')}</Text>
|
||||
<View style={styles.bmiModalHealthTips}>
|
||||
<View style={styles.bmiModalTipsItem}>
|
||||
<Ionicons name="nutrition-outline" size={20} color="#3B82F6" />
|
||||
<Text style={styles.bmiModalTipsText}>保持均衡饮食,控制热量摄入</Text>
|
||||
<Text style={styles.bmiModalTipsText}>{t('statistics.components.weight.bmiModal.tips.nutrition')}</Text>
|
||||
</View>
|
||||
<View style={styles.bmiModalTipsItem}>
|
||||
<Ionicons name="walk-outline" size={20} color="#3B82F6" />
|
||||
<Text style={styles.bmiModalTipsText}>每周至少150分钟中等强度运动</Text>
|
||||
<Text style={styles.bmiModalTipsText}>{t('statistics.components.weight.bmiModal.tips.exercise')}</Text>
|
||||
</View>
|
||||
<View style={styles.bmiModalTipsItem}>
|
||||
<Ionicons name="moon-outline" size={20} color="#3B82F6" />
|
||||
<Text style={styles.bmiModalTipsText}>保证7-9小时充足睡眠</Text>
|
||||
<Text style={styles.bmiModalTipsText}>{t('statistics.components.weight.bmiModal.tips.sleep')}</Text>
|
||||
</View>
|
||||
<View style={styles.bmiModalTipsItem}>
|
||||
<Ionicons name="calendar-outline" size={20} color="#3B82F6" />
|
||||
<Text style={styles.bmiModalTipsText}>定期监测体重变化,及时调整</Text>
|
||||
<Text style={styles.bmiModalTipsText}>{t('statistics.components.weight.bmiModal.tips.monitoring')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 免责声明 */}
|
||||
{/* Disclaimer */}
|
||||
<View style={styles.bmiModalDisclaimer}>
|
||||
<Ionicons name="information-circle-outline" size={16} color="#6B7280" />
|
||||
<Text style={styles.bmiModalDisclaimerText}>
|
||||
BMI 仅供参考,不能反映肌肉量、骨密度等指标。如有健康疑问,请咨询专业医生。
|
||||
{t('statistics.components.weight.bmiModal.disclaimer')}
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部继续按钮 */}
|
||||
{/* Bottom continue button */}
|
||||
<View style={styles.bmiModalBottomContainer}>
|
||||
<TouchableOpacity style={styles.bmiModalContinueButton} onPress={handleHideBMIModal}>
|
||||
<View style={styles.bmiModalButtonBackground}>
|
||||
<Text style={styles.bmiModalButtonText}>继续</Text>
|
||||
<Text style={styles.bmiModalButtonText}>{t('statistics.components.weight.bmiModal.continueButton')}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.bmiModalHomeIndicator} />
|
||||
@@ -429,7 +431,7 @@ const styles = StyleSheet.create({
|
||||
color: '#192126',
|
||||
},
|
||||
|
||||
// BMI 弹窗样式
|
||||
// BMI modal styles
|
||||
bmiModalContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user