feat(i18n): 全面实现应用核心功能模块的国际化支持
- 新增 i18n 翻译资源,覆盖睡眠、饮水、体重、锻炼、用药 AI 识别、步数、健身圆环、基础代谢及设置等核心模块 - 重构相关页面及组件(如 SleepDetail, WaterDetail, WorkoutHistory 等)使用 `useI18n` 钩子替换硬编码文本 - 升级 `utils/date` 工具库与 `DateSelector` 组件,支持基于语言环境的日期格式化与显示 - 完善登录页、注销流程及权限申请弹窗的双语提示信息 - 优化部分页面的 UI 细节与字体样式以适配多语言显示
This commit is contained in:
@@ -4,6 +4,7 @@ import { Colors } from '@/constants/Colors';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { createMedicationRecognitionTask } from '@/services/medications';
|
||||
import { getItem, setItem } from '@/utils/kvStore';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@@ -39,9 +40,9 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
const MEDICATION_GUIDE_SEEN_KEY = 'medication_ai_camera_guide_seen';
|
||||
|
||||
const captureSteps = [
|
||||
{ key: 'front', title: '正面', subtitle: '保证药品名称清晰可见', mandatory: true },
|
||||
{ key: 'side', title: '背面', subtitle: '包含规格、成分等信息', mandatory: true },
|
||||
{ key: 'aux', title: '侧面', subtitle: '补充更多细节提升准确率', mandatory: false },
|
||||
{ key: 'front', mandatory: true },
|
||||
{ key: 'side', mandatory: true },
|
||||
{ key: 'aux', mandatory: false },
|
||||
] as const;
|
||||
|
||||
type CaptureKey = (typeof captureSteps)[number]['key'];
|
||||
@@ -51,6 +52,7 @@ type Shot = {
|
||||
};
|
||||
|
||||
export default function MedicationAiCameraScreen() {
|
||||
const { t } = useI18n();
|
||||
const insets = useSafeAreaInsets();
|
||||
const scheme = (useColorScheme() ?? 'light') as keyof typeof Colors;
|
||||
const colors = Colors[scheme];
|
||||
@@ -113,7 +115,14 @@ export default function MedicationAiCameraScreen() {
|
||||
}
|
||||
}, [allRequiredCaptured]);
|
||||
|
||||
const stepTitle = useMemo(() => `步骤 ${currentStepIndex + 1} / ${captureSteps.length}`, [currentStepIndex]);
|
||||
const stepTitle = useMemo(
|
||||
() =>
|
||||
t('medications.aiCamera.steps.stepProgress', {
|
||||
current: currentStepIndex + 1,
|
||||
total: captureSteps.length,
|
||||
}),
|
||||
[currentStepIndex, t]
|
||||
);
|
||||
|
||||
// 计算固定的相机高度,不受按钮状态影响,避免布局跳动
|
||||
const cameraHeight = useMemo(() => {
|
||||
@@ -149,7 +158,7 @@ export default function MedicationAiCameraScreen() {
|
||||
if (!result.canceled && result.assets?.length) {
|
||||
const asset = result.assets[0];
|
||||
setShots((prev) => ({ ...prev, [currentStep.key]: { uri: asset.uri } }));
|
||||
|
||||
|
||||
// 拍摄完成后自动进入下一步(如果还有下一步)
|
||||
if (currentStepIndex < captureSteps.length - 1) {
|
||||
setTimeout(() => {
|
||||
@@ -159,7 +168,10 @@ export default function MedicationAiCameraScreen() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MEDICATION_AI] pick image failed', error);
|
||||
Alert.alert('选择失败', '请重试或更换图片');
|
||||
Alert.alert(
|
||||
t('medications.aiCamera.alerts.pickFailed.title'),
|
||||
t('medications.aiCamera.alerts.pickFailed.message')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -169,7 +181,7 @@ export default function MedicationAiCameraScreen() {
|
||||
const photo = await cameraRef.current.takePictureAsync({ quality: 0.85 });
|
||||
if (photo?.uri) {
|
||||
setShots((prev) => ({ ...prev, [currentStep.key]: { uri: photo.uri } }));
|
||||
|
||||
|
||||
// 拍摄完成后自动进入下一步(如果还有下一步)
|
||||
if (currentStepIndex < captureSteps.length - 1) {
|
||||
setTimeout(() => {
|
||||
@@ -179,7 +191,10 @@ export default function MedicationAiCameraScreen() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MEDICATION_AI] take picture failed', error);
|
||||
Alert.alert('拍摄失败', '请重试');
|
||||
Alert.alert(
|
||||
t('medications.aiCamera.alerts.captureFailed.title'),
|
||||
t('medications.aiCamera.alerts.captureFailed.message')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,7 +207,10 @@ export default function MedicationAiCameraScreen() {
|
||||
const handleStartRecognition = async () => {
|
||||
// 检查必需照片是否完成
|
||||
if (!allRequiredCaptured) {
|
||||
Alert.alert('照片不足', '请至少完成正面和背面拍摄');
|
||||
Alert.alert(
|
||||
t('medications.aiCamera.alerts.insufficientPhotos.title'),
|
||||
t('medications.aiCamera.alerts.insufficientPhotos.message')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -209,7 +227,9 @@ export default function MedicationAiCameraScreen() {
|
||||
const [frontUpload, sideUpload, auxUpload] = await Promise.all([
|
||||
upload({ uri: shots.front.uri, name: `front-${Date.now()}.jpg`, type: 'image/jpeg' }),
|
||||
upload({ uri: shots.side.uri, name: `side-${Date.now()}.jpg`, type: 'image/jpeg' }),
|
||||
shots.aux ? upload({ uri: shots.aux.uri, name: `aux-${Date.now()}.jpg`, type: 'image/jpeg' }) : Promise.resolve(null),
|
||||
shots.aux
|
||||
? upload({ uri: shots.aux.uri, name: `aux-${Date.now()}.jpg`, type: 'image/jpeg' })
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const task = await createMedicationRecognitionTask({
|
||||
@@ -227,7 +247,10 @@ export default function MedicationAiCameraScreen() {
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[MEDICATION_AI] recognize failed', error);
|
||||
Alert.alert('创建任务失败', error?.message || '请检查网络后重试');
|
||||
Alert.alert(
|
||||
t('medications.aiCamera.alerts.taskFailed.title'),
|
||||
error?.message || t('medications.aiCamera.alerts.taskFailed.defaultMessage')
|
||||
);
|
||||
} finally {
|
||||
setCreatingTask(false);
|
||||
}
|
||||
@@ -278,12 +301,16 @@ export default function MedicationAiCameraScreen() {
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="camera-reverse-outline" size={20} color="#0f172a" />
|
||||
<Text style={styles.secondaryBtnText}>翻转</Text>
|
||||
<Text style={styles.secondaryBtnText}>
|
||||
{t('medications.aiCamera.buttons.flip')}
|
||||
</Text>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.secondaryBtn, styles.fallbackSecondaryBtn]}>
|
||||
<Ionicons name="camera-reverse-outline" size={20} color="#0f172a" />
|
||||
<Text style={styles.secondaryBtnText}>翻转</Text>
|
||||
<Text style={styles.secondaryBtnText}>
|
||||
{t('medications.aiCamera.buttons.flip')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@@ -440,12 +467,16 @@ export default function MedicationAiCameraScreen() {
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="camera" size={20} color="#0ea5e9" />
|
||||
<Text style={styles.splitButtonLabel}>拍照</Text>
|
||||
<Text style={styles.splitButtonLabel}>
|
||||
{t('medications.aiCamera.buttons.capture')}
|
||||
</Text>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.splitButton, styles.fallbackSplitButton]}>
|
||||
<Ionicons name="camera" size={20} color="#0ea5e9" />
|
||||
<Text style={styles.splitButtonLabel}>拍照</Text>
|
||||
<Text style={styles.splitButtonLabel}>
|
||||
{t('medications.aiCamera.buttons.capture')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@@ -470,7 +501,9 @@ export default function MedicationAiCameraScreen() {
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="checkmark-circle" size={20} color="#10b981" />
|
||||
<Text style={styles.splitButtonLabel}>完成</Text>
|
||||
<Text style={styles.splitButtonLabel}>
|
||||
{t('medications.aiCamera.buttons.complete')}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</GlassView>
|
||||
@@ -481,7 +514,9 @@ export default function MedicationAiCameraScreen() {
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="checkmark-circle" size={20} color="#10b981" />
|
||||
<Text style={styles.splitButtonLabel}>完成</Text>
|
||||
<Text style={styles.splitButtonLabel}>
|
||||
{t('medications.aiCamera.buttons.complete')}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
@@ -501,12 +536,25 @@ export default function MedicationAiCameraScreen() {
|
||||
if (!permission.granted) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: '#f8fafc' }]}>
|
||||
<HeaderBar title="AI 用药识别" onBack={() => router.back()} transparent />
|
||||
<HeaderBar
|
||||
title={t('medications.aiCamera.title')}
|
||||
onBack={() => router.back()}
|
||||
transparent
|
||||
/>
|
||||
<View style={[styles.permissionCard, { marginTop: insets.top + 60 }]}>
|
||||
<Text style={styles.permissionTitle}>需要相机权限</Text>
|
||||
<Text style={styles.permissionTip}>授权后即可快速拍摄药品包装,自动识别信息</Text>
|
||||
<TouchableOpacity style={[styles.permissionBtn, { backgroundColor: colors.primary }]} onPress={requestPermission}>
|
||||
<Text style={styles.permissionBtnText}>授权访问相机</Text>
|
||||
<Text style={styles.permissionTitle}>
|
||||
{t('medications.aiCamera.permission.title')}
|
||||
</Text>
|
||||
<Text style={styles.permissionTip}>
|
||||
{t('medications.aiCamera.permission.description')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.permissionBtn, { backgroundColor: colors.primary }]}
|
||||
onPress={requestPermission}
|
||||
>
|
||||
<Text style={styles.permissionBtnText}>
|
||||
{t('medications.aiCamera.permission.button')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -524,14 +572,14 @@ export default function MedicationAiCameraScreen() {
|
||||
<View style={styles.container}>
|
||||
<LinearGradient colors={['#fefefe', '#f4f7fb']} style={StyleSheet.absoluteFill} />
|
||||
<HeaderBar
|
||||
title="AI 用药识别"
|
||||
title={t('medications.aiCamera.title')}
|
||||
onBack={() => router.back()}
|
||||
transparent
|
||||
right={
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowGuideModal(true)}
|
||||
activeOpacity={0.7}
|
||||
accessibilityLabel="查看拍摄说明"
|
||||
accessibilityLabel={t('medications.aiCamera.guideModal.title')}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
@@ -556,8 +604,12 @@ export default function MedicationAiCameraScreen() {
|
||||
<View style={styles.metaBadge}>
|
||||
<Text style={styles.metaBadgeText}>{stepTitle}</Text>
|
||||
</View>
|
||||
<Text style={styles.metaTitle}>{currentStep.title}</Text>
|
||||
<Text style={styles.metaSubtitle}>{currentStep.subtitle}</Text>
|
||||
<Text style={styles.metaTitle}>
|
||||
{t(`medications.aiCamera.steps.${currentStep.key}.title`)}
|
||||
</Text>
|
||||
<Text style={styles.metaSubtitle}>
|
||||
{t(`medications.aiCamera.steps.${currentStep.key}.subtitle`)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.cameraCard}>
|
||||
@@ -587,14 +639,22 @@ export default function MedicationAiCameraScreen() {
|
||||
style={[styles.shotCard, active && styles.shotCardActive]}
|
||||
>
|
||||
<Text style={[styles.shotLabel, active && styles.shotLabelActive]}>
|
||||
{step.title}
|
||||
{!step.mandatory ? '(可选)' : ''}
|
||||
{t(`medications.aiCamera.steps.${step.key}.title`)}
|
||||
{!step.mandatory
|
||||
? ` ${t('medications.aiCamera.steps.optional')}`
|
||||
: ''}
|
||||
</Text>
|
||||
{shot ? (
|
||||
<Image source={{ uri: shot.uri }} style={styles.shotThumb} contentFit="cover" />
|
||||
<Image
|
||||
source={{ uri: shot.uri }}
|
||||
style={styles.shotThumb}
|
||||
contentFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.shotPlaceholder}>
|
||||
<Text style={styles.shotPlaceholderText}>未拍摄</Text>
|
||||
<Text style={styles.shotPlaceholderText}>
|
||||
{t('medications.aiCamera.steps.notTaken')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@@ -617,12 +677,16 @@ export default function MedicationAiCameraScreen() {
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="images-outline" size={20} color="#0f172a" />
|
||||
<Text style={styles.secondaryBtnText}>从相册</Text>
|
||||
<Text style={styles.secondaryBtnText}>
|
||||
{t('medications.aiCamera.buttons.album')}
|
||||
</Text>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.secondaryBtn, styles.fallbackSecondaryBtn]}>
|
||||
<Ionicons name="images-outline" size={20} color="#0f172a" />
|
||||
<Text style={styles.secondaryBtnText}>从相册</Text>
|
||||
<Text style={styles.secondaryBtnText}>
|
||||
{t('medications.aiCamera.buttons.album')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
Reference in New Issue
Block a user