feat(i18n): 实现应用国际化支持,添加中英文翻译
- 为所有UI组件添加国际化支持,替换硬编码文本 - 新增useI18n钩子函数统一管理翻译 - 完善中英文翻译资源,覆盖统计、用药、通知设置等模块 - 优化Tab布局使用翻译键值替代静态文本 - 更新药品管理、个人资料编辑等页面的多语言支持
This commit is contained in:
@@ -4,6 +4,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { selectUserProfile, updateUserBodyMeasurements, UserProfile } from '@/store/userSlice';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
interface CircumferenceCardProps {
|
||||
@@ -11,6 +12,7 @@ interface CircumferenceCardProps {
|
||||
}
|
||||
|
||||
const CircumferenceCard: React.FC<CircumferenceCardProps> = ({ style }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const userProfile = useAppSelector(selectUserProfile);
|
||||
|
||||
@@ -30,32 +32,32 @@ const CircumferenceCard: React.FC<CircumferenceCardProps> = ({ style }) => {
|
||||
const measurements = [
|
||||
{
|
||||
key: 'chestCircumference',
|
||||
label: '胸围',
|
||||
label: t('statistics.components.circumference.measurements.chest'),
|
||||
value: userProfile?.chestCircumference,
|
||||
},
|
||||
{
|
||||
key: 'waistCircumference',
|
||||
label: '腰围',
|
||||
label: t('statistics.components.circumference.measurements.waist'),
|
||||
value: userProfile?.waistCircumference,
|
||||
},
|
||||
{
|
||||
key: 'upperHipCircumference',
|
||||
label: '上臀围',
|
||||
label: t('statistics.components.circumference.measurements.hip'),
|
||||
value: userProfile?.upperHipCircumference,
|
||||
},
|
||||
{
|
||||
key: 'armCircumference',
|
||||
label: '臂围',
|
||||
label: t('statistics.components.circumference.measurements.arm'),
|
||||
value: userProfile?.armCircumference,
|
||||
},
|
||||
{
|
||||
key: 'thighCircumference',
|
||||
label: '大腿围',
|
||||
label: t('statistics.components.circumference.measurements.thigh'),
|
||||
value: userProfile?.thighCircumference,
|
||||
},
|
||||
{
|
||||
key: 'calfCircumference',
|
||||
label: '小腿围',
|
||||
label: t('statistics.components.circumference.measurements.calf'),
|
||||
value: userProfile?.calfCircumference,
|
||||
},
|
||||
];
|
||||
@@ -145,7 +147,7 @@ const CircumferenceCard: React.FC<CircumferenceCardProps> = ({ style }) => {
|
||||
onPress={handleCardPress}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.title}>围度 (cm)</Text>
|
||||
<Text style={styles.title}>{t('statistics.components.circumference.title')}</Text>
|
||||
|
||||
<View style={styles.measurementsContainer}>
|
||||
{measurements.map((measurement, index) => (
|
||||
@@ -174,12 +176,12 @@ const CircumferenceCard: React.FC<CircumferenceCardProps> = ({ style }) => {
|
||||
setModalVisible(false);
|
||||
setSelectedMeasurement(null);
|
||||
}}
|
||||
title={selectedMeasurement ? `设置${selectedMeasurement.label}` : '设置围度'}
|
||||
title={selectedMeasurement ? t('statistics.components.circumference.setTitle', { label: selectedMeasurement.label }) : t('statistics.components.circumference.title')}
|
||||
items={circumferenceOptions}
|
||||
selectedValue={selectedMeasurement?.currentValue}
|
||||
onValueChange={() => { }} // Real-time update not needed
|
||||
onConfirm={handleUpdateMeasurement}
|
||||
confirmButtonText="确认"
|
||||
confirmButtonText={t('statistics.components.circumference.confirm')}
|
||||
pickerHeight={180}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import HealthDataCard from './HealthDataCard';
|
||||
import { fetchOxygenSaturation } from '@/utils/health';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import HealthDataCard from './HealthDataCard';
|
||||
|
||||
interface OxygenSaturationCardProps {
|
||||
style?: object;
|
||||
@@ -13,6 +14,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
||||
style,
|
||||
selectedDate
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [oxygenSaturation, setOxygenSaturation] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const loadingRef = useRef(false);
|
||||
@@ -38,7 +40,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
||||
const data = await fetchOxygenSaturation(options);
|
||||
setOxygenSaturation(data);
|
||||
} catch (error) {
|
||||
console.error('OxygenSaturationCard: 获取血氧饱和度数据失败:', error);
|
||||
console.error('OxygenSaturationCard: Failed to get blood oxygen data:', error);
|
||||
setOxygenSaturation(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -52,7 +54,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
||||
|
||||
return (
|
||||
<HealthDataCard
|
||||
title="血氧饱和度"
|
||||
title={t('statistics.components.oxygen.title')}
|
||||
value={loading ? '--' : (oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--')}
|
||||
unit="%"
|
||||
style={style}
|
||||
|
||||
@@ -7,6 +7,7 @@ import dayjs from 'dayjs';
|
||||
import { Image } from 'expo-image';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
|
||||
@@ -19,6 +20,7 @@ const SleepCard: React.FC<SleepCardProps> = ({
|
||||
selectedDate,
|
||||
style,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const challenges = useAppSelector(selectChallengeList);
|
||||
const [sleepDuration, setSleepDuration] = useState<number | null>(null);
|
||||
@@ -39,7 +41,7 @@ const SleepCard: React.FC<SleepCardProps> = ({
|
||||
const data = await fetchCompleteSleepData(selectedDate);
|
||||
setSleepDuration(data?.totalSleepTime || null);
|
||||
} catch (error) {
|
||||
console.error('SleepCard: 获取睡眠数据失败:', error);
|
||||
console.error('SleepCard: Failed to get sleep data:', error);
|
||||
setSleepDuration(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -75,7 +77,7 @@ const SleepCard: React.FC<SleepCardProps> = ({
|
||||
try {
|
||||
await dispatch(reportChallengeProgress({ id: sleepChallenge.id, value: sleepDuration })).unwrap();
|
||||
} catch (error) {
|
||||
logger.warn('SleepCard: 挑战进度上报失败', { error, challengeId: sleepChallenge.id });
|
||||
logger.warn('SleepCard: Challenge progress report failed', { error, challengeId: sleepChallenge.id });
|
||||
}
|
||||
|
||||
lastReportedRef.current = { date: dateKey, value: sleepDuration };
|
||||
@@ -91,10 +93,10 @@ const SleepCard: React.FC<SleepCardProps> = ({
|
||||
source={require('@/assets/images/icons/icon-sleep.png')}
|
||||
style={styles.titleIcon}
|
||||
/>
|
||||
<Text style={styles.cardTitle}>睡眠</Text>
|
||||
<Text style={styles.cardTitle}>{t('statistics.components.sleep.title')}</Text>
|
||||
</View>
|
||||
<Text style={styles.sleepValue}>
|
||||
{loading ? '加载中...' : (sleepDuration != null ? formatSleepTime(sleepDuration) : '--')}
|
||||
{loading ? t('statistics.components.sleep.loading') : (sleepDuration != null ? formatSleepTime(sleepDuration) : '--')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user