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:
Millian Lamiaux
2026-03-23 21:27:19 +01:00
parent 197324188c
commit 8703c484e8
7 changed files with 1120 additions and 446 deletions

View File

@@ -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
View 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],
},
})

View File

@@ -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,
},
})
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}
}