Replace workouts tab with explore tab connected to Supabase
- Rename workouts.tsx to explore.tsx with new functionality - Add horizontal scrolling collections section with gradient cards - Add featured workouts section - Implement filtering by category (All, Full Body, Upper Body, Lower Body, Core, Cardio) - Implement filtering by level (All Levels, Beginner, Intermediate, Advanced) - Implement filtering by equipment (All, No Equipment, Band, Dumbbells, Mat) - Add clear filters button when filters are active - Add loading states with ActivityIndicator - Add empty state for no results - Update tab label from "Workouts" to "Explore" - Add explore translations for en, fr, de, es
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* TabataFit Tab Layout
|
||||
* Native iOS tabs with liquid glass effect
|
||||
* 5 tabs: Home, Workouts, Activity, Browse, Profile
|
||||
* 4 tabs: Home, Workouts, Activity, Profile
|
||||
* Redirects to onboarding if not completed
|
||||
*/
|
||||
|
||||
@@ -28,9 +28,9 @@ export default function TabLayout() {
|
||||
<Label>{t('tabs.home')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
|
||||
<NativeTabs.Trigger name="workouts">
|
||||
<NativeTabs.Trigger name="explore">
|
||||
<Icon sf={{ default: 'flame', selected: 'flame.fill' }} />
|
||||
<Label>{t('tabs.workouts')}</Label>
|
||||
<Label>{t('tabs.explore')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
|
||||
<NativeTabs.Trigger name="activity">
|
||||
@@ -38,11 +38,6 @@ export default function TabLayout() {
|
||||
<Label>{t('tabs.activity')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
|
||||
<NativeTabs.Trigger name="browse">
|
||||
<Icon sf={{ default: 'square.grid.2x2', selected: 'square.grid.2x2.fill' }} />
|
||||
<Label>{t('tabs.browse')}</Label>
|
||||
</NativeTabs.Trigger>
|
||||
|
||||
<NativeTabs.Trigger name="profile">
|
||||
<Icon sf={{ default: 'person', selected: 'person.fill' }} />
|
||||
<Label>{t('tabs.profile')}</Label>
|
||||
|
||||
549
app/(tabs)/explore.tsx
Normal file
549
app/(tabs)/explore.tsx
Normal file
@@ -0,0 +1,549 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { View, StyleSheet, ScrollView, Pressable, Dimensions, ActivityIndicator, FlatList } from 'react-native'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { BlurView } from 'expo-blur'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import Ionicons from '@expo/vector-icons/Ionicons'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHaptics } from '@/src/shared/hooks'
|
||||
import { useWorkouts, useCollections, useFeaturedWorkouts, useTrainers } from '@/src/shared/hooks/useSupabaseData'
|
||||
import { StyledText } from '@/src/shared/components/StyledText'
|
||||
|
||||
import { useThemeColors, BRAND } from '@/src/shared/theme'
|
||||
import type { ThemeColors } from '@/src/shared/theme/types'
|
||||
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
|
||||
import { RADIUS } from '@/src/shared/constants/borderRadius'
|
||||
import type { Workout, WorkoutCategory, WorkoutLevel } from '@/src/shared/types'
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window')
|
||||
const CARD_WIDTH = (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2
|
||||
const COLLECTION_CARD_WIDTH = SCREEN_WIDTH * 0.7
|
||||
|
||||
type FilterState = {
|
||||
category: WorkoutCategory | 'all'
|
||||
level: WorkoutLevel | 'all'
|
||||
equipment: string | 'all'
|
||||
}
|
||||
|
||||
const CATEGORY_ICONS: Record<WorkoutCategory | 'all', string> = {
|
||||
all: 'grid-outline',
|
||||
'full-body': 'body-outline',
|
||||
'upper-body': 'barbell-outline',
|
||||
'lower-body': 'footsteps-outline',
|
||||
core: 'fitness-outline',
|
||||
cardio: 'heart-outline',
|
||||
}
|
||||
|
||||
const CATEGORY_COLORS: Record<WorkoutCategory | 'all', string> = {
|
||||
all: BRAND.PRIMARY,
|
||||
'full-body': '#5AC8FA',
|
||||
'upper-body': '#FF6B35',
|
||||
'lower-body': '#30D158',
|
||||
core: '#FF9500',
|
||||
cardio: '#FF3B30',
|
||||
}
|
||||
|
||||
const CATEGORY_TRANSLATION_KEYS: Record<WorkoutCategory | 'all', string> = {
|
||||
all: 'all',
|
||||
'full-body': 'fullBody',
|
||||
'upper-body': 'upperBody',
|
||||
'lower-body': 'lowerBody',
|
||||
core: 'core',
|
||||
cardio: 'cardio',
|
||||
}
|
||||
|
||||
function CollectionCard({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
gradient,
|
||||
workoutCount,
|
||||
onPress,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
icon: string
|
||||
gradient?: [string, string]
|
||||
workoutCount: number
|
||||
onPress: () => void
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
const haptics = useHaptics()
|
||||
const gradientColors = gradient || [BRAND.PRIMARY, '#FF3B30']
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.collectionCard}
|
||||
onPress={() => {
|
||||
haptics.buttonTap()
|
||||
onPress()
|
||||
}}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<View style={styles.collectionOverlay}>
|
||||
<View style={styles.collectionIcon}>
|
||||
<Ionicons name={icon as any} size={28} color="#FFFFFF" />
|
||||
</View>
|
||||
<StyledText size={18} weight="bold" color="#FFFFFF" style={{ marginTop: SPACING[3] }}>
|
||||
{title}
|
||||
</StyledText>
|
||||
<StyledText size={13} color="rgba(255,255,255,0.8)" numberOfLines={2} style={{ marginTop: SPACING[1] }}>
|
||||
{description}
|
||||
</StyledText>
|
||||
<StyledText size={12} color="rgba(255,255,255,0.6)" style={{ marginTop: SPACING[2] }}>
|
||||
{workoutCount} workouts
|
||||
</StyledText>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkoutCard({
|
||||
workout,
|
||||
onPress,
|
||||
}: {
|
||||
workout: Workout
|
||||
onPress: () => void
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
const categoryColor = CATEGORY_COLORS[workout.category]
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.workoutCard, { borderColor: colors.border.glassLight }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<BlurView intensity={colors.glass.blurMedium} tint={colors.glass.blurTint} style={StyleSheet.absoluteFill} />
|
||||
<LinearGradient
|
||||
colors={[categoryColor + '20', 'transparent']}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
|
||||
<View style={[styles.durationBadge, { backgroundColor: categoryColor + '30' }]}>
|
||||
<Ionicons name="time-outline" size={10} color={categoryColor} />
|
||||
<StyledText size={11} weight="semibold" color={categoryColor} style={{ marginLeft: 3 }}>
|
||||
{workout.duration} min
|
||||
</StyledText>
|
||||
</View>
|
||||
|
||||
<View style={styles.playArea}>
|
||||
<View style={[styles.playCircle, { backgroundColor: categoryColor + '20' }]}>
|
||||
<Ionicons name="play" size={18} color={categoryColor} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.workoutInfo}>
|
||||
<StyledText size={14} weight="semibold" color={colors.text.primary} numberOfLines={2}>
|
||||
{workout.title}
|
||||
</StyledText>
|
||||
<StyledText size={11} color={colors.text.tertiary} style={{ marginTop: 2 }}>
|
||||
{workout.level} · {workout.calories} cal
|
||||
</StyledText>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
function FilterPill({
|
||||
label,
|
||||
isSelected,
|
||||
color,
|
||||
onPress,
|
||||
}: {
|
||||
label: string
|
||||
isSelected: boolean
|
||||
color: string
|
||||
onPress: () => void
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
const haptics = useHaptics()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[
|
||||
styles.filterPill,
|
||||
{
|
||||
backgroundColor: isSelected ? color + '20' : colors.glass.base.backgroundColor,
|
||||
borderColor: isSelected ? color : colors.border.glass,
|
||||
},
|
||||
]}
|
||||
onPress={() => {
|
||||
haptics.selection()
|
||||
onPress()
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
size={13}
|
||||
weight={isSelected ? 'semibold' : 'medium'}
|
||||
color={isSelected ? color : colors.text.secondary}
|
||||
>
|
||||
{label}
|
||||
</StyledText>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadingSkeleton() {
|
||||
const colors = useThemeColors()
|
||||
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={BRAND.PRIMARY} />
|
||||
<StyledText size={15} color={colors.text.secondary} style={{ marginTop: SPACING[3] }}>
|
||||
Loading workouts...
|
||||
</StyledText>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ExploreScreen() {
|
||||
const { t } = useTranslation()
|
||||
const insets = useSafeAreaInsets()
|
||||
const router = useRouter()
|
||||
const haptics = useHaptics()
|
||||
const colors = useThemeColors()
|
||||
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
category: 'all',
|
||||
level: 'all',
|
||||
equipment: 'all',
|
||||
})
|
||||
|
||||
const { data: workoutsData, isLoading: workoutsLoading } = useWorkouts()
|
||||
const { data: collectionsData, isLoading: collectionsLoading } = useCollections()
|
||||
const { data: featuredData } = useFeaturedWorkouts()
|
||||
|
||||
const workouts = workoutsData || []
|
||||
const collections = collectionsData || []
|
||||
const featured = featuredData || []
|
||||
|
||||
const filteredWorkouts = useMemo(() => {
|
||||
return workouts.filter((workout) => {
|
||||
if (filters.category !== 'all' && workout.category !== filters.category) return false
|
||||
if (filters.level !== 'all' && workout.level !== filters.level) return false
|
||||
if (filters.equipment !== 'all') {
|
||||
if (filters.equipment === 'none') {
|
||||
return workout.equipment.length === 0
|
||||
}
|
||||
return workout.equipment.some((e) => e.toLowerCase().includes(filters.equipment.toLowerCase()))
|
||||
}
|
||||
return true
|
||||
})
|
||||
}, [workouts, filters])
|
||||
|
||||
const categories: (WorkoutCategory | 'all')[] = ['all', 'full-body', 'upper-body', 'lower-body', 'core', 'cardio']
|
||||
const levels: (WorkoutLevel | 'all')[] = ['all', 'Beginner', 'Intermediate', 'Advanced']
|
||||
const equipmentOptions = ['all', 'none', 'band', 'dumbbells', 'mat']
|
||||
|
||||
const handleWorkoutPress = (id: string) => {
|
||||
haptics.buttonTap()
|
||||
router.push(`/workout/${id}`)
|
||||
}
|
||||
|
||||
const handleCollectionPress = (id: string) => {
|
||||
haptics.buttonTap()
|
||||
router.push(`/collection/${id}` as any)
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
haptics.selection()
|
||||
setFilters({ category: 'all', level: 'all', equipment: 'all' })
|
||||
}
|
||||
|
||||
const hasActiveFilters = filters.category !== 'all' || filters.level !== 'all' || filters.equipment !== 'all'
|
||||
|
||||
if (workoutsLoading || collectionsLoading) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.bg.base, paddingTop: insets.top }]}>
|
||||
<LoadingSkeleton />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.bg.base, paddingTop: insets.top }]}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<StyledText size={34} weight="bold" color={colors.text.primary}>
|
||||
{t('screens:explore.title')}
|
||||
</StyledText>
|
||||
<StyledText size={15} color={colors.text.tertiary}>
|
||||
{t('screens:explore.workoutsCount', { count: workouts.length })}
|
||||
</StyledText>
|
||||
</View>
|
||||
|
||||
{collections.length > 0 && (
|
||||
<View style={styles.collectionsSection}>
|
||||
<StyledText size={22} weight="bold" color={colors.text.primary}>
|
||||
{t('screens:explore.collections')}
|
||||
</StyledText>
|
||||
<FlatList
|
||||
horizontal
|
||||
data={collections}
|
||||
keyExtractor={(item) => item.id}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.collectionsList}
|
||||
renderItem={({ item }) => (
|
||||
<CollectionCard
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
icon={item.icon}
|
||||
gradient={item.gradient}
|
||||
workoutCount={item.workoutIds.length}
|
||||
onPress={() => handleCollectionPress(item.id)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{featured.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<StyledText size={22} weight="bold" color={colors.text.primary}>
|
||||
{t('screens:explore.featured')}
|
||||
</StyledText>
|
||||
<View style={styles.workoutsGrid}>
|
||||
{featured.slice(0, 4).map((workout) => (
|
||||
<WorkoutCard
|
||||
key={workout.id}
|
||||
workout={workout}
|
||||
onPress={() => handleWorkoutPress(workout.id)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.filtersSection}>
|
||||
<View style={styles.filterHeader}>
|
||||
<StyledText size={22} weight="bold" color={colors.text.primary}>
|
||||
{t('screens:explore.allWorkouts')}
|
||||
</StyledText>
|
||||
{hasActiveFilters && (
|
||||
<Pressable onPress={clearFilters}>
|
||||
<StyledText size={14} weight="medium" color={BRAND.PRIMARY}>
|
||||
{t('screens:explore.clearFilters')}
|
||||
</StyledText>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.filterRow}>
|
||||
<StyledText size={12} weight="semibold" color={colors.text.tertiary}>
|
||||
{t('screens:explore.filterCategory')}
|
||||
</StyledText>
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterScroll}>
|
||||
{categories.map((cat) => (
|
||||
<FilterPill
|
||||
key={cat}
|
||||
label={t(`common:categories.${CATEGORY_TRANSLATION_KEYS[cat]}`)}
|
||||
isSelected={filters.category === cat}
|
||||
color={CATEGORY_COLORS[cat]}
|
||||
onPress={() => setFilters((f) => ({ ...f, category: cat }))}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
<View style={[styles.filterRow, { marginTop: SPACING[3] }]}>
|
||||
<StyledText size={12} weight="semibold" color={colors.text.tertiary}>
|
||||
{t('screens:explore.filterLevel')}
|
||||
</StyledText>
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterScroll}>
|
||||
{levels.map((level) => (
|
||||
<FilterPill
|
||||
key={level}
|
||||
label={level === 'all' ? 'All Levels' : level}
|
||||
isSelected={filters.level === level}
|
||||
color={BRAND.PRIMARY}
|
||||
onPress={() => setFilters((f) => ({ ...f, level }))}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
<View style={[styles.filterRow, { marginTop: SPACING[3] }]}>
|
||||
<StyledText size={12} weight="semibold" color={colors.text.tertiary}>
|
||||
{t('screens:explore.filterEquipment')}
|
||||
</StyledText>
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterScroll}>
|
||||
{equipmentOptions.map((eq) => (
|
||||
<FilterPill
|
||||
key={eq}
|
||||
label={eq === 'all' ? 'All Equipment' : t(`screens:explore.equipmentOptions.${eq}`)}
|
||||
isSelected={filters.equipment === eq}
|
||||
color={BRAND.PRIMARY}
|
||||
onPress={() => setFilters((f) => ({ ...f, equipment: eq }))}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{filteredWorkouts.length > 0 ? (
|
||||
<View style={styles.workoutsGrid}>
|
||||
{filteredWorkouts.map((workout) => (
|
||||
<WorkoutCard
|
||||
key={workout.id}
|
||||
workout={workout}
|
||||
onPress={() => handleWorkoutPress(workout.id)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.noResults}>
|
||||
<Ionicons name="search-outline" size={48} color={colors.text.tertiary} />
|
||||
<StyledText size={17} weight="semibold" color={colors.text.secondary} style={{ marginTop: SPACING[3] }}>
|
||||
{t('screens:explore.noResults')}
|
||||
</StyledText>
|
||||
<StyledText size={14} color={colors.text.tertiary} style={{ marginTop: SPACING[1] }}>
|
||||
{t('screens:explore.tryAdjustingFilters')}
|
||||
</StyledText>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: LAYOUT.SCREEN_PADDING,
|
||||
},
|
||||
header: {
|
||||
marginBottom: SPACING[6],
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
collectionsSection: {
|
||||
marginBottom: SPACING[6],
|
||||
},
|
||||
collectionsList: {
|
||||
marginTop: SPACING[4],
|
||||
paddingRight: LAYOUT.SCREEN_PADDING,
|
||||
},
|
||||
collectionCard: {
|
||||
width: COLLECTION_CARD_WIDTH,
|
||||
height: 180,
|
||||
borderRadius: RADIUS.XL,
|
||||
marginRight: SPACING[3],
|
||||
overflow: 'hidden',
|
||||
},
|
||||
collectionOverlay: {
|
||||
flex: 1,
|
||||
padding: SPACING[5],
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
},
|
||||
collectionIcon: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
section: {
|
||||
marginBottom: SPACING[6],
|
||||
},
|
||||
|
||||
filtersSection: {
|
||||
marginBottom: SPACING[6],
|
||||
},
|
||||
filterHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: SPACING[4],
|
||||
},
|
||||
filterRow: {
|
||||
marginBottom: SPACING[2],
|
||||
},
|
||||
filterScroll: {
|
||||
marginHorizontal: -LAYOUT.SCREEN_PADDING,
|
||||
paddingHorizontal: LAYOUT.SCREEN_PADDING,
|
||||
},
|
||||
filterPill: {
|
||||
paddingHorizontal: SPACING[4],
|
||||
paddingVertical: SPACING[2],
|
||||
borderRadius: RADIUS.FULL,
|
||||
marginRight: SPACING[2],
|
||||
borderWidth: 1,
|
||||
borderCurve: 'continuous',
|
||||
},
|
||||
|
||||
workoutsGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: SPACING[3],
|
||||
},
|
||||
workoutCard: {
|
||||
width: CARD_WIDTH,
|
||||
height: 200,
|
||||
borderRadius: RADIUS.GLASS_CARD,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderCurve: 'continuous',
|
||||
},
|
||||
durationBadge: {
|
||||
position: 'absolute',
|
||||
top: SPACING[3],
|
||||
right: SPACING[3],
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: SPACING[2],
|
||||
paddingVertical: 3,
|
||||
borderRadius: RADIUS.SM,
|
||||
},
|
||||
playArea: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 70,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
playCircle: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
workoutInfo: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: SPACING[3],
|
||||
paddingTop: SPACING[2],
|
||||
},
|
||||
|
||||
noResults: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: SPACING[10],
|
||||
},
|
||||
})
|
||||
@@ -1,354 +0,0 @@
|
||||
/**
|
||||
* TabataFit Workouts Screen
|
||||
* Premium workout browser — scrollable category pills, trainers, workout grid
|
||||
*/
|
||||
|
||||
import { useState, useRef, useMemo } from 'react'
|
||||
import { View, StyleSheet, ScrollView, Pressable, Dimensions, Animated } from 'react-native'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import { BlurView } from 'expo-blur'
|
||||
import Ionicons from '@expo/vector-icons/Ionicons'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHaptics } from '@/src/shared/hooks'
|
||||
import { WORKOUTS } from '@/src/shared/data'
|
||||
import { useTranslatedCategories, useTranslatedWorkouts } from '@/src/shared/data/useTranslatedData'
|
||||
import { StyledText } from '@/src/shared/components/StyledText'
|
||||
|
||||
import { useThemeColors, BRAND, GRADIENTS } from '@/src/shared/theme'
|
||||
import type { ThemeColors } from '@/src/shared/theme/types'
|
||||
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
|
||||
import { RADIUS } from '@/src/shared/constants/borderRadius'
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window')
|
||||
const CARD_WIDTH = (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CATEGORY PILL
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function CategoryPill({
|
||||
label,
|
||||
selected,
|
||||
onPress,
|
||||
}: {
|
||||
label: string
|
||||
selected: boolean
|
||||
onPress: () => void
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.pill, selected && styles.pillSelected]}
|
||||
onPress={onPress}
|
||||
>
|
||||
{selected && (
|
||||
<LinearGradient
|
||||
colors={GRADIENTS.CTA}
|
||||
style={[StyleSheet.absoluteFill, { borderRadius: 20 }]}
|
||||
/>
|
||||
)}
|
||||
<StyledText
|
||||
size={14}
|
||||
weight={selected ? 'semibold' : 'regular'}
|
||||
color={selected ? '#FFFFFF' : colors.text.tertiary}
|
||||
>
|
||||
{label}
|
||||
</StyledText>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// WORKOUT CARD
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function WorkoutCard({
|
||||
title,
|
||||
duration,
|
||||
level,
|
||||
levelLabel,
|
||||
onPress,
|
||||
}: {
|
||||
title: string
|
||||
duration: number
|
||||
level: string
|
||||
levelLabel: string
|
||||
onPress: () => void
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
return (
|
||||
<Pressable style={styles.workoutCard} onPress={onPress}>
|
||||
<BlurView intensity={colors.glass.blurMedium} tint={colors.glass.blurTint} style={StyleSheet.absoluteFill} />
|
||||
|
||||
{/* Subtle gradient accent at top */}
|
||||
<LinearGradient
|
||||
colors={[levelColor(level, colors) + '30', 'transparent']}
|
||||
style={styles.cardGradient}
|
||||
/>
|
||||
|
||||
{/* Duration badge */}
|
||||
<View style={styles.durationBadge}>
|
||||
<Ionicons name="time-outline" size={10} color={colors.text.secondary} />
|
||||
<StyledText size={11} weight="semibold" color={colors.text.secondary} style={{ marginLeft: 3 }}>
|
||||
{duration + ' min'}
|
||||
</StyledText>
|
||||
</View>
|
||||
|
||||
{/* Play button */}
|
||||
<View style={styles.playArea}>
|
||||
<View style={styles.playCircle}>
|
||||
<Ionicons name="play" size={18} color={colors.text.primary} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Info */}
|
||||
<View style={styles.workoutInfo}>
|
||||
<StyledText size={14} weight="semibold" color={colors.text.primary} numberOfLines={2}>
|
||||
{title}
|
||||
</StyledText>
|
||||
<View style={styles.levelRow}>
|
||||
<View style={[styles.levelDot, { backgroundColor: levelColor(level, colors) }]} />
|
||||
<StyledText size={11} color={colors.text.tertiary}>
|
||||
{levelLabel}
|
||||
</StyledText>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
function levelColor(level: string, colors: ThemeColors): string {
|
||||
switch (level.toLowerCase()) {
|
||||
case 'beginner': return BRAND.SUCCESS
|
||||
case 'intermediate': return BRAND.SECONDARY
|
||||
case 'advanced': return BRAND.DANGER
|
||||
default: return colors.text.tertiary
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// MAIN SCREEN
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export default function WorkoutsScreen() {
|
||||
const { t } = useTranslation()
|
||||
const insets = useSafeAreaInsets()
|
||||
const router = useRouter()
|
||||
const haptics = useHaptics()
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
const [selectedCategory, setSelectedCategory] = useState('all')
|
||||
const categories = useTranslatedCategories()
|
||||
|
||||
const filteredWorkouts = selectedCategory === 'all'
|
||||
? WORKOUTS
|
||||
: WORKOUTS.filter(w => w.category === selectedCategory)
|
||||
|
||||
const translatedFiltered = useTranslatedWorkouts(filteredWorkouts)
|
||||
|
||||
const handleWorkoutPress = (id: string) => {
|
||||
haptics.buttonTap()
|
||||
router.push(`/workout/${id}`)
|
||||
}
|
||||
|
||||
const selectedLabel = categories.find(c => c.id === selectedCategory)?.label ?? t('screens:workouts.allWorkouts')
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<StyledText size={34} weight="bold" color={colors.text.primary}>
|
||||
{t('screens:workouts.title')}
|
||||
</StyledText>
|
||||
<StyledText size={15} color={colors.text.tertiary}>
|
||||
{t('screens:workouts.available', { count: WORKOUTS.length })}
|
||||
</StyledText>
|
||||
</View>
|
||||
|
||||
{/* Category Pills — horizontal scroll, no truncation */}
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.pillsRow}
|
||||
style={styles.pillsScroll}
|
||||
>
|
||||
{categories.map((cat) => (
|
||||
<CategoryPill
|
||||
key={cat.id}
|
||||
label={cat.label}
|
||||
selected={selectedCategory === cat.id}
|
||||
onPress={() => {
|
||||
haptics.selection()
|
||||
setSelectedCategory(cat.id)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* Workouts Grid */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<StyledText size={20} weight="semibold" color={colors.text.primary}>
|
||||
{selectedCategory === 'all' ? t('screens:workouts.allWorkouts') : selectedLabel}
|
||||
</StyledText>
|
||||
{selectedCategory !== 'all' && (
|
||||
<Pressable onPress={() => { haptics.buttonTap(); router.push(`/workout/category/${selectedCategory}`) }}>
|
||||
<StyledText size={14} color={BRAND.PRIMARY} weight="medium">{t('seeAll')}</StyledText>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.workoutsGrid}>
|
||||
{translatedFiltered.map((workout) => (
|
||||
<WorkoutCard
|
||||
key={workout.id}
|
||||
title={workout.title}
|
||||
duration={workout.duration}
|
||||
level={workout.level}
|
||||
levelLabel={t(`levels.${workout.level.toLowerCase()}`)}
|
||||
onPress={() => handleWorkoutPress(workout.id)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// STYLES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function createStyles(colors: ThemeColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.bg.base,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: LAYOUT.SCREEN_PADDING,
|
||||
},
|
||||
|
||||
// Header
|
||||
header: {
|
||||
marginBottom: SPACING[4],
|
||||
},
|
||||
|
||||
// Pills
|
||||
pillsScroll: {
|
||||
marginHorizontal: -LAYOUT.SCREEN_PADDING,
|
||||
marginBottom: SPACING[6],
|
||||
},
|
||||
pillsRow: {
|
||||
paddingHorizontal: LAYOUT.SCREEN_PADDING,
|
||||
gap: SPACING[2],
|
||||
},
|
||||
pill: {
|
||||
paddingHorizontal: SPACING[4],
|
||||
paddingVertical: SPACING[2],
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.bg.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border.glassLight,
|
||||
},
|
||||
pillSelected: {
|
||||
borderColor: BRAND.PRIMARY,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
|
||||
// Section
|
||||
section: {
|
||||
marginBottom: SPACING[6],
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: SPACING[4],
|
||||
},
|
||||
|
||||
// Workouts Grid
|
||||
workoutsGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: SPACING[3],
|
||||
},
|
||||
workoutCard: {
|
||||
width: CARD_WIDTH,
|
||||
height: 190,
|
||||
borderRadius: RADIUS.GLASS_CARD,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.bg.overlay2,
|
||||
},
|
||||
cardGradient: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 80,
|
||||
},
|
||||
durationBadge: {
|
||||
position: 'absolute',
|
||||
top: SPACING[3],
|
||||
right: SPACING[3],
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
paddingHorizontal: SPACING[2],
|
||||
paddingVertical: 3,
|
||||
borderRadius: RADIUS.SM,
|
||||
},
|
||||
playArea: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 64,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
playCircle: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.border.glassStrong,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.25)',
|
||||
},
|
||||
workoutInfo: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: SPACING[3],
|
||||
paddingTop: SPACING[2],
|
||||
},
|
||||
levelRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 3,
|
||||
gap: 5,
|
||||
},
|
||||
levelDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"tabs": {
|
||||
"home": "Start",
|
||||
"workouts": "Workouts",
|
||||
"explore": "Entdecken",
|
||||
"activity": "Aktivität",
|
||||
"browse": "Entdecken",
|
||||
"profile": "Profil"
|
||||
},
|
||||
|
||||
@@ -11,14 +10,38 @@
|
||||
"featured": "EMPFOHLEN",
|
||||
"recent": "Zuletzt",
|
||||
"popularThisWeek": "Beliebt diese Woche",
|
||||
"collections": "Sammlungen"
|
||||
"collections": "Sammlungen",
|
||||
"chooseYourPath": "W\u00e4hle deinen Weg",
|
||||
"continueYourJourney": "Setze deine Reise fort",
|
||||
"yourPrograms": "Deine Programme",
|
||||
"programsSubtitle": "W\u00e4hle deinen Fokus",
|
||||
"switchProgram": "Programm wechseln",
|
||||
"statsStreak": "Serie",
|
||||
"statsThisWeek": "Diese Woche",
|
||||
"statsMinutes": "Minuten"
|
||||
},
|
||||
|
||||
"workouts": {
|
||||
"title": "Workouts",
|
||||
"available": "{{count}} Workouts verfügbar",
|
||||
"explore": {
|
||||
"title": "Entdecken",
|
||||
"collections": "Sammlungen",
|
||||
"featured": "Empfohlen",
|
||||
"allWorkouts": "Alle Workouts",
|
||||
"noWorkouts": "Keine Workouts gefunden"
|
||||
"trainers": "Trainer",
|
||||
"noResults": "Keine Workouts gefunden",
|
||||
"tryAdjustingFilters": "Versuchen Sie, Ihre Filter anzupassen",
|
||||
"loading": "Wird geladen...",
|
||||
"filterCategory": "Kategorie",
|
||||
"filterLevel": "Niveau",
|
||||
"filterEquipment": "Ausrüstung",
|
||||
"filterDuration": "Dauer",
|
||||
"clearFilters": "Filter löschen",
|
||||
"workoutsCount": "{{count}} Workouts",
|
||||
"equipmentOptions": {
|
||||
"none": "Ohne Ausrüstung",
|
||||
"band": "Widerstandsband",
|
||||
"dumbbells": "Hanteln",
|
||||
"mat": "Matte"
|
||||
}
|
||||
},
|
||||
|
||||
"activity": {
|
||||
@@ -78,7 +101,41 @@
|
||||
"learnMore": "Mehr erfahren",
|
||||
"version": "Version",
|
||||
"privacyPolicy": "Datenschutzrichtlinie",
|
||||
"signOut": "Abmelden"
|
||||
"signOut": "Abmelden",
|
||||
"statsWorkouts": "Workouts",
|
||||
"statsStreak": "Tage in Folge",
|
||||
"statsCalories": "Kalorien",
|
||||
"faq": "FAQ",
|
||||
"contactUs": "Kontakt",
|
||||
"rateApp": "App bewerten",
|
||||
"sectionPremium": "Auf Premium upgraden",
|
||||
"sectionPersonalization": "PERSONALISIERUNG",
|
||||
"personalization": "Personalisierung",
|
||||
"personalizationEnabled": "KI-gestützte Empfehlungen aktiv",
|
||||
"personalizationDisabled": "Aktivieren für personalisierte Workouts",
|
||||
"enablePersonalization": "Personalisierung aktivieren",
|
||||
"deleteData": "Meine Daten löschen"
|
||||
},
|
||||
|
||||
"sync": {
|
||||
"title": "Personalisierte Workouts freischalten",
|
||||
"benefits": {
|
||||
"recommendations": "KI-gestützte Empfehlungen basierend auf Ihrem Fortschritt",
|
||||
"adaptive": "Adaptive Schwierigkeit, die mit Ihnen wächst",
|
||||
"sync": "Synchronisieren Sie Ihren Fortschritt über alle Geräte",
|
||||
"secure": "Ihre Daten sind verschlüsselt und sicher"
|
||||
},
|
||||
"privacy": "Wir speichern Ihren Trainingsverlauf, um Ihr personalisiertes Programm zu erstellen. Sie können diese Daten jederzeit in den Einstellungen löschen.",
|
||||
"primaryButton": "Mein Erlebnis personalisieren",
|
||||
"secondaryButton": "Mit generischen Programmen fortfahren"
|
||||
},
|
||||
|
||||
"dataDeletion": {
|
||||
"title": "Ihre Daten löschen?",
|
||||
"description": "Dies löscht dauerhaft Ihren synchronisierten Trainingsverlauf und Ihre Personalisierungsdaten von unseren Servern.",
|
||||
"note": "Ihr lokaler Trainingsverlauf wird auf diesem Gerät gespeichert. Sie fahren mit generischen Programmen fort.",
|
||||
"deleteButton": "Meine Daten löschen",
|
||||
"cancelButton": "Meine Daten behalten"
|
||||
},
|
||||
|
||||
"player": {
|
||||
@@ -212,5 +269,64 @@
|
||||
"restorePurchases": "K\u00e4ufe wiederherstellen",
|
||||
"skipButton": "Ohne Abo fortfahren"
|
||||
}
|
||||
},
|
||||
|
||||
"programs": {
|
||||
"title": "Programme",
|
||||
"weeks": "Wochen",
|
||||
"week": "Woche",
|
||||
"workouts": "Workouts",
|
||||
"workout": "Workout",
|
||||
"minutes": "Minuten",
|
||||
"min": "min",
|
||||
"perWeek": "/Woche",
|
||||
"equipment": "Ausr\u00fcstung",
|
||||
"optional": "(optional)",
|
||||
"bodyweightOnly": "Nur K\u00f6rpergewicht",
|
||||
"focusAreas": "Schwerpunkte",
|
||||
"exercises": "\u00dcbungen",
|
||||
"of": "von",
|
||||
"complete": "abgeschlossen",
|
||||
"completed": "Abgeschlossen",
|
||||
"notStarted": "Nicht gestartet",
|
||||
"inProgress": "In Bearbeitung",
|
||||
"allWorkoutsComplete": "Alle Workouts abgeschlossen!",
|
||||
"status": {
|
||||
"notStarted": "Nicht gestartet",
|
||||
"inProgress": "In Bearbeitung",
|
||||
"complete": "Abgeschlossen",
|
||||
"completed": "Abgeschlossen"
|
||||
},
|
||||
"yourProgress": "Ihr Fortschritt",
|
||||
"trainingPlan": "Trainingsplan",
|
||||
"current": "Aktuell",
|
||||
"startProgram": "Programm starten",
|
||||
"continue": "Fortsetzen",
|
||||
"continueTraining": "Training fortsetzen",
|
||||
"restart": "Neu starten",
|
||||
"restartProgram": "Programm neu starten"
|
||||
},
|
||||
|
||||
"assessment": {
|
||||
"title": "Bewegungsbewertung",
|
||||
"welcomeTitle": "Schnelle Bewertung",
|
||||
"welcomeDescription": "Lassen Sie uns Ihre Bewegungsmuster \u00fcberpr\u00fcfen, um Ihre Erfahrung zu personalisieren und den besten Startpunkt zu empfehlen.",
|
||||
"minutes": "Minuten",
|
||||
"quickCheck": "Schnelle \u00dcberpr\u00fcfung Ihres Fitnessniveaus",
|
||||
"movements": "Bewegungen",
|
||||
"testMovements": "Testen Sie wichtige Bewegungsmuster",
|
||||
"noEquipment": "Keine Ausr\u00fcstung erforderlich",
|
||||
"justYourBody": "Nur Ihr K\u00f6rpergewicht",
|
||||
"whatWeCheck": "Was wir \u00fcberpr\u00fcfen",
|
||||
"mobility": "Mobilit\u00e4t",
|
||||
"strength": "Kraft",
|
||||
"stability": "Stabilit\u00e4t",
|
||||
"balance": "Gleichgewicht",
|
||||
"takeAssessment": "Bewertung durchf\u00fchren",
|
||||
"startAssessment": "Bewertung starten",
|
||||
"skipForNow": "Vorerst \u00fcberspringen",
|
||||
"tips": "Tipps f\u00fcr beste Ergebnisse",
|
||||
"duration": "Dauer",
|
||||
"exercises": "\u00dcbungen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"tabs": {
|
||||
"home": "Home",
|
||||
"workouts": "Workouts",
|
||||
"explore": "Explore",
|
||||
"activity": "Activity",
|
||||
"browse": "Browse",
|
||||
"profile": "Profile"
|
||||
},
|
||||
|
||||
@@ -11,14 +10,38 @@
|
||||
"featured": "FEATURED",
|
||||
"recent": "Recent",
|
||||
"popularThisWeek": "Popular This Week",
|
||||
"collections": "Collections"
|
||||
"collections": "Collections",
|
||||
"chooseYourPath": "Choose Your Path",
|
||||
"continueYourJourney": "Continue Your Journey",
|
||||
"yourPrograms": "Your Programs",
|
||||
"programsSubtitle": "Choose your focus",
|
||||
"switchProgram": "Switch Program",
|
||||
"statsStreak": "Streak",
|
||||
"statsThisWeek": "This Week",
|
||||
"statsMinutes": "Minutes"
|
||||
},
|
||||
|
||||
"workouts": {
|
||||
"title": "Workouts",
|
||||
"available": "{{count}} workouts available",
|
||||
"explore": {
|
||||
"title": "Explore",
|
||||
"collections": "Collections",
|
||||
"featured": "Featured",
|
||||
"allWorkouts": "All Workouts",
|
||||
"noWorkouts": "No workouts found"
|
||||
"trainers": "Trainers",
|
||||
"noResults": "No workouts found",
|
||||
"tryAdjustingFilters": "Try adjusting your filters",
|
||||
"loading": "Loading...",
|
||||
"filterCategory": "Category",
|
||||
"filterLevel": "Level",
|
||||
"filterEquipment": "Equipment",
|
||||
"filterDuration": "Duration",
|
||||
"clearFilters": "Clear Filters",
|
||||
"workoutsCount": "{{count}} workouts",
|
||||
"equipmentOptions": {
|
||||
"none": "No Equipment",
|
||||
"band": "Resistance Band",
|
||||
"dumbbells": "Dumbbells",
|
||||
"mat": "Mat"
|
||||
}
|
||||
},
|
||||
|
||||
"activity": {
|
||||
@@ -85,7 +108,34 @@
|
||||
"faq": "FAQ",
|
||||
"contactUs": "Contact Us",
|
||||
"rateApp": "Rate App",
|
||||
"sectionPremium": "Upgrade to Premium"
|
||||
"sectionPremium": "Upgrade to Premium",
|
||||
"sectionPersonalization": "PERSONALIZATION",
|
||||
"personalization": "Personalization",
|
||||
"personalizationEnabled": "AI-powered recommendations active",
|
||||
"personalizationDisabled": "Enable for personalized workouts",
|
||||
"enablePersonalization": "Enable Personalization",
|
||||
"deleteData": "Delete My Data"
|
||||
},
|
||||
|
||||
"sync": {
|
||||
"title": "Unlock Personalized Workouts",
|
||||
"benefits": {
|
||||
"recommendations": "AI-powered recommendations based on your progress",
|
||||
"adaptive": "Adaptive difficulty that grows with you",
|
||||
"sync": "Sync your progress across devices",
|
||||
"secure": "Your data is encrypted and secure"
|
||||
},
|
||||
"privacy": "We'll save your workout history to create your personalized program. You can delete this data anytime in Settings.",
|
||||
"primaryButton": "Personalize My Experience",
|
||||
"secondaryButton": "Continue with Generic Programs"
|
||||
},
|
||||
|
||||
"dataDeletion": {
|
||||
"title": "Delete Your Data?",
|
||||
"description": "This will permanently delete your synced workout history and personalization data from our servers.",
|
||||
"note": "Your local workout history will be kept on this device. You'll continue with generic programs.",
|
||||
"deleteButton": "Delete My Data",
|
||||
"cancelButton": "Keep My Data"
|
||||
},
|
||||
|
||||
"player": {
|
||||
@@ -224,6 +274,23 @@
|
||||
"5x": "5x / week"
|
||||
}
|
||||
},
|
||||
"paywall": {
|
||||
"title": "Stay on track.\nNo limits.",
|
||||
"features": {
|
||||
"unlimited": "Unlimited Workouts",
|
||||
"offline": "Offline Downloads",
|
||||
"stats": "Advanced Stats & Apple Watch",
|
||||
"noAds": "No Ads + Family Sharing"
|
||||
},
|
||||
"bestValue": "BEST VALUE",
|
||||
"yearlyPrice": "$49.99",
|
||||
"monthlyPrice": "$6.99",
|
||||
"savePercent": "Save 42%",
|
||||
"trialCta": "START FREE TRIAL (7 days)",
|
||||
"guarantees": "Cancel anytime · 30-day money-back guarantee",
|
||||
"restorePurchases": "Restore Purchases",
|
||||
"skipButton": "Continue without subscription"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy Policy",
|
||||
"lastUpdated": "Last Updated: March 2026",
|
||||
@@ -261,5 +328,64 @@
|
||||
"content": "If you have questions about this privacy policy, please contact us at:"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"programs": {
|
||||
"title": "Programs",
|
||||
"weeks": "Weeks",
|
||||
"week": "Week",
|
||||
"workouts": "Workouts",
|
||||
"workout": "Workout",
|
||||
"minutes": "Minutes",
|
||||
"min": "min",
|
||||
"perWeek": "/week",
|
||||
"equipment": "Equipment",
|
||||
"optional": "(optional)",
|
||||
"bodyweightOnly": "Bodyweight only",
|
||||
"focusAreas": "Focus Areas",
|
||||
"exercises": "exercises",
|
||||
"of": "of",
|
||||
"complete": "complete",
|
||||
"completed": "Completed",
|
||||
"notStarted": "Not Started",
|
||||
"inProgress": "In Progress",
|
||||
"allWorkoutsComplete": "All Workouts Complete!",
|
||||
"status": {
|
||||
"notStarted": "Not Started",
|
||||
"inProgress": "In Progress",
|
||||
"complete": "Complete",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"yourProgress": "Your Progress",
|
||||
"trainingPlan": "Training Plan",
|
||||
"current": "Current",
|
||||
"startProgram": "Start Program",
|
||||
"continue": "Continue",
|
||||
"continueTraining": "Continue Training",
|
||||
"restart": "Restart",
|
||||
"restartProgram": "Restart Program"
|
||||
},
|
||||
|
||||
"assessment": {
|
||||
"title": "Movement Assessment",
|
||||
"welcomeTitle": "Quick Assessment",
|
||||
"welcomeDescription": "Let's check your movement patterns to personalize your experience and recommend the best starting point.",
|
||||
"minutes": "minutes",
|
||||
"quickCheck": "Quick check of your fitness level",
|
||||
"movements": "movements",
|
||||
"testMovements": "Test key movement patterns",
|
||||
"noEquipment": "No equipment needed",
|
||||
"justYourBody": "Just your bodyweight",
|
||||
"whatWeCheck": "What We'll Check",
|
||||
"mobility": "Mobility",
|
||||
"strength": "Strength",
|
||||
"stability": "Stability",
|
||||
"balance": "Balance",
|
||||
"takeAssessment": "Take Assessment",
|
||||
"startAssessment": "Start Assessment",
|
||||
"skipForNow": "Skip for now",
|
||||
"tips": "Tips for best results",
|
||||
"duration": "Duration",
|
||||
"exercises": "Exercises"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"tabs": {
|
||||
"home": "Inicio",
|
||||
"workouts": "Entrenos",
|
||||
"explore": "Explorar",
|
||||
"activity": "Actividad",
|
||||
"browse": "Explorar",
|
||||
"profile": "Perfil"
|
||||
},
|
||||
|
||||
@@ -11,14 +10,38 @@
|
||||
"featured": "DESTACADO",
|
||||
"recent": "Recientes",
|
||||
"popularThisWeek": "Popular esta semana",
|
||||
"collections": "Colecciones"
|
||||
"collections": "Colecciones",
|
||||
"chooseYourPath": "Elige tu camino",
|
||||
"continueYourJourney": "Contin\u00faa tu viaje",
|
||||
"yourPrograms": "Tus programas",
|
||||
"programsSubtitle": "Elige tu enfoque",
|
||||
"switchProgram": "Cambiar programa",
|
||||
"statsStreak": "Racha",
|
||||
"statsThisWeek": "Esta semana",
|
||||
"statsMinutes": "Minutos"
|
||||
},
|
||||
|
||||
"workouts": {
|
||||
"title": "Entrenos",
|
||||
"available": "{{count}} entrenos disponibles",
|
||||
"explore": {
|
||||
"title": "Explorar",
|
||||
"collections": "Colecciones",
|
||||
"featured": "Destacados",
|
||||
"allWorkouts": "Todos los entrenos",
|
||||
"noWorkouts": "No se encontraron entrenos"
|
||||
"trainers": "Entrenadores",
|
||||
"noResults": "No se encontraron entrenos",
|
||||
"tryAdjustingFilters": "Intenta ajustar tus filtros",
|
||||
"loading": "Cargando...",
|
||||
"filterCategory": "Categoría",
|
||||
"filterLevel": "Nivel",
|
||||
"filterEquipment": "Equipo",
|
||||
"filterDuration": "Duración",
|
||||
"clearFilters": "Borrar filtros",
|
||||
"workoutsCount": "{{count}} entrenos",
|
||||
"equipmentOptions": {
|
||||
"none": "Sin equipo",
|
||||
"band": "Banda elástica",
|
||||
"dumbbells": "Mancuernas",
|
||||
"mat": "Colchoneta"
|
||||
}
|
||||
},
|
||||
|
||||
"activity": {
|
||||
@@ -78,7 +101,41 @@
|
||||
"learnMore": "M\u00e1s informaci\u00f3n",
|
||||
"version": "Versi\u00f3n",
|
||||
"privacyPolicy": "Pol\u00edtica de privacidad",
|
||||
"signOut": "Cerrar sesi\u00f3n"
|
||||
"signOut": "Cerrar sesi\u00f3n",
|
||||
"statsWorkouts": "entrenos",
|
||||
"statsStreak": "d\u00edas consecutivos",
|
||||
"statsCalories": "calor\u00edas",
|
||||
"faq": "FAQ",
|
||||
"contactUs": "Contactarnos",
|
||||
"rateApp": "Calificar app",
|
||||
"sectionPremium": "Actualizar a Premium",
|
||||
"sectionPersonalization": "PERSONALIZACI\u00d3N",
|
||||
"personalization": "Personalizaci\u00f3n",
|
||||
"personalizationEnabled": "Recomendaciones IA activas",
|
||||
"personalizationDisabled": "Active para entrenos personalizados",
|
||||
"enablePersonalization": "Activar personalizaci\u00f3n",
|
||||
"deleteData": "Eliminar mis datos"
|
||||
},
|
||||
|
||||
"sync": {
|
||||
"title": "Desbloquea entrenos personalizados",
|
||||
"benefits": {
|
||||
"recommendations": "Recomendaciones IA basadas en tu progreso",
|
||||
"adaptive": "Dificultad adaptativa que crece contigo",
|
||||
"sync": "Sincroniza tu progreso en todos tus dispositivos",
|
||||
"secure": "Tus datos est\u00e1n cifrados y seguros"
|
||||
},
|
||||
"privacy": "Guardaremos tu historial de entrenos para crear tu programa personalizado. Puedes eliminar estos datos en cualquier momento en Configuraci\u00f3n.",
|
||||
"primaryButton": "Personalizar mi experiencia",
|
||||
"secondaryButton": "Continuar con programas gen\u00e9ricos"
|
||||
},
|
||||
|
||||
"dataDeletion": {
|
||||
"title": "\u00bfEliminar tus datos?",
|
||||
"description": "Esto eliminar\u00e1 permanentemente tu historial de entrenos sincronizado y tus datos de personalizaci\u00f3n de nuestros servidores.",
|
||||
"note": "Tu historial de entrenos local se mantendr\u00e1 en este dispositivo. Continuar\u00e1s con programas gen\u00e9ricos.",
|
||||
"deleteButton": "Eliminar mis datos",
|
||||
"cancelButton": "Conservar mis datos"
|
||||
},
|
||||
|
||||
"player": {
|
||||
@@ -212,5 +269,64 @@
|
||||
"restorePurchases": "Restaurar compras",
|
||||
"skipButton": "Continuar sin suscripci\u00f3n"
|
||||
}
|
||||
},
|
||||
|
||||
"programs": {
|
||||
"title": "Programas",
|
||||
"weeks": "Semanas",
|
||||
"week": "Semana",
|
||||
"workouts": "Entrenamientos",
|
||||
"workout": "Entrenamiento",
|
||||
"minutes": "Minutos",
|
||||
"min": "min",
|
||||
"perWeek": "/semana",
|
||||
"equipment": "Equipo",
|
||||
"optional": "(opcional)",
|
||||
"bodyweightOnly": "Solo peso corporal",
|
||||
"focusAreas": "\u00c1reas de enfoque",
|
||||
"exercises": "ejercicios",
|
||||
"of": "de",
|
||||
"complete": "completado",
|
||||
"completed": "Completado",
|
||||
"notStarted": "No iniciado",
|
||||
"inProgress": "En progreso",
|
||||
"allWorkoutsComplete": "\u00a1Todos los entrenamientos completados!",
|
||||
"status": {
|
||||
"notStarted": "No iniciado",
|
||||
"inProgress": "En progreso",
|
||||
"complete": "Completado",
|
||||
"completed": "Completado"
|
||||
},
|
||||
"yourProgress": "Tu progreso",
|
||||
"trainingPlan": "Plan de entrenamiento",
|
||||
"current": "Actual",
|
||||
"startProgram": "Iniciar programa",
|
||||
"continue": "Continuar",
|
||||
"continueTraining": "Continuar entrenamiento",
|
||||
"restart": "Reiniciar",
|
||||
"restartProgram": "Reiniciar programa"
|
||||
},
|
||||
|
||||
"assessment": {
|
||||
"title": "Evaluaci\u00f3n de movimiento",
|
||||
"welcomeTitle": "Evaluaci\u00f3n r\u00e1pida",
|
||||
"welcomeDescription": "Verifiquemos tus patrones de movimiento para personalizar tu experiencia y recomendar el mejor punto de partida.",
|
||||
"minutes": "minutos",
|
||||
"quickCheck": "Verificaci\u00f3n r\u00e1pida de tu nivel de fitness",
|
||||
"movements": "movimientos",
|
||||
"testMovements": "Prueba patrones de movimiento clave",
|
||||
"noEquipment": "No se necesita equipo",
|
||||
"justYourBody": "Solo tu peso corporal",
|
||||
"whatWeCheck": "Qu\u00e9 verificamos",
|
||||
"mobility": "Movilidad",
|
||||
"strength": "Fuerza",
|
||||
"stability": "Estabilidad",
|
||||
"balance": "Equilibrio",
|
||||
"takeAssessment": "Realizar evaluaci\u00f3n",
|
||||
"startAssessment": "Iniciar evaluaci\u00f3n",
|
||||
"skipForNow": "Omitir por ahora",
|
||||
"tips": "Consejos para mejores resultados",
|
||||
"duration": "Duraci\u00f3n",
|
||||
"exercises": "Ejercicios"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"tabs": {
|
||||
"home": "Accueil",
|
||||
"workouts": "Exercices",
|
||||
"activity": "Activit\u00e9",
|
||||
"browse": "Explorer",
|
||||
"explore": "Explorer",
|
||||
"activity": "Activité",
|
||||
"profile": "Profil"
|
||||
},
|
||||
|
||||
@@ -11,31 +10,55 @@
|
||||
"featured": "\u00c0 LA UNE",
|
||||
"recent": "R\u00e9cents",
|
||||
"popularThisWeek": "Populaires cette semaine",
|
||||
"collections": "Collections"
|
||||
"collections": "Collections",
|
||||
"chooseYourPath": "Choisissez votre parcours",
|
||||
"continueYourJourney": "Continuez votre parcours",
|
||||
"yourPrograms": "Vos programmes",
|
||||
"programsSubtitle": "Choisissez votre focus",
|
||||
"switchProgram": "Changer de programme",
|
||||
"statsStreak": "S\u00e9rie",
|
||||
"statsThisWeek": "Cette semaine",
|
||||
"statsMinutes": "Minutes"
|
||||
},
|
||||
|
||||
"workouts": {
|
||||
"title": "Exercices",
|
||||
"available": "{{count}} exercices disponibles",
|
||||
"explore": {
|
||||
"title": "Explorer",
|
||||
"collections": "Collections",
|
||||
"featured": "En vedette",
|
||||
"allWorkouts": "Tous les exercices",
|
||||
"noWorkouts": "Aucun exercice trouv\u00e9"
|
||||
"trainers": "Entraîneurs",
|
||||
"noResults": "Aucun exercice trouvé",
|
||||
"tryAdjustingFilters": "Essayez d'ajuster vos filtres",
|
||||
"loading": "Chargement...",
|
||||
"filterCategory": "Catégorie",
|
||||
"filterLevel": "Niveau",
|
||||
"filterEquipment": "Équipement",
|
||||
"filterDuration": "Durée",
|
||||
"clearFilters": "Effacer les filtres",
|
||||
"workoutsCount": "{{count}} exercices",
|
||||
"equipmentOptions": {
|
||||
"none": "Sans équipement",
|
||||
"band": "Bande élastique",
|
||||
"dumbbells": "Haltères",
|
||||
"mat": "Tapis"
|
||||
}
|
||||
},
|
||||
|
||||
"activity": {
|
||||
"title": "Activit\u00e9",
|
||||
"dayStreak": "jours cons\u00e9cutifs",
|
||||
"title": "Activité",
|
||||
"dayStreak": "jours consécutifs",
|
||||
"longest": "RECORD",
|
||||
"workouts": "Entra\u00eenements",
|
||||
"workouts": "Entraînements",
|
||||
"minutes": "Minutes",
|
||||
"calories": "Calories",
|
||||
"bestStreak": "Meilleure s\u00e9rie",
|
||||
"bestStreak": "Meilleure série",
|
||||
"thisWeek": "Cette semaine",
|
||||
"ofDays": "{{completed}} sur 7 jours",
|
||||
"recent": "R\u00e9cents",
|
||||
"recent": "Récents",
|
||||
"today": "Aujourd'hui",
|
||||
"yesterday": "Hier",
|
||||
"daysAgo": "il y a {{count}}j",
|
||||
"achievements": "Succ\u00e8s"
|
||||
"achievements": "Succès"
|
||||
},
|
||||
|
||||
"browse": {
|
||||
@@ -55,12 +78,12 @@
|
||||
|
||||
"profile": {
|
||||
"title": "Profil",
|
||||
"guest": "Invit\u00e9",
|
||||
"guest": "Invité",
|
||||
"memberSince": "Membre depuis {{date}}",
|
||||
"sectionAccount": "COMPTE",
|
||||
"sectionWorkout": "ENTRA\u00ceNEMENT",
|
||||
"sectionWorkout": "ENTRAÎNEMENT",
|
||||
"sectionNotifications": "NOTIFICATIONS",
|
||||
"sectionAbout": "\u00c0 PROPOS",
|
||||
"sectionAbout": "À PROPOS",
|
||||
"sectionSubscription": "ABONNEMENT",
|
||||
"email": "E-mail",
|
||||
"plan": "Formule",
|
||||
@@ -71,55 +94,82 @@
|
||||
"voiceCoaching": "Coaching vocal",
|
||||
"dailyReminders": "Rappels quotidiens",
|
||||
"reminderTime": "Heure du rappel",
|
||||
"reminderFooter": "Recevez un rappel quotidien pour maintenir votre s\u00e9rie",
|
||||
"workoutSettingsFooter": "Personnalisez votre exp\u00e9rience d'entra\u00eenement",
|
||||
"upgradeTitle": "D\u00e9bloquer TabataFit+",
|
||||
"upgradeDescription": "Acc\u00e9dez \u00e0 des entra\u00eenements illimit\u00e9s, t\u00e9l\u00e9chargements hors ligne et plus.",
|
||||
"reminderFooter": "Recevez un rappel quotidien pour maintenir votre série",
|
||||
"workoutSettingsFooter": "Personnalisez votre expérience d'entraînement",
|
||||
"upgradeTitle": "Débloquer TabataFit+",
|
||||
"upgradeDescription": "Accédez à des entraînements illimités, téléchargements hors ligne et plus.",
|
||||
"learnMore": "En savoir plus",
|
||||
"version": "Version",
|
||||
"privacyPolicy": "Politique de confidentialit\u00e9",
|
||||
"signOut": "Se d\u00e9connecter",
|
||||
"statsWorkouts": "entra\u00eenements",
|
||||
"statsStreak": "jours cons\u00e9cutifs",
|
||||
"privacyPolicy": "Politique de confidentialité",
|
||||
"signOut": "Se déconnecter",
|
||||
"statsWorkouts": "entraînements",
|
||||
"statsStreak": "jours consécutifs",
|
||||
"statsCalories": "calories",
|
||||
"faq": "FAQ",
|
||||
"contactUs": "Nous contacter",
|
||||
"rateApp": "Noter l'app",
|
||||
"sectionPremium": "Passer à Premium"
|
||||
"sectionPremium": "Passer à Premium",
|
||||
"sectionPersonalization": "PERSONNALISATION",
|
||||
"personalization": "Personnalisation",
|
||||
"personalizationEnabled": "Recommandations IA actives",
|
||||
"personalizationDisabled": "Activez pour des entraînements personnalisés",
|
||||
"enablePersonalization": "Activer la personnalisation",
|
||||
"deleteData": "Supprimer mes données"
|
||||
},
|
||||
|
||||
"sync": {
|
||||
"title": "Débloquez les entraînements personnalisés",
|
||||
"benefits": {
|
||||
"recommendations": "Recommandations IA basées sur vos progrès",
|
||||
"adaptive": "Difficulté adaptative qui évolue avec vous",
|
||||
"sync": "Synchronisez vos progrès sur tous vos appareils",
|
||||
"secure": "Vos données sont chiffrées et sécurisées"
|
||||
},
|
||||
"privacy": "Nous sauvegarderons votre historique d'entraînement pour créer votre programme personnalisé. Vous pouvez supprimer ces données à tout moment dans les Paramètres.",
|
||||
"primaryButton": "Personnaliser mon expérience",
|
||||
"secondaryButton": "Continuer avec les programmes génériques"
|
||||
},
|
||||
|
||||
"dataDeletion": {
|
||||
"title": "Supprimer vos données ?",
|
||||
"description": "Cela supprimera définitivement votre historique d'entraînement synchronisé et vos données de personnalisation de nos serveurs.",
|
||||
"note": "Votre historique d'entraînement local sera conservé sur cet appareil. Vous continuerez avec les programmes génériques.",
|
||||
"deleteButton": "Supprimer mes données",
|
||||
"cancelButton": "Conserver mes données"
|
||||
},
|
||||
|
||||
"player": {
|
||||
"phases": {
|
||||
"prep": "PR\u00c9PAREZ-VOUS",
|
||||
"prep": "PRÉPAREZ-VOUS",
|
||||
"work": "EFFORT",
|
||||
"rest": "REPOS",
|
||||
"complete": "TERMIN\u00c9"
|
||||
"complete": "TERMINÉ"
|
||||
},
|
||||
"current": "En cours",
|
||||
"next": "Suivant : ",
|
||||
"round": "Round",
|
||||
"burnBar": "Burn Bar",
|
||||
"communityAvg": "Moyenne communaut\u00e9 : {{calories}} cal",
|
||||
"workoutComplete": "Entra\u00eenement termin\u00e9 !",
|
||||
"greatJob": "Bien jou\u00e9 !",
|
||||
"communityAvg": "Moyenne communauté : {{calories}} cal",
|
||||
"workoutComplete": "Entraînement terminé !",
|
||||
"greatJob": "Bien joué !",
|
||||
"rounds": "Rounds",
|
||||
"calories": "Calories",
|
||||
"minutes": "Minutes"
|
||||
},
|
||||
|
||||
"complete": {
|
||||
"title": "ENTRA\u00ceNEMENT TERMIN\u00c9",
|
||||
"title": "ENTRAÎNEMENT TERMINÉ",
|
||||
"caloriesLabel": "CALORIES",
|
||||
"minutesLabel": "MINUTES",
|
||||
"completeLabel": "TERMIN\u00c9",
|
||||
"completeLabel": "TERMINÉ",
|
||||
"burnBar": "Burn Bar",
|
||||
"burnBarResult": "Vous avez battu {{percentile}}% des utilisateurs !",
|
||||
"streakTitle": "{{count}} jours cons\u00e9cutifs !",
|
||||
"streakSubtitle": "Continuez sur cette lanc\u00e9e !",
|
||||
"shareWorkout": "Partagez votre entra\u00eenement",
|
||||
"shareText": "Je viens de terminer {{title}} ! \uD83D\uDD25 {{calories}} calories en {{duration}} minutes.",
|
||||
"recommendedNext": "Recommand\u00e9 ensuite",
|
||||
"backToHome": "Retour \u00e0 l'accueil"
|
||||
"streakTitle": "{{count}} jours consécutifs !",
|
||||
"streakSubtitle": "Continuez sur cette lancée !",
|
||||
"shareWorkout": "Partagez votre entraînement",
|
||||
"shareText": "Je viens de terminer {{title}} ! 🔥 {{calories}} calories en {{duration}} minutes.",
|
||||
"recommendedNext": "Recommandé ensuite",
|
||||
"backToHome": "Retour à l'accueil"
|
||||
},
|
||||
|
||||
"collection": {
|
||||
@@ -132,14 +182,14 @@
|
||||
},
|
||||
|
||||
"workout": {
|
||||
"notFound": "Entra\u00eenement introuvable",
|
||||
"notFound": "Entraînement introuvable",
|
||||
"whatYoullNeed": "Ce qu'il vous faut",
|
||||
"exercises": "Exercices ({{count}} rounds)",
|
||||
"repeatRounds": "R\u00e9p\u00e9ter \u00D7 {{count}} rounds",
|
||||
"repeatRounds": "Répéter × {{count}} rounds",
|
||||
"music": "Musique",
|
||||
"musicMix": "Mix {{vibe}}",
|
||||
"curatedForWorkout": "S\u00e9lectionn\u00e9 pour votre entra\u00eenement",
|
||||
"startWorkout": "COMMENCER L'ENTRA\u00ceNEMENT"
|
||||
"curatedForWorkout": "Sélectionné pour votre entraînement",
|
||||
"startWorkout": "COMMENCER L'ENTRAÎNEMENT"
|
||||
},
|
||||
|
||||
"paywall": {
|
||||
@@ -168,19 +218,19 @@
|
||||
"problem": {
|
||||
"title": "Vous n'avez pas 1 heure\npour la salle.",
|
||||
"subtitle1": "Personne n'a le temps.",
|
||||
"subtitle2": "Pourtant vous voulez des r\u00e9sultats. On a la solution.",
|
||||
"subtitle2": "Pourtant vous voulez des résultats. On a la solution.",
|
||||
"cta": "Montrez-moi"
|
||||
},
|
||||
"empathy": {
|
||||
"title": "Qu'est-ce qui vous freine ?",
|
||||
"chooseUpTo": "Choisissez jusqu'\u00e0 2",
|
||||
"chooseUpTo": "Choisissez jusqu'à 2",
|
||||
"noTime": "Pas le temps",
|
||||
"lowMotivation": "Manque de motivation",
|
||||
"noKnowledge": "Je ne sais pas quoi faire",
|
||||
"noGym": "Pas d'acc\u00e8s \u00e0 une salle"
|
||||
"noGym": "Pas d'accès à une salle"
|
||||
},
|
||||
"solution": {
|
||||
"title": "4 minutes.\nV\u00e9ritablement transformateur.",
|
||||
"title": "4 minutes.\nVéritablement transformateur.",
|
||||
"tabataCalories": "85 kcal",
|
||||
"cardioCalories": "90 kcal",
|
||||
"tabata": "Tabata",
|
||||
@@ -188,35 +238,35 @@
|
||||
"tabataDuration": "4 min",
|
||||
"cardioDuration": "30 min",
|
||||
"vs": "VS",
|
||||
"citation": "\u00ab La m\u00e9thode Tabata augmente simultan\u00e9ment les capacit\u00e9s a\u00e9robie et ana\u00e9robie. \u00bb",
|
||||
"citationAuthor": "\u2014 Dr. Izumi Tabata, 1996",
|
||||
"citation": "« La méthode Tabata augmente simultanément les capacités aérobie et anaérobie. »",
|
||||
"citationAuthor": "— Dr. Izumi Tabata, 1996",
|
||||
"cta": "Je suis convaincu"
|
||||
},
|
||||
"wow": {
|
||||
"title": "Votre app, en avant-premi\u00e8re.",
|
||||
"title": "Votre app, en avant-première.",
|
||||
"subtitle": "Tout ce qu'il vous faut pour vous transformer.",
|
||||
"card1Title": "Le chrono parfait",
|
||||
"card1Subtitle": "EFFORT, REPOS, R\u00c9P\u00c9TEZ \u2014 des phases minut\u00e9es avec un retour visuel.",
|
||||
"card2Title": "50+ entra\u00eenements experts",
|
||||
"card2Subtitle": "De 4 minutes intenses \u00e0 20 minutes de br\u00fblage. D\u00e9butant \u00e0 avanc\u00e9.",
|
||||
"card1Subtitle": "EFFORT, REPOS, RÉPÉTEZ — des phases minutées avec un retour visuel.",
|
||||
"card2Title": "50+ entraînements experts",
|
||||
"card2Subtitle": "De 4 minutes intenses à 20 minutes de brûlage. Débutant à avancé.",
|
||||
"card3Title": "Coaching intelligent",
|
||||
"card3Subtitle": "Indications vocales et retour haptique pour rester dans la zone.",
|
||||
"card4Title": "Suivez vos progr\u00e8s",
|
||||
"card4Subtitle": "S\u00e9ries hebdomadaires, suivi des calories et records personnels."
|
||||
"card4Title": "Suivez vos progrès",
|
||||
"card4Subtitle": "Séries hebdomadaires, suivi des calories et records personnels."
|
||||
},
|
||||
"personalization": {
|
||||
"title": "Pr\u00e9parons votre\npremi\u00e8re semaine.",
|
||||
"title": "Préparons votre\npremière semaine.",
|
||||
"yourName": "VOTRE NOM",
|
||||
"namePlaceholder": "Entrez votre nom",
|
||||
"fitnessLevel": "NIVEAU DE FORME",
|
||||
"yourGoal": "VOTRE OBJECTIF",
|
||||
"weeklyFrequency": "FR\u00c9QUENCE HEBDOMADAIRE",
|
||||
"readyMessage": "Votre programme personnalis\u00e9 est pr\u00eat.",
|
||||
"weeklyFrequency": "FRÉQUENCE HEBDOMADAIRE",
|
||||
"readyMessage": "Votre programme personnalisé est prêt.",
|
||||
"goals": {
|
||||
"weightLoss": "Perte de poids",
|
||||
"cardio": "Cardio",
|
||||
"strength": "Force",
|
||||
"wellness": "Bien-\u00eatre"
|
||||
"wellness": "Bien-être"
|
||||
},
|
||||
"frequencies": {
|
||||
"2x": "2x / semaine",
|
||||
@@ -224,6 +274,23 @@
|
||||
"5x": "5x / semaine"
|
||||
}
|
||||
},
|
||||
"paywall": {
|
||||
"title": "Restez motivé.\nSans limites.",
|
||||
"features": {
|
||||
"unlimited": "Entraînements illimités",
|
||||
"offline": "Téléchargements hors ligne",
|
||||
"stats": "Statistiques avancées et Apple Watch",
|
||||
"noAds": "Sans publicités + Partage familial"
|
||||
},
|
||||
"bestValue": "MEILLEURE OFFRE",
|
||||
"yearlyPrice": "49,99 $",
|
||||
"monthlyPrice": "6,99 $",
|
||||
"savePercent": "Économisez 42%",
|
||||
"trialCta": "ESSAI GRATUIT (7 jours)",
|
||||
"guarantees": "Annulez à tout moment · Garantie satisfait ou remboursé 30 jours",
|
||||
"restorePurchases": "Restaurer les achats",
|
||||
"skipButton": "Continuer sans abonnement"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Politique de Confidentialité",
|
||||
"lastUpdated": "Dernière mise à jour : Mars 2026",
|
||||
@@ -261,5 +328,64 @@
|
||||
"content": "Si vous avez des questions sur cette politique de confidentialité, contactez-nous à :"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"programs": {
|
||||
"title": "Programmes",
|
||||
"weeks": "Semaines",
|
||||
"week": "Semaine",
|
||||
"workouts": "Entraînements",
|
||||
"workout": "Entraînement",
|
||||
"minutes": "Minutes",
|
||||
"min": "min",
|
||||
"perWeek": "/semaine",
|
||||
"equipment": "Équipement",
|
||||
"optional": "(optionnel)",
|
||||
"bodyweightOnly": "Poids du corps uniquement",
|
||||
"focusAreas": "Zones ciblées",
|
||||
"exercises": "exercices",
|
||||
"of": "sur",
|
||||
"complete": "terminé",
|
||||
"completed": "Terminé",
|
||||
"notStarted": "Non commencé",
|
||||
"inProgress": "En cours",
|
||||
"allWorkoutsComplete": "Tous les entraînements terminés !",
|
||||
"status": {
|
||||
"notStarted": "Non commencé",
|
||||
"inProgress": "En cours",
|
||||
"complete": "Terminé",
|
||||
"completed": "Terminé"
|
||||
},
|
||||
"yourProgress": "Votre progression",
|
||||
"trainingPlan": "Plan d'entraînement",
|
||||
"current": "Actuel",
|
||||
"startProgram": "Commencer le programme",
|
||||
"continue": "Continuer",
|
||||
"continueTraining": "Continuer l'entraînement",
|
||||
"restart": "Recommencer",
|
||||
"restartProgram": "Recommencer le programme"
|
||||
},
|
||||
|
||||
"assessment": {
|
||||
"title": "Évaluation des mouvements",
|
||||
"welcomeTitle": "Évaluation rapide",
|
||||
"welcomeDescription": "Vérifions vos patterns de mouvement pour personnaliser votre expérience et recommander le meilleur point de départ.",
|
||||
"minutes": "minutes",
|
||||
"quickCheck": "Vérification rapide de votre niveau de fitness",
|
||||
"movements": "mouvements",
|
||||
"testMovements": "Testez les patterns de mouvement clés",
|
||||
"noEquipment": "Aucun équipement nécessaire",
|
||||
"justYourBody": "Juste votre poids du corps",
|
||||
"whatWeCheck": "Ce que nous vérifions",
|
||||
"mobility": "Mobilité",
|
||||
"strength": "Force",
|
||||
"stability": "Stabilité",
|
||||
"balance": "Équilibre",
|
||||
"takeAssessment": "Faire l'évaluation",
|
||||
"startAssessment": "Commencer l'évaluation",
|
||||
"skipForNow": "Passer pour l'instant",
|
||||
"tips": "Conseils pour de meilleurs résultats",
|
||||
"duration": "Durée",
|
||||
"exercises": "Exercices"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user