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

@@ -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>

View File

@@ -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}

View File

@@ -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>
);