feat(i18n): 实现应用国际化支持,添加中英文翻译
- 为所有UI组件添加国际化支持,替换硬编码文本 - 新增useI18n钩子函数统一管理翻译 - 完善中英文翻译资源,覆盖统计、用药、通知设置等模块 - 优化Tab布局使用翻译键值替代静态文本 - 更新药品管理、个人资料编辑等页面的多语言支持
This commit is contained in:
@@ -3,6 +3,7 @@ import { GlassContainer, GlassView, isLiquidGlassAvailable } from 'expo-glass-ef
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { Tabs, usePathname } from 'expo-router';
|
||||
import { Icon, Label, NativeTabs } from 'expo-router/unstable-native-tabs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import React from 'react';
|
||||
import { Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
||||
@@ -16,18 +17,19 @@ import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
// Tab configuration
|
||||
type TabConfig = {
|
||||
icon: string;
|
||||
title: string;
|
||||
titleKey: string;
|
||||
};
|
||||
|
||||
const TAB_CONFIGS: Record<string, TabConfig> = {
|
||||
statistics: { icon: 'chart.pie.fill', title: '健康' },
|
||||
medications: { icon: 'pills.fill', title: '用药' },
|
||||
fasting: { icon: 'timer', title: '断食' },
|
||||
challenges: { icon: 'trophy.fill', title: '挑战' },
|
||||
personal: { icon: 'person.fill', title: '个人' },
|
||||
statistics: { icon: 'chart.pie.fill', titleKey: 'statistics.tabs.health' },
|
||||
medications: { icon: 'pills.fill', titleKey: 'statistics.tabs.medications' },
|
||||
fasting: { icon: 'timer', titleKey: 'statistics.tabs.fasting' },
|
||||
challenges: { icon: 'trophy.fill', titleKey: 'statistics.tabs.challenges' },
|
||||
personal: { icon: 'person.fill', titleKey: 'statistics.tabs.personal' },
|
||||
};
|
||||
|
||||
export default function TabLayout() {
|
||||
const { t } = useTranslation();
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const pathname = usePathname();
|
||||
@@ -96,7 +98,7 @@ export default function TabLayout() {
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{tabConfig.title}
|
||||
{t(tabConfig.titleKey)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
@@ -175,24 +177,24 @@ export default function TabLayout() {
|
||||
if (glassEffectAvailable) {
|
||||
return <NativeTabs>
|
||||
<NativeTabs.Trigger name="statistics">
|
||||
<Label>健康</Label>
|
||||
<Label>{t('statistics.tabs.health')}</Label>
|
||||
<Icon sf="chart.pie.fill" drawable="custom_android_drawable" />
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="medications">
|
||||
<Icon sf="pills.fill" drawable="custom_android_drawable" />
|
||||
<Label>用药</Label>
|
||||
<Label>{t('statistics.tabs.medications')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="fasting">
|
||||
<Icon sf="timer" drawable="custom_android_drawable" />
|
||||
<Label>断食</Label>
|
||||
<Label>{t('statistics.tabs.fasting')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="challenges">
|
||||
<Icon sf="trophy.fill" drawable="custom_android_drawable" />
|
||||
<Label>挑战</Label>
|
||||
<Label>{t('statistics.tabs.challenges')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="personal">
|
||||
<Icon sf="person.fill" drawable="custom_settings_drawable" />
|
||||
<Label>我的</Label>
|
||||
<Label>{t('statistics.tabs.personal')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
</NativeTabs>
|
||||
}
|
||||
@@ -203,11 +205,11 @@ export default function TabLayout() {
|
||||
screenOptions={({ route }) => getScreenOptions(route.name)}
|
||||
>
|
||||
|
||||
<Tabs.Screen name="statistics" options={{ title: '健康' }} />
|
||||
<Tabs.Screen name="medications" options={{ title: '用药' }} />
|
||||
<Tabs.Screen name="fasting" options={{ title: '断食' }} />
|
||||
<Tabs.Screen name="challenges" options={{ title: '挑战' }} />
|
||||
<Tabs.Screen name="personal" options={{ title: '个人' }} />
|
||||
<Tabs.Screen name="statistics" options={{ title: t('statistics.tabs.health') }} />
|
||||
<Tabs.Screen name="medications" options={{ title: t('statistics.tabs.medications') }} />
|
||||
<Tabs.Screen name="fasting" options={{ title: t('statistics.tabs.fasting') }} />
|
||||
<Tabs.Screen name="challenges" options={{ title: t('statistics.tabs.challenges') }} />
|
||||
<Tabs.Screen name="personal" options={{ title: t('statistics.tabs.personal') }} />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DateSelector } from '@/components/DateSelector';
|
||||
import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation';
|
||||
import { DateSelector } from '@/components/DateSelector';
|
||||
import { MedicationCard } from '@/components/medication/MedicationCard';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
@@ -17,6 +17,7 @@ import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
@@ -32,6 +33,7 @@ type MedicationFilter = 'all' | 'taken' | 'missed';
|
||||
type ThemeColors = (typeof Colors)[keyof typeof Colors];
|
||||
|
||||
export default function MedicationsScreen() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
@@ -147,8 +149,8 @@ export default function MedicationsScreen() {
|
||||
|
||||
const displayName = userProfile.name?.trim() || DEFAULT_MEMBER_NAME;
|
||||
const headerDateLabel = selectedDate.isSame(dayjs(), 'day')
|
||||
? `今天,${selectedDate.format('M月D日')}`
|
||||
: selectedDate.format('M月D日 dddd');
|
||||
? t('medications.dateFormats.today', { date: selectedDate.format('M月D日') })
|
||||
: t('medications.dateFormats.other', { date: selectedDate.format('M月D日 dddd') });
|
||||
|
||||
const emptyState = filteredMedications.length === 0;
|
||||
|
||||
@@ -178,9 +180,9 @@ export default function MedicationsScreen() {
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<ThemedText style={styles.greeting}>你好,{displayName}</ThemedText>
|
||||
<ThemedText style={styles.greeting}>{t('medications.greeting', { name: displayName })}</ThemedText>
|
||||
<ThemedText style={[styles.welcome, { color: colors.textMuted }]}>
|
||||
欢迎来到用药助手!
|
||||
{t('medications.welcome')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View style={styles.headerActions}>
|
||||
@@ -239,15 +241,10 @@ export default function MedicationsScreen() {
|
||||
</View>
|
||||
|
||||
<View style={styles.sectionSpacing}>
|
||||
<ThemedText style={styles.sectionHeader}>今日用药</ThemedText>
|
||||
<ThemedText style={styles.sectionHeader}>{t('medications.todayMedications')}</ThemedText>
|
||||
<View style={[styles.segmentedControl, { backgroundColor: colors.surface }]}>
|
||||
{(['all', 'taken', 'missed'] as MedicationFilter[]).map((filter) => {
|
||||
const isActive = activeFilter === filter;
|
||||
const labelMap: Record<MedicationFilter, string> = {
|
||||
all: '全部',
|
||||
taken: '已服用',
|
||||
missed: '未服用',
|
||||
};
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={filter}
|
||||
@@ -263,7 +260,7 @@ export default function MedicationsScreen() {
|
||||
{ color: isActive ? colors.onPrimary : colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{labelMap[filter]}
|
||||
{t(`medications.filters.${filter}`)}
|
||||
</ThemedText>
|
||||
<View
|
||||
style={[
|
||||
@@ -295,9 +292,9 @@ export default function MedicationsScreen() {
|
||||
style={styles.emptyIllustration}
|
||||
contentFit="cover"
|
||||
/>
|
||||
<ThemedText style={styles.emptyTitle}>今日暂无用药安排</ThemedText>
|
||||
<ThemedText style={styles.emptyTitle}>{t('medications.emptyState.title')}</ThemedText>
|
||||
<ThemedText style={[styles.emptySubtitle, { color: colors.textMuted }]}>
|
||||
还未添加任何用药计划,快来补充吧。
|
||||
{t('medications.emptyState.subtitle')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
) : (
|
||||
|
||||
@@ -24,6 +24,7 @@ import dayjs from 'dayjs';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AppState,
|
||||
Image,
|
||||
@@ -55,6 +56,7 @@ const FloatingCard = ({ children, style }: {
|
||||
};
|
||||
|
||||
export default function ExploreScreen() {
|
||||
const { t } = useTranslation();
|
||||
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
||||
|
||||
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
|
||||
@@ -351,7 +353,7 @@ export default function ExploreScreen() {
|
||||
|
||||
{/* 右边文字区域 */}
|
||||
<View style={styles.headerTextContainer}>
|
||||
<Text style={styles.headerTitle}>Out Live</Text>
|
||||
<Text style={styles.headerTitle}>{t('statistics.title')}</Text>
|
||||
</View>
|
||||
|
||||
{/* 开发环境调试按钮 */}
|
||||
@@ -360,7 +362,7 @@ export default function ExploreScreen() {
|
||||
<TouchableOpacity
|
||||
style={styles.debugButton}
|
||||
onPress={async () => {
|
||||
console.log('🔧 手动触发后台任务测试...');
|
||||
console.log('🔧 Manual background task test...');
|
||||
await BackgroundTaskManager.getInstance().triggerTaskForTesting();
|
||||
}}
|
||||
>
|
||||
@@ -370,7 +372,7 @@ export default function ExploreScreen() {
|
||||
<TouchableOpacity
|
||||
style={[styles.debugButton, styles.hrvTestButton]}
|
||||
onPress={async () => {
|
||||
console.log('🫀 测试HRV数据获取...');
|
||||
console.log('🫀 Testing HRV data fetch...');
|
||||
await testHRVDataFetch();
|
||||
}}
|
||||
>
|
||||
@@ -407,7 +409,7 @@ export default function ExploreScreen() {
|
||||
|
||||
{/* 身体指标section标题 */}
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>身体指标</Text>
|
||||
<Text style={styles.sectionTitle}>{t('statistics.sections.bodyMetrics')}</Text>
|
||||
</View>
|
||||
|
||||
{/* 真正瀑布流布局 */}
|
||||
|
||||
Reference in New Issue
Block a user