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

@@ -5,6 +5,7 @@ import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { appStoreReviewService } from '@/services/appStoreReview';
import { deleteWeightRecord, fetchWeightHistory, updateUserProfile, updateWeightRecord, WeightHistoryItem } from '@/store/userSlice';
@@ -24,6 +25,7 @@ import {
} from 'react-native';
export default function WeightRecordsPage() {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop()
const dispatch = useAppDispatch();
@@ -43,7 +45,7 @@ export default function WeightRecordsPage() {
try {
await dispatch(fetchWeightHistory() as any);
} catch (error) {
console.error('加载体重历史失败:', error);
console.error(t('weightRecords.loadingHistory'), error);
}
}, [dispatch]);
@@ -92,15 +94,15 @@ export default function WeightRecordsPage() {
await dispatch(deleteWeightRecord(id) as any);
await loadWeightHistory();
} catch (error) {
console.error('删除体重记录失败:', error);
Alert.alert('错误', '删除体重记录失败,请重试');
console.error(t('weightRecords.alerts.deleteFailed'), error);
Alert.alert('错误', t('weightRecords.alerts.deleteFailed'));
}
};
const handleWeightSave = async () => {
const weight = parseFloat(inputWeight);
if (isNaN(weight) || weight <= 0 || weight > 500) {
alert('请输入有效的体重值0-500kg');
alert(t('weightRecords.alerts.invalidWeight'));
return;
}
@@ -130,8 +132,8 @@ export default function WeightRecordsPage() {
setEditingRecord(null);
await loadWeightHistory();
} catch (error) {
console.error('保存体重失败:', error);
Alert.alert('错误', '保存体重失败,请重试');
console.error(t('weightRecords.alerts.saveFailed'), error);
Alert.alert('错误', t('weightRecords.alerts.saveFailed'));
}
};
@@ -190,7 +192,7 @@ export default function WeightRecordsPage() {
/>
<HeaderBar
title="体重记录"
title={t('weightRecords.title')}
right={<TouchableOpacity onPress={handleAddWeight} style={styles.addButton}>
<Ionicons name="add" size={24} color="#192126" />
</TouchableOpacity>}
@@ -204,27 +206,27 @@ export default function WeightRecordsPage() {
<View style={styles.statsRow}>
<View style={styles.statItem}>
<Text style={styles.statValue}>{totalWeightLoss.toFixed(1)}kg</Text>
<Text style={styles.statLabel}></Text>
<Text style={styles.statLabel}>{t('weightRecords.stats.totalLoss')}</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{currentWeight.toFixed(1)}kg</Text>
<Text style={styles.statLabel}></Text>
<Text style={styles.statLabel}>{t('weightRecords.stats.currentWeight')}</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{initialWeight.toFixed(1)}kg</Text>
<View style={styles.statLabelContainer}>
<Text style={styles.statLabel}></Text>
<Text style={styles.statLabel}>{t('weightRecords.stats.initialWeight')}</Text>
<TouchableOpacity onPress={handleEditInitialWeight} style={styles.editIcon}>
<Ionicons name="create-outline" size={14} color="#FF9500" />
<Ionicons name="create-outline" size={12} color="#FF9500" />
</TouchableOpacity>
</View>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{targetWeight.toFixed(1)}kg</Text>
<View style={styles.statLabelContainer}>
<Text style={styles.statLabel}></Text>
<Text style={styles.statLabel}>{t('weightRecords.stats.targetWeight')}</Text>
<TouchableOpacity onPress={handleEditTargetWeight} style={styles.editIcon}>
<Ionicons name="create-outline" size={14} color="#FF9500" />
<Ionicons name="create-outline" size={12} color="#FF9500" />
</TouchableOpacity>
</View>
</View>
@@ -282,8 +284,8 @@ export default function WeightRecordsPage() {
) : (
<View style={styles.emptyContainer}>
<View style={styles.emptyContent}>
<Text style={styles.emptyText}></Text>
<Text style={styles.emptySubtext}></Text>
<Text style={styles.emptyText}>{t('weightRecords.empty.title')}</Text>
<Text style={styles.emptySubtext}>{t('weightRecords.empty.subtitle')}</Text>
</View>
</View>
)}
@@ -309,10 +311,10 @@ export default function WeightRecordsPage() {
<Ionicons name="close" size={24} color={themeColors.text} />
</TouchableOpacity>
<Text style={[styles.modalTitle, { color: themeColors.text }]}>
{pickerType === 'current' && '记录体重'}
{pickerType === 'initial' && '编辑初始体重'}
{pickerType === 'target' && '编辑目标体重'}
{pickerType === 'edit' && '编辑体重记录'}
{pickerType === 'current' && t('weightRecords.modal.recordWeight')}
{pickerType === 'initial' && t('weightRecords.modal.editInitialWeight')}
{pickerType === 'target' && t('weightRecords.modal.editTargetWeight')}
{pickerType === 'edit' && t('weightRecords.modal.editRecord')}
</Text>
<View style={{ width: 24 }} />
</View>
@@ -329,21 +331,21 @@ export default function WeightRecordsPage() {
</View>
<View style={styles.inputWrapper}>
<Text style={[styles.weightDisplay, { color: inputWeight ? themeColors.text : themeColors.textSecondary }]}>
{inputWeight || '输入体重'}
{inputWeight || t('weightRecords.modal.inputPlaceholder')}
</Text>
<Text style={[styles.unitLabel, { color: themeColors.textSecondary }]}>kg</Text>
<Text style={[styles.unitLabel, { color: themeColors.textSecondary }]}>{t('weightRecords.modal.unit')}</Text>
</View>
</View>
{/* Weight Range Hint */}
<Text style={[styles.hintText, { color: themeColors.textSecondary }]}>
0-500
{t('weightRecords.modal.inputHint')}
</Text>
</View>
{/* Quick Selection */}
<View style={styles.quickSelectionSection}>
<Text style={[styles.quickSelectionTitle, { color: themeColors.text }]}></Text>
<Text style={[styles.quickSelectionTitle, { color: themeColors.text }]}>{t('weightRecords.modal.quickSelection')}</Text>
<View style={styles.quickButtons}>
{[50, 60, 70, 80, 90].map((weight) => (
<TouchableOpacity
@@ -358,7 +360,7 @@ export default function WeightRecordsPage() {
styles.quickButtonText,
inputWeight === weight.toString() && styles.quickButtonTextSelected
]}>
{weight}kg
{weight}{t('weightRecords.modal.unit')}
</Text>
</TouchableOpacity>
))}
@@ -386,7 +388,7 @@ export default function WeightRecordsPage() {
onPress={handleWeightSave}
disabled={!inputWeight.trim()}
>
<Text style={styles.saveButtonText}></Text>
<Text style={styles.saveButtonText}>{t('weightRecords.modal.confirm')}</Text>
</TouchableOpacity>
</View>
</View>
@@ -461,7 +463,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
statValue: {
fontSize: 16,
fontSize: 14,
fontWeight: '800',
color: '#192126',
marginBottom: 4,
@@ -475,6 +477,7 @@ const styles = StyleSheet.create({
fontSize: 12,
color: '#687076',
marginRight: 4,
textAlign: 'center'
},
editIcon: {
padding: 2,