Files
digital-pilates/app/(tabs)/_layout.tsx
richarjiang 2dca3253e6 feat(i18n): 实现应用国际化支持,添加中英文翻译
- 为所有UI组件添加国际化支持,替换硬编码文本
- 新增useI18n钩子函数统一管理翻译
- 完善中英文翻译资源,覆盖统计、用药、通知设置等模块
- 优化Tab布局使用翻译键值替代静态文本
- 更新药品管理、个人资料编辑等页面的多语言支持
2025-11-13 11:09:55 +08:00

216 lines
7.1 KiB
TypeScript

import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs';
import { GlassContainer, GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
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';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { Colors } from '@/constants/Colors';
import { ROUTES } from '@/constants/Routes';
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
import { useColorScheme } from '@/hooks/useColorScheme';
// Tab configuration
type TabConfig = {
icon: string;
titleKey: string;
};
const TAB_CONFIGS: Record<string, TabConfig> = {
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();
const glassEffectAvailable = isLiquidGlassAvailable();
// Helper function to determine if a tab is selected
const isTabSelected = (routeName: string): boolean => {
const routeMap: Record<string, string> = {
statistics: ROUTES.TAB_STATISTICS,
medications: ROUTES.TAB_MEDICATIONS,
fasting: ROUTES.TAB_FASTING,
challenges: ROUTES.TAB_CHALLENGES,
personal: ROUTES.TAB_PERSONAL,
};
return routeMap[routeName] === pathname || pathname.includes(routeName);
};
// Custom tab button component
const createTabButton = (routeName: string) => (props: any) => {
const { onPress } = props;
const tabConfig = TAB_CONFIGS[routeName];
if (!tabConfig) return null;
const isSelected = isTabSelected(routeName);
const handlePress = (event: any) => {
if (process.env.EXPO_OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
onPress?.(event);
};
return (
<TouchableOpacity
onPress={handlePress}
accessibilityRole="button"
activeOpacity={1}
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
marginHorizontal: 2,
marginVertical: 10,
borderRadius: 25,
backgroundColor: isSelected ? colorTokens.tabBarActiveBackground : 'transparent',
paddingHorizontal: isSelected ? 8 : 4,
paddingVertical: 8,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<IconSymbol
size={22}
name={tabConfig.icon as any}
color={isSelected ? colorTokens.tabIconSelected : colorTokens.tabIconDefault}
/>
{isSelected && (
<Text
style={{
color: colorTokens.tabIconSelected,
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
}}
numberOfLines={1}
>
{t(tabConfig.titleKey)}
</Text>
)}
</View>
</TouchableOpacity>
);
};
// Custom tab bar background component
const TabBarBackground = () => {
if (glassEffectAvailable) {
return (
<GlassContainer
spacing={8}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 34,
}}
>
<GlassView
isInteractive
glassEffectStyle="regular"
tintColor={theme === 'dark' ? 'rgba(0,0,0,0.3)' : 'rgba(255,255,255,0.3)'}
style={{
flex: 1,
borderRadius: 34,
}}
/>
</GlassContainer>
);
}
return null;
};
// Common screen options
const getScreenOptions = (routeName: string): BottomTabNavigationOptions => ({
headerShown: false,
tabBarActiveTintColor: colorTokens.tabIconSelected,
tabBarButton: createTabButton(routeName),
tabBarBackground: TabBarBackground,
tabBarStyle: {
position: 'absolute',
bottom: TAB_BAR_BOTTOM_OFFSET,
height: TAB_BAR_HEIGHT,
borderRadius: 34,
backgroundColor: glassEffectAvailable ? 'transparent' : colorTokens.tabBarBackground,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: glassEffectAvailable ? 0.1 : 0.2,
shadowRadius: 10,
elevation: 5,
paddingHorizontal: 6,
paddingTop: 0,
paddingBottom: 0,
marginHorizontal: 16,
left: 16,
right: 16,
alignSelf: 'center',
borderWidth: glassEffectAvailable ? 1 : 0,
borderColor: glassEffectAvailable ? (theme === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)') : 'transparent',
} as ViewStyle,
tabBarItemStyle: {
backgroundColor: 'transparent',
height: TAB_BAR_HEIGHT,
marginTop: 0,
marginBottom: 0,
paddingTop: 0,
paddingBottom: 0,
},
tabBarShowLabel: false,
});
if (glassEffectAvailable) {
return <NativeTabs>
<NativeTabs.Trigger name="statistics">
<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>{t('statistics.tabs.medications')}</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="fasting">
<Icon sf="timer" drawable="custom_android_drawable" />
<Label>{t('statistics.tabs.fasting')}</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="challenges">
<Icon sf="trophy.fill" drawable="custom_android_drawable" />
<Label>{t('statistics.tabs.challenges')}</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="personal">
<Icon sf="person.fill" drawable="custom_settings_drawable" />
<Label>{t('statistics.tabs.personal')}</Label>
</NativeTabs.Trigger>
</NativeTabs>
}
return (
<Tabs
initialRouteName="statistics"
screenOptions={({ route }) => getScreenOptions(route.name)}
>
<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>
);
}