feat(i18n): 实现应用国际化支持,添加中英文翻译
- 为所有UI组件添加国际化支持,替换硬编码文本 - 新增useI18n钩子函数统一管理翻译 - 完善中英文翻译资源,覆盖统计、用药、通知设置等模块 - 优化Tab布局使用翻译键值替代静态文本 - 更新药品管理、个人资料编辑等页面的多语言支持
This commit is contained in:
@@ -3,6 +3,7 @@ import { Colors } from '@/constants/Colors';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||
import { fetchMyProfile, updateUserProfile } from '@/store/userSlice';
|
||||
import { fetchMaximumHeartRate } from '@/utils/health';
|
||||
@@ -46,6 +47,7 @@ interface UserProfile {
|
||||
const STORAGE_KEY = '@user_profile';
|
||||
|
||||
export default function EditProfileScreen() {
|
||||
const { t } = useI18n();
|
||||
const safeAreaTop = useSafeAreaTop()
|
||||
const colorScheme = useColorScheme();
|
||||
const colors = Colors[colorScheme ?? 'light'];
|
||||
@@ -189,7 +191,7 @@ export default function EditProfileScreen() {
|
||||
const handleSaveWithProfile = async (profileData: UserProfile) => {
|
||||
try {
|
||||
if (!userId) {
|
||||
Alert.alert('未登录', '请先登录后再尝试保存');
|
||||
Alert.alert(t('editProfile.alerts.notLoggedIn.title'), t('editProfile.alerts.notLoggedIn.message'));
|
||||
return;
|
||||
}
|
||||
const next: UserProfile = { ...profileData };
|
||||
@@ -215,7 +217,7 @@ export default function EditProfileScreen() {
|
||||
console.warn('更新用户信息失败', e?.message || e);
|
||||
}
|
||||
} catch (e) {
|
||||
Alert.alert('保存失败', '请稍后重试');
|
||||
Alert.alert(t('editProfile.alerts.saveFailed.title'), t('editProfile.alerts.saveFailed.message'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -249,7 +251,7 @@ export default function EditProfileScreen() {
|
||||
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited';
|
||||
if (!libGranted) {
|
||||
Alert.alert('权限不足', '需要相册权限以选择头像');
|
||||
Alert.alert(t('editProfile.alerts.avatarPermissions.title'), t('editProfile.alerts.avatarPermissions.message'));
|
||||
return;
|
||||
}
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
@@ -275,21 +277,21 @@ export default function EditProfileScreen() {
|
||||
setProfile((p) => ({ ...p, avatarUri: url }));
|
||||
// 保存更新后的 profile
|
||||
await handleSaveWithProfile({ ...profile, avatarUri: url });
|
||||
Alert.alert('成功', '头像更新成功');
|
||||
Alert.alert(t('editProfile.alerts.avatarSuccess.title'), t('editProfile.alerts.avatarSuccess.message'));
|
||||
} catch (e) {
|
||||
console.warn('上传头像失败', e);
|
||||
Alert.alert('上传失败', '头像上传失败,请重试');
|
||||
Alert.alert(t('editProfile.alerts.avatarUploadFailed.title'), t('editProfile.alerts.avatarUploadFailed.message'));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Alert.alert('发生错误', '选择头像失败,请重试');
|
||||
Alert.alert(t('editProfile.alerts.avatarError.title'), t('editProfile.alerts.avatarError.message'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
|
||||
<HeaderBar
|
||||
title="编辑资料"
|
||||
title={t('editProfile.title')}
|
||||
onBack={() => router.back()}
|
||||
withSafeTop={false}
|
||||
transparent={true}
|
||||
@@ -321,8 +323,8 @@ export default function EditProfileScreen() {
|
||||
{/* 姓名 */}
|
||||
<ProfileCard
|
||||
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-name.png'
|
||||
title="昵称"
|
||||
value={profile.name || '今晚要吃肉'}
|
||||
title={t('editProfile.fields.name')}
|
||||
value={profile.name || t('editProfile.defaultValues.name')}
|
||||
onPress={() => {
|
||||
setTempValue(profile.name || '');
|
||||
setEditingField('name');
|
||||
@@ -334,8 +336,8 @@ export default function EditProfileScreen() {
|
||||
icon="body"
|
||||
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-sex.png"
|
||||
iconColor="#FF6B9D"
|
||||
title="性别"
|
||||
value={profile.gender === 'male' ? '男' : profile.gender === 'female' ? '女' : '未设置'}
|
||||
title={t('editProfile.fields.gender')}
|
||||
value={profile.gender === 'male' ? t('editProfile.gender.male') : profile.gender === 'female' ? t('editProfile.gender.female') : t('editProfile.gender.notSet')}
|
||||
onPress={() => {
|
||||
setEditingField('gender');
|
||||
}}
|
||||
@@ -344,10 +346,10 @@ export default function EditProfileScreen() {
|
||||
{/* 身高 */}
|
||||
<ProfileCard
|
||||
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-height.png"
|
||||
title="身高"
|
||||
value={profile.height ? `${Math.round(profile.height)}厘米` : '170厘米'}
|
||||
title={t('editProfile.fields.height')}
|
||||
value={profile.height ? `${Math.round(profile.height)}${t('editProfile.height.unit')}` : t('editProfile.height.placeholder')}
|
||||
onPress={() => {
|
||||
setTempValue(profile.height ? String(Math.round(profile.height)) : '170');
|
||||
setTempValue(profile.height ? String(Math.round(profile.height)) : String(t('editProfile.defaultValues.height')));
|
||||
setEditingField('height');
|
||||
}}
|
||||
/>
|
||||
@@ -355,10 +357,10 @@ export default function EditProfileScreen() {
|
||||
{/* 体重 */}
|
||||
<ProfileCard
|
||||
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-weight.png"
|
||||
title="体重"
|
||||
value={profile.weight ? `${round(profile.weight, 1)}公斤` : '55公斤'}
|
||||
title={t('editProfile.fields.weight')}
|
||||
value={profile.weight ? `${round(profile.weight, 1)}${t('editProfile.weight.unit')}` : t('editProfile.weight.placeholder')}
|
||||
onPress={() => {
|
||||
setTempValue(profile.weight ? String(round(profile.weight, 1)) : '55');
|
||||
setTempValue(profile.weight ? String(round(profile.weight, 1)) : String(t('editProfile.defaultValues.weight')));
|
||||
setEditingField('weight');
|
||||
}}
|
||||
/>
|
||||
@@ -366,14 +368,14 @@ export default function EditProfileScreen() {
|
||||
{/* 活动水平 */}
|
||||
<ProfileCard
|
||||
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-activity.png'
|
||||
title="活动水平"
|
||||
title={t('editProfile.fields.activityLevel')}
|
||||
value={(() => {
|
||||
switch (profile.activityLevel) {
|
||||
case 1: return '久坐';
|
||||
case 2: return '轻度活跃';
|
||||
case 3: return '中度活跃';
|
||||
case 4: return '非常活跃';
|
||||
default: return '久坐';
|
||||
case 1: return t('editProfile.activityLevels.1');
|
||||
case 2: return t('editProfile.activityLevels.2');
|
||||
case 3: return t('editProfile.activityLevels.3');
|
||||
case 4: return t('editProfile.activityLevels.4');
|
||||
default: return t('editProfile.activityLevels.1');
|
||||
}
|
||||
})()}
|
||||
onPress={() => {
|
||||
@@ -384,15 +386,20 @@ export default function EditProfileScreen() {
|
||||
{/* 出生日期 */}
|
||||
<ProfileCard
|
||||
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-birth.png'
|
||||
title="出生日期"
|
||||
title={t('editProfile.fields.birthDate')}
|
||||
value={profile.birthDate ? (() => {
|
||||
try {
|
||||
const d = new Date(profile.birthDate);
|
||||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`;
|
||||
if (t('editProfile.birthDate.format').includes('{{year}}年')) {
|
||||
return t('editProfile.birthDate.format', { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate() });
|
||||
} else {
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
return t('editProfile.birthDate.format', { year: d.getFullYear(), month: monthNames[d.getMonth()], day: d.getDate() });
|
||||
}
|
||||
} catch {
|
||||
return '1995年1月1日';
|
||||
return t('editProfile.birthDate.placeholder');
|
||||
}
|
||||
})() : '1995年1月1日'}
|
||||
})() : t('editProfile.birthDate.placeholder')}
|
||||
onPress={() => {
|
||||
openDatePicker();
|
||||
}}
|
||||
@@ -402,11 +409,11 @@ export default function EditProfileScreen() {
|
||||
<ProfileCard
|
||||
icon="heart"
|
||||
iconColor="#FF6B9D"
|
||||
title="最大心率"
|
||||
value={profile.maxHeartRate ? `${Math.round(profile.maxHeartRate)}次/分钟` : '未获取'}
|
||||
title={t('editProfile.fields.maxHeartRate')}
|
||||
value={profile.maxHeartRate ? `${Math.round(profile.maxHeartRate)}${t('editProfile.maxHeartRate.unit')}` : t('editProfile.maxHeartRate.notAvailable')}
|
||||
onPress={() => {
|
||||
// 最大心率不可编辑,只显示
|
||||
Alert.alert('提示', '最大心率数据从健康应用自动获取');
|
||||
Alert.alert(t('editProfile.maxHeartRate.alert.title'), t('editProfile.maxHeartRate.alert.message'));
|
||||
}}
|
||||
disabled={true}
|
||||
hideArrow={true}
|
||||
@@ -432,6 +439,7 @@ export default function EditProfileScreen() {
|
||||
} else if (field === 'gender') {
|
||||
updatedProfile.gender = value as 'male' | 'female';
|
||||
setProfile(p => ({ ...p, gender: value as 'male' | 'female' }));
|
||||
|
||||
} else if (field === 'height') {
|
||||
updatedProfile.height = parseFloat(value) || undefined;
|
||||
setProfile(p => ({ ...p, height: parseFloat(value) || undefined }));
|
||||
@@ -455,8 +463,8 @@ export default function EditProfileScreen() {
|
||||
colors={colors}
|
||||
textColor={textColor}
|
||||
placeholderColor={placeholderColor}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* 出生日期选择器弹窗 */}
|
||||
<Modal
|
||||
visible={datePickerVisible}
|
||||
@@ -488,12 +496,12 @@ export default function EditProfileScreen() {
|
||||
{Platform.OS === 'ios' && (
|
||||
<View style={styles.modalActions}>
|
||||
<Pressable onPress={closeDatePicker} style={[styles.modalBtn]}>
|
||||
<Text style={styles.modalBtnText}>取消</Text>
|
||||
<Text style={styles.modalBtnText}>{t('editProfile.modals.cancel')}</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => {
|
||||
onConfirmDate(pickerDate);
|
||||
}} style={[styles.modalBtn, styles.modalBtnPrimary]}>
|
||||
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>确定</Text>
|
||||
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>{t('editProfile.modals.confirm')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
@@ -536,7 +544,7 @@ function ProfileCard({ icon, iconUri, iconColor, title, value, onPress, disabled
|
||||
);
|
||||
}
|
||||
|
||||
function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor }: {
|
||||
function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor, t }: {
|
||||
visible: boolean;
|
||||
field: string | null;
|
||||
value: string;
|
||||
@@ -546,6 +554,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
||||
colors: any;
|
||||
textColor: string;
|
||||
placeholderColor: string;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [selectedGender, setSelectedGender] = useState(profile.gender || 'female');
|
||||
@@ -563,10 +572,10 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
||||
case 'name':
|
||||
return (
|
||||
<View>
|
||||
<Text style={styles.modalTitle}>昵称</Text>
|
||||
<Text style={styles.modalTitle}>{t('editProfile.fields.name')}</Text>
|
||||
<TextInput
|
||||
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
|
||||
placeholder="输入昵称"
|
||||
placeholder={t('editProfile.modals.input.namePlaceholder')}
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={inputValue}
|
||||
onChangeText={setInputValue}
|
||||
@@ -577,21 +586,21 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
||||
case 'gender':
|
||||
return (
|
||||
<View>
|
||||
<Text style={styles.modalTitle}>性别</Text>
|
||||
<Text style={styles.modalTitle}>{t('editProfile.fields.gender')}</Text>
|
||||
<View style={styles.genderSelector}>
|
||||
<TouchableOpacity
|
||||
style={[styles.genderOption, selectedGender === 'female' && { backgroundColor: colors.primary + '20' }]}
|
||||
onPress={() => setSelectedGender('female')}
|
||||
>
|
||||
<Text style={[styles.genderEmoji, selectedGender === 'female' && { color: colors.primary }]}>♀</Text>
|
||||
<Text style={[styles.genderText, selectedGender === 'female' && { color: colors.primary }]}>女性</Text>
|
||||
<Text style={[styles.genderText, selectedGender === 'female' && { color: colors.primary }]}>{t('editProfile.modals.female')}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.genderOption, selectedGender === 'male' && { backgroundColor: colors.primary + '20' }]}
|
||||
onPress={() => setSelectedGender('male')}
|
||||
>
|
||||
<Text style={[styles.genderEmoji, selectedGender === 'male' && { color: colors.primary }]}>♂</Text>
|
||||
<Text style={[styles.genderText, selectedGender === 'male' && { color: colors.primary }]}>男性</Text>
|
||||
<Text style={[styles.genderText, selectedGender === 'male' && { color: colors.primary }]}>{t('editProfile.modals.male')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -599,7 +608,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
||||
case 'height':
|
||||
return (
|
||||
<View>
|
||||
<Text style={styles.modalTitle}>身高</Text>
|
||||
<Text style={styles.modalTitle}>{t('editProfile.fields.height')}</Text>
|
||||
<View style={styles.pickerContainer}>
|
||||
<Picker
|
||||
selectedValue={inputValue}
|
||||
@@ -607,7 +616,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
||||
style={styles.picker}
|
||||
>
|
||||
{Array.from({ length: 101 }, (_, i) => 120 + i).map(height => (
|
||||
<Picker.Item key={height} label={`${height}厘米`} value={String(height)} />
|
||||
<Picker.Item key={height} label={`${height}${t('editProfile.height.unit')}`} value={String(height)} />
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
@@ -616,29 +625,29 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
||||
case 'weight':
|
||||
return (
|
||||
<View>
|
||||
<Text style={styles.modalTitle}>体重</Text>
|
||||
<Text style={styles.modalTitle}>{t('editProfile.fields.weight')}</Text>
|
||||
<TextInput
|
||||
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
|
||||
placeholder="输入体重"
|
||||
placeholder={t('editProfile.modals.input.weightPlaceholder')}
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={inputValue}
|
||||
onChangeText={setInputValue}
|
||||
keyboardType="numeric"
|
||||
autoFocus
|
||||
/>
|
||||
<Text style={styles.unitText}>公斤 (kg)</Text>
|
||||
<Text style={styles.unitText}>{t('editProfile.modals.input.weightUnit')}</Text>
|
||||
</View>
|
||||
);
|
||||
case 'activity':
|
||||
return (
|
||||
<View>
|
||||
<Text style={styles.modalTitle}>活动水平</Text>
|
||||
<Text style={styles.modalTitle}>{t('editProfile.fields.activityLevel')}</Text>
|
||||
<View style={styles.activitySelector}>
|
||||
{[
|
||||
{ key: 1, label: '久坐', desc: '很少运动' },
|
||||
{ key: 2, label: '轻度活跃', desc: '每周1-3次运动' },
|
||||
{ key: 3, label: '中度活跃', desc: '每周3-5次运动' },
|
||||
{ key: 4, label: '非常活跃', desc: '每周6-7次运动' },
|
||||
{ key: 1, label: t('editProfile.activityLevels.1'), desc: t('editProfile.activityLevels.descriptions.1') },
|
||||
{ key: 2, label: t('editProfile.activityLevels.2'), desc: t('editProfile.activityLevels.descriptions.2') },
|
||||
{ key: 3, label: t('editProfile.activityLevels.3'), desc: t('editProfile.activityLevels.descriptions.3') },
|
||||
{ key: 4, label: t('editProfile.activityLevels.4'), desc: t('editProfile.activityLevels.descriptions.4') },
|
||||
].map(item => (
|
||||
<TouchableOpacity
|
||||
key={item.key}
|
||||
@@ -668,7 +677,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
||||
{renderContent()}
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.modalCancelBtn}>
|
||||
<Text style={styles.modalCancelText}>取消</Text>
|
||||
<Text style={styles.modalCancelText}>{t('editProfile.modals.cancel')}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
@@ -682,7 +691,7 @@ function EditModal({ visible, field, value, profile, onClose, onSave, colors, te
|
||||
}}
|
||||
style={[styles.modalSaveBtn, { backgroundColor: colors.primary }]}
|
||||
>
|
||||
<Text style={[styles.modalSaveText, { color: colors.onPrimary }]}>保存</Text>
|
||||
<Text style={[styles.modalSaveText, { color: colors.onPrimary }]}>{t('editProfile.modals.save')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user