diff --git a/src/__tests__/hooks/useAudio.test.ts b/src/__tests__/hooks/useAudio.test.ts index d5e6be4..73b2f4b 100644 --- a/src/__tests__/hooks/useAudio.test.ts +++ b/src/__tests__/hooks/useAudio.test.ts @@ -33,6 +33,7 @@ describe('useAudio logic', () => { musicVolume: 0.5, reminders: false, reminderTime: '09:00', + hasPromptedReview: false, }, }) }) diff --git a/src/__tests__/hooks/useHaptics.test.ts b/src/__tests__/hooks/useHaptics.test.ts index f6c53f9..0fac384 100644 --- a/src/__tests__/hooks/useHaptics.test.ts +++ b/src/__tests__/hooks/useHaptics.test.ts @@ -30,6 +30,7 @@ describe('useHaptics logic', () => { musicVolume: 0.5, reminders: false, reminderTime: '09:00', + hasPromptedReview: false, }, }) }) @@ -114,6 +115,7 @@ describe('useHaptics logic', () => { musicVolume: 0.5, reminders: false, reminderTime: '09:00', + hasPromptedReview: false, }, }) }) diff --git a/src/__tests__/hooks/useMusicPlayer.test.ts b/src/__tests__/hooks/useMusicPlayer.test.ts index df5b977..64ef83c 100644 --- a/src/__tests__/hooks/useMusicPlayer.test.ts +++ b/src/__tests__/hooks/useMusicPlayer.test.ts @@ -40,6 +40,7 @@ describe('useMusicPlayer', () => { musicVolume: 0.5, reminders: false, reminderTime: '09:00', + hasPromptedReview: false, }, }) }) diff --git a/src/__tests__/hooks/useNotifications.test.ts b/src/__tests__/hooks/useNotifications.test.ts index 77dd611..8ece9af 100644 --- a/src/__tests__/hooks/useNotifications.test.ts +++ b/src/__tests__/hooks/useNotifications.test.ts @@ -6,6 +6,7 @@ const mockUserStoreState = { settings: { reminders: false, reminderTime: '09:00', + hasPromptedReview: false, }, } diff --git a/src/__tests__/stores/userStore.test.ts b/src/__tests__/stores/userStore.test.ts index 379ebd6..e3fb581 100644 --- a/src/__tests__/stores/userStore.test.ts +++ b/src/__tests__/stores/userStore.test.ts @@ -27,6 +27,7 @@ describe('userStore', () => { musicVolume: 0.5, reminders: false, reminderTime: '09:00', + hasPromptedReview: false, }, }) }) diff --git a/src/shared/data/index.ts b/src/shared/data/index.ts index 5010798..e59622a 100644 --- a/src/shared/data/index.ts +++ b/src/shared/data/index.ts @@ -1,135 +1,7 @@ /** * TabataFit Data Layer - * New 3-Program System + Legacy Support + * Workout programs are fetched from Supabase via `workoutPrograms.ts`. */ -import { PROGRAMS, ALL_PROGRAM_WORKOUTS, ASSESSMENT_WORKOUT } from './programs' -import { TRAINERS } from './trainers' -import { ACHIEVEMENTS } from './achievements' -import { WORKOUTS } from './workouts' -import { GREEN, PHASE, TEXT, BRAND } from '../constants/colors' -import type { ProgramId } from '../types' - -// Re-export new program system -export { - PROGRAMS, - ALL_PROGRAM_WORKOUTS, - ASSESSMENT_WORKOUT, - TRAINERS, - ACHIEVEMENTS, -} - -// ═══════════════════════════════════════════════════════════════════════════ -// PROGRAM LOOKUPS -// ═══════════════════════════════════════════════════════════════════════════ - -export function getProgramById(id: ProgramId) { - return PROGRAMS[id] -} - -export function getAllPrograms() { - return Object.values(PROGRAMS) -} - -export function getProgramWorkouts(programId: ProgramId) { - const program = PROGRAMS[programId] - if (!program) return [] - return program.weeks.flatMap((week) => week.workouts) -} - -export function getWorkoutById(id: string) { - return ALL_PROGRAM_WORKOUTS.find((w) => w.id === id) -} - -export function getWorkoutsByCategory(category: string) { - if (category === 'all') return ALL_PROGRAM_WORKOUTS - return ALL_PROGRAM_WORKOUTS.filter((w) => { - const programId = getWorkoutProgramId(w.id) - return programId === category - }) -} - -export function getWorkoutProgramId(workoutId: string): ProgramId | null { - for (const [programId, program] of Object.entries(PROGRAMS)) { - for (const week of program.weeks) { - if (week.workouts.some((w) => w.id === workoutId)) { - return programId as ProgramId - } - } - } - return null -} - -// ═══════════════════════════════════════════════════════════════════════════ -// TRAINER LOOKUPS -// ═══════════════════════════════════════════════════════════════════════════ - -export function getTrainerById(id: string) { - return TRAINERS.find((t) => t.id === id) -} - -export function getTrainerByName(name: string) { - return TRAINERS.find((t) => t.name.toLowerCase() === name.toLowerCase()) -} - -// ═══════════════════════════════════════════════════════════════════════════ -// ACCENT COLOR -// ═══════════════════════════════════════════════════════════════════════════ - -/** Per-program accent colors (matches home screen cards) */ -const PROGRAM_ACCENT_COLORS: Record = { - 'upper-body': PHASE.PREP, - 'lower-body': GREEN[500], - 'full-body': BRAND.INFO, -} - -/** - * Resolve accent color for a workout: - * 1. Trainer color (if workout has trainerId) - * 2. Program accent color (if workout belongs to a program) - * 3. Fallback to orange - */ -export function getWorkoutAccentColor(workoutId: string): string { - // Check if it's a legacy workout with a trainer - const trainerWorkout = WORKOUTS.find((w) => w.id === workoutId) - if (trainerWorkout) { - const trainer = TRAINERS.find((t) => t.id === trainerWorkout.trainerId) - if (trainer?.color) return trainer.color - } - - // Check which program it belongs to - const programId = getWorkoutProgramId(workoutId) - if (programId) return PROGRAM_ACCENT_COLORS[programId] - - return GREEN[500] // fallback — brand primary -} - -// ═══════════════════════════════════════════════════════════════════════════ -// CATEGORY METADATA -// ═══════════════════════════════════════════════════════════════════════════ - -export const CATEGORIES: { id: ProgramId | 'all'; label: string }[] = [ - { id: 'all', label: 'All' }, - { id: 'upper-body', label: 'Upper Body' }, - { id: 'lower-body', label: 'Lower Body' }, - { id: 'full-body', label: 'Full Body' }, -] - -// ═══════════════════════════════════════════════════════════════════════════ -// POPULAR / RECOMMENDED -// ═══════════════════════════════════════════════════════════════════════════ - -/** Return a shuffled sample of workouts across all programs (one per program per week). */ -export function getPopularWorkouts(count: number = 4) { - // Pick the first workout from each program week to get variety - const picks = Object.values(PROGRAMS).flatMap((program) => - program.weeks.map((week) => week.workouts[0]).filter(Boolean) - ) - // Simple deterministic shuffle based on id to avoid re-renders - const sorted = [...picks].sort((a, b) => a.id.localeCompare(b.id)) - return sorted.slice(0, count) -} - -// Legacy exports for backward compatibility (to be removed) -export { WORKOUTS } from './workouts' -export { FEATURED_COLLECTION_ID } from './collections' +export * from './workoutPrograms' +export { useMusicVibeLabel } from './useTranslatedData' diff --git a/src/shared/data/useTranslatedData.ts b/src/shared/data/useTranslatedData.ts index 06c532e..e7998cd 100644 --- a/src/shared/data/useTranslatedData.ts +++ b/src/shared/data/useTranslatedData.ts @@ -1,113 +1,9 @@ /** * TabataFit Translated Data Hooks - * Wraps raw data objects with t() lookups at render time + * Wraps raw data objects with t() lookups at render time. */ -import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import type { Workout, Collection, Program } from '../types' -import { CATEGORIES } from './index' - -/** Convert an exercise/equipment name to a slug key for i18n lookup */ -function slugify(name: string): string { - return name - .toLowerCase() - .replace(/[()]/g, '') - .replace(/&/g, 'and') - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, '') -} - -/** Translate a single workout's display strings */ -export function useTranslatedWorkout(workout: Workout | undefined) { - const { t } = useTranslation('content') - - return useMemo(() => { - if (!workout) return undefined - return { - ...workout, - title: t(`workouts.${workout.id}`, { defaultValue: workout.title }), - exercises: workout.exercises.map((ex) => ({ - ...ex, - name: t(`exercises.${slugify(ex.name)}`, { defaultValue: ex.name }), - })), - equipment: workout.equipment.map((item) => - t(`equipment.${slugify(item)}`, { defaultValue: item }) - ), - } - }, [workout, t]) -} - -/** Translate an array of workouts (titles only, for list views) */ -export function useTranslatedWorkouts( - workouts: T[] -) { - const { t } = useTranslation('content') - - return useMemo( - () => - workouts.map((w) => ({ - ...w, - title: t(`workouts.${w.id}`, { defaultValue: w.title }), - })), - [workouts, t] - ) -} - -/** Translate collections */ -export function useTranslatedCollections(collections: Collection[]) { - const { t } = useTranslation('content') - - return useMemo( - () => - collections.map((c) => ({ - ...c, - title: t(`collections.${c.id}.title`, { defaultValue: c.title }), - description: t(`collections.${c.id}.description`, { - defaultValue: c.description, - }), - })), - [collections, t] - ) -} - -/** Translate programs */ -export function useTranslatedPrograms(programs: Program[]) { - const { t } = useTranslation('content') - - return useMemo( - () => - programs.map((p) => ({ - ...p, - title: t(`programs.${p.id}.title`, { defaultValue: p.title }), - description: t(`programs.${p.id}.description`, { - defaultValue: p.description, - }), - })), - [programs, t] - ) -} - -/** Translate category labels */ -export function useTranslatedCategories() { - const { t } = useTranslation('common') - - return useMemo(() => { - const categoryKeyMap: Record = { - all: 'categories.all', - 'full-body': 'categories.fullBody', - core: 'categories.core', - 'upper-body': 'categories.upperBody', - 'lower-body': 'categories.lowerBody', - cardio: 'categories.cardio', - } - - return CATEGORIES.map((cat) => ({ - ...cat, - label: t(categoryKeyMap[cat.id] ?? cat.id, { defaultValue: cat.label }), - })) - }, [t]) -} /** Translate a music vibe name */ export function useMusicVibeLabel(vibe: string): string { diff --git a/src/shared/data/workoutPrograms.ts b/src/shared/data/workoutPrograms.ts index f79ccca..b4cb0fd 100644 --- a/src/shared/data/workoutPrograms.ts +++ b/src/shared/data/workoutPrograms.ts @@ -10,13 +10,17 @@ import type { WorkoutTabata, WorkoutProgramRow, WorkoutTabataRow, + WorkoutTimedExerciseRow, + WarmupBlock, + StretchBlock, + TimedExercise, BodyZone, } from '../types/workoutProgram' import type { TabataSession, TabataBlock, TabataExercise, TimedMovement } from '../types/program' // ─── Constants ────────────────────────────────────────────────── -const CACHE_KEY = 'tabatafit-workout-programs-cache-v2' +const CACHE_KEY = 'tabatafit-workout-programs-cache-v3' const CACHE_TTL = 1000 * 60 * 60 // 1 hour // ─── Row Mappers ──────────────────────────────────────────────── @@ -34,6 +38,7 @@ function tabataRowToWorkoutTabata(row: WorkoutTabataRow): WorkoutTabata { modificationEn: row.exercise_1_modification_en ?? undefined, progression: row.exercise_1_progression ?? undefined, progressionEn: row.exercise_1_progression_en ?? undefined, + videoUrl: row.exercise_1_video_url ?? null, }, exercise2: { name: row.exercise_2_name, @@ -44,6 +49,7 @@ function tabataRowToWorkoutTabata(row: WorkoutTabataRow): WorkoutTabata { modificationEn: row.exercise_2_modification_en ?? undefined, progression: row.exercise_2_progression ?? undefined, progressionEn: row.exercise_2_progression_en ?? undefined, + videoUrl: row.exercise_2_video_url ?? null, }, rounds: row.rounds, workTime: row.work_time, @@ -51,10 +57,63 @@ function tabataRowToWorkoutTabata(row: WorkoutTabataRow): WorkoutTabata { } } +function timedExerciseRowToTimedExercise(row: WorkoutTimedExerciseRow): TimedExercise { + return { + name: row.name, + nameEn: row.name_en ?? '', + duration: row.duration, + videoUrl: row.video_url ?? null, + tip: row.tip ?? undefined, + tipEn: row.tip_en ?? undefined, + } +} + +function buildWarmupBlock(rows: WorkoutTimedExerciseRow[]): WarmupBlock { + const exercises = rows + .sort((a, b) => a.position - b.position) + .map(timedExerciseRowToTimedExercise) + const totalDuration = exercises.reduce((sum, e) => sum + e.duration, 0) + return { exercises, totalDuration } +} + +function buildStretchBlock(rows: WorkoutTimedExerciseRow[]): StretchBlock { + const exercises = rows + .sort((a, b) => a.position - b.position) + .map(timedExerciseRowToTimedExercise) + const totalDuration = exercises.reduce((sum, e) => sum + e.duration, 0) + return { exercises, totalDuration } +} + +/** Placeholder blocks until admin-web seeds real content */ +const DEFAULT_WARMUP: WarmupBlock = { + exercises: [ + { name: 'Jumping jacks', nameEn: 'Jumping jacks', duration: 45, videoUrl: null }, + { name: 'Rotation des bras', nameEn: 'Arm circles', duration: 30, videoUrl: null }, + { name: 'Montées de genoux', nameEn: 'High knees', duration: 45, videoUrl: null }, + { name: 'Squats légers', nameEn: 'Light squats', duration: 30, videoUrl: null }, + ], + totalDuration: 150, +} + +const DEFAULT_STRETCH: StretchBlock = { + exercises: [ + { name: 'Étirement quadriceps', nameEn: 'Quad stretch', duration: 30, videoUrl: null }, + { name: 'Étirement ischio-jambiers', nameEn: 'Hamstring stretch', duration: 30, videoUrl: null }, + { name: 'Étirement épaules', nameEn: 'Shoulder stretch', duration: 30, videoUrl: null }, + { name: 'Respiration profonde', nameEn: 'Deep breathing', duration: 30, videoUrl: null }, + ], + totalDuration: 120, +} + function rowsToWorkoutProgram( programRow: WorkoutProgramRow, tabataRows: WorkoutTabataRow[], + warmupRows: WorkoutTimedExerciseRow[], + stretchRows: WorkoutTimedExerciseRow[], ): WorkoutProgram { + const warmup = warmupRows.length > 0 ? buildWarmupBlock(warmupRows) : DEFAULT_WARMUP + const stretch = stretchRows.length > 0 ? buildStretchBlock(stretchRows) : DEFAULT_STRETCH + return { id: programRow.id, title: programRow.title, @@ -68,9 +127,11 @@ function rowsToWorkoutProgram( icon: programRow.icon, accentColor: programRow.accent_color, sortOrder: programRow.sort_order, + warmup, tabatas: tabataRows .sort((a, b) => a.position - b.position) .map(tabataRowToWorkoutTabata), + stretch, createdAt: programRow.created_at, updatedAt: programRow.updated_at, } @@ -107,16 +168,14 @@ async function setCachedPrograms(programs: WorkoutProgram[]): Promise { // ─── Fetch Functions ──────────────────────────────────────────── /** - * Fetch all workout programs with their tabatas. - * Uses cache first, falls back to Supabase. + * Fetch all workout programs with their tabatas, warmup, and stretch. + * Always fetches fresh from Supabase; falls back to cache if offline. */ export async function fetchAllPrograms(): Promise { - // Always fetch fresh from Supabase (cache can become stale after schema changes) if (!isSupabaseConfigured()) { const cached = await getCachedPrograms() return cached ?? [] } - if (!isSupabaseConfigured()) return [] const { data: programRows, error: progError } = await supabase .from('workout_programs') @@ -126,35 +185,58 @@ export async function fetchAllPrograms(): Promise { .order('level') .returns() - if (progError || !programRows?.length) return [] + if (progError || !programRows?.length) { + const cached = await getCachedPrograms() + return cached ?? [] + } - const { data: tabataRows, error: tabError } = await supabase + const { data: tabataRows } = await supabase .from('program_tabatas') .select('*') .order('position') .returns() - if (tabError || !tabataRows) return [] + const { data: warmupRows } = await supabase + .from('workout_warmup_exercises') + .select('*') + .order('position') + .returns() - // Group tabatas by program - const tabatasByProgram = new Map() - for (const t of tabataRows) { - const existing = tabatasByProgram.get(t.program_id) ?? [] - existing.push(t) - tabatasByProgram.set(t.program_id, existing) - } + const { data: stretchRows } = await supabase + .from('workout_stretch_exercises') + .select('*') + .order('position') + .returns() + + const tabatasByProgram = groupBy(tabataRows ?? [], r => r.program_id) + const warmupByProgram = groupBy(warmupRows ?? [], r => r.program_id) + const stretchByProgram = groupBy(stretchRows ?? [], r => r.program_id) const programs = programRows.map(pr => - rowsToWorkoutProgram(pr, tabatasByProgram.get(pr.id) ?? []), + rowsToWorkoutProgram( + pr, + tabatasByProgram.get(pr.id) ?? [], + warmupByProgram.get(pr.id) ?? [], + stretchByProgram.get(pr.id) ?? [], + ), ) await setCachedPrograms(programs) return programs } -/** - * Fetch programs filtered by body zone - */ +function groupBy(items: T[], keyFn: (t: T) => K): Map { + const map = new Map() + for (const item of items) { + const key = keyFn(item) + const bucket = map.get(key) ?? [] + bucket.push(item) + map.set(key, bucket) + } + return map +} + +/** Fetch programs filtered by body zone */ export async function fetchProgramsByBodyZone( bodyZone: BodyZone, ): Promise { @@ -162,60 +244,37 @@ export async function fetchProgramsByBodyZone( return all.filter(p => p.bodyZone === bodyZone) } -/** - * Fetch a single program by ID - */ -export async function fetchProgramById( - id: string, -): Promise { +/** Fetch a single program by ID */ +export async function fetchProgramById(id: string): Promise { const all = await fetchAllPrograms() return all.find(p => p.id === id) ?? null } -/** - * Check if an ID refers to a workout program - */ +/** Check if an ID refers to a workout program */ export function isWorkoutProgramId(id: string): boolean { return id.startsWith('wp-') } -/** - * Parse a workout program ID like 'wp-{programId}' - */ -export function parseWorkoutProgramId( - id: string, -): { programId: string } | null { +/** Parse a workout program ID like 'wp-{programId}' */ +export function parseWorkoutProgramId(id: string): { programId: string } | null { if (!id.startsWith('wp-')) return null return { programId: id.slice(3) } } -/** - * Build an ID for a workout program - */ +/** Build an ID for a workout program */ export function buildWorkoutProgramId(programId: string): string { return `wp-${programId}` } // ─── Adapter: WorkoutProgram → TabataSession ───────────────────── -/** Generic warmup movements */ -const GENERIC_WARMUP: TimedMovement[] = [ - { name: 'Jumping jacks', nameEn: 'Jumping jacks', duration: 30 }, - { name: 'Rotation des bras', nameEn: 'Arm circles', duration: 30 }, - { name: 'Montées de genoux', nameEn: 'High knees', duration: 30 }, - { name: 'Squats légers', nameEn: 'Bodyweight squats', duration: 30 }, -] - -/** Generic cooldown movements */ -const GENERIC_COOLDOWN: TimedMovement[] = [ - { name: 'Étirement des quadriceps', nameEn: 'Quad stretch', duration: 30 }, - { name: 'Étirement des ischio-jambiers', nameEn: 'Hamstring stretch', duration: 30 }, - { name: 'Respiration profonde', nameEn: 'Deep breathing', duration: 30 }, -] +function timedExerciseToMovement(e: TimedExercise): TimedMovement { + return { name: e.name, nameEn: e.nameEn, duration: e.duration } +} /** - * Convert a full WorkoutProgram (all 3 tabatas) to a TabataSession for the player. - * All tabata blocks are played sequentially in one session. + * Convert a full WorkoutProgram (warmup + 3 tabatas + stretch) to a TabataSession + * for the legacy player. Uses program's warmup and stretch blocks directly. */ export function workoutProgramToTabataSession( program: WorkoutProgram, @@ -231,7 +290,6 @@ export function workoutProgramToTabataSession( progression: tabata.exercise1.progression, progressionEn: tabata.exercise1.progressionEn, } - const evenExercise: TabataExercise = { name: tabata.exercise2.name, nameEn: tabata.exercise2.nameEn, @@ -242,7 +300,6 @@ export function workoutProgramToTabataSession( progression: tabata.exercise2.progression, progressionEn: tabata.exercise2.progressionEn, } - return { id: tabata.id, oddExercise, @@ -253,13 +310,13 @@ export function workoutProgramToTabataSession( } }) - // Calculate total duration: warmup (2min) + all tabatas + inter-block rests (1min each) + cooldown (1.5min) - const tabatasDuration = blocks.reduce( - (sum, b) => sum + (b.rounds * (b.workTime + b.restTime)) / 60, + const tabatasDurationSec = blocks.reduce( + (sum, b) => sum + b.rounds * (b.workTime + b.restTime), 0, ) - const interBlockRest = Math.max(0, blocks.length - 1) // 1 min between blocks - const totalDuration = 2 + tabatasDuration + interBlockRest + 1.5 + const interBlockRestSec = Math.max(0, blocks.length - 1) * 15 // 15s between blocks + const totalSec = program.warmup.totalDuration + tabatasDurationSec + interBlockRestSec + program.stretch.totalDuration + const totalDuration = Math.ceil(totalSec / 60) const totalRounds = blocks.reduce((sum, b) => sum + b.rounds, 0) const calorieMultiplier = program.level === 'Advanced' ? 12 : program.level === 'Intermediate' ? 9 : 7 @@ -275,17 +332,17 @@ export function workoutProgramToTabataSession( focus: [program.bodyZone], focusEn: [program.bodyZone], warmup: { - movements: GENERIC_WARMUP, - totalDuration: 120, + movements: program.warmup.exercises.map(timedExerciseToMovement), + totalDuration: program.warmup.totalDuration, }, blocks, cooldown: { - movements: GENERIC_COOLDOWN, - totalDuration: 90, + movements: program.stretch.exercises.map(timedExerciseToMovement), + totalDuration: program.stretch.totalDuration, }, equipment: [], totalRounds, - totalDuration: Math.ceil(totalDuration), + totalDuration, calories: Math.ceil(totalRounds * calorieMultiplier), musicVibe: program.musicVibe, } diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts index 3743de7..dae76e9 100644 --- a/src/shared/hooks/index.ts +++ b/src/shared/hooks/index.ts @@ -9,16 +9,4 @@ export { useAudio } from './useAudio' export { useMusicPlayer } from './useMusicPlayer' export { useNotifications, requestNotificationPermissions } from './useNotifications' export { usePurchases } from './usePurchases' -export { - useWorkouts, - useWorkout, - useWorkoutsByCategory, - useTrainers, - useTrainer, - useCollections, - useCollection, - usePrograms, - useFeaturedWorkouts, - usePopularWorkouts, - useWorkoutsByTrainer, -} from './useSupabaseData' +export { useTabataTimer } from './useTabataTimer' diff --git a/src/shared/i18n/locales/de/common.json b/src/shared/i18n/locales/de/common.json index 32f1c78..11360f7 100644 --- a/src/shared/i18n/locales/de/common.json +++ b/src/shared/i18n/locales/de/common.json @@ -9,6 +9,7 @@ "cancel": "Abbrechen", "save": "Speichern", "loading": "Laden...", + "offline": "Keine Internetverbindung", "levels": { "beginner": "Anfänger", diff --git a/src/shared/i18n/locales/de/screens.json b/src/shared/i18n/locales/de/screens.json index 016e36b..afda39a 100644 --- a/src/shared/i18n/locales/de/screens.json +++ b/src/shared/i18n/locales/de/screens.json @@ -7,14 +7,13 @@ "progression": "Fortschritt", "profile": "Profil" }, - "home": { "readyToCrush": "Bereit, heute alles zu geben?", "featured": "EMPFOHLEN", "recent": "Zuletzt", "popularThisWeek": "Beliebt diese Woche", "collections": "Sammlungen", - "chooseYourPath": "W\u00e4hle deinen Weg", + "chooseYourPath": "Wähle deinen Weg", "continueYourJourney": "Setze deine Reise fort", "yourPrograms": "Deine Programme", "programsSubtitle": "Entwickelt von Physiotherapeuten", @@ -26,6 +25,7 @@ "lowerBody": "Unterkörper", "fullBody": "Ganzkörper", "programsByZone": "Programme nach Zone", + "filterAll": "Alle", "tabataCount": "{{count}} Tabatas", "freeBadge": "KOSTENLOS", "premiumBadge": "PREMIUM", @@ -34,9 +34,17 @@ "unlockPremium": "Premium freischalten", "tabataPrograms": "Physio Programme", "tabataProgramsSubtitle": "Rehabilitation und Physiotherapie Programme", - "recommendedNext": "Sitzung fortsetzen" + "recommendedNext": "Sitzung fortsetzen", + "mascotStreak": "{{count}}-Tage-Serie{{name}}! 🔥", + "mascotReady": "Bereit zu trainieren{{name}}?", + "statsCompleted": "Programme", + "zoneSubtitle": "3 Level · Anfänger → Fortgeschrittene", + "zoneDescUpper": "Schultern, Brust, Arme & Rumpfkraft", + "zoneDescLower": "Beine, Gesäß & Hüftmobilität", + "zoneDescFull": "Ganzkörperkräftigung & Ausdauer", + "zoneLevels": "3 Level", + "zonePrograms": "{{count}} Programme" }, - "explore": { "title": "Entdecken", "collections": "Sammlungen", @@ -72,7 +80,6 @@ "errorRetry": "Tippe zum Wiederholen", "featuredCollection": "Empfohlene Sammlung" }, - "activity": { "title": "Aktivität", "dayStreak": "Tage in Folge", @@ -92,22 +99,6 @@ "emptySubtitle": "Absolviere dein erstes Workout und deine Statistiken erscheinen hier.", "startFirstWorkout": "Starte dein erstes Workout" }, - - "browse": { - "title": "Entdecken", - "searchPlaceholder": "Workouts suchen...", - "searchResults": "Ergebnisse", - "allWorkouts": "Alle Workouts", - "noResults": "Keine Workouts gefunden", - "tryDifferentSearch": "Versuchen Sie eine andere Suche oder Kategorie", - "featured": "EMPFOHLEN", - "collections": "Sammlungen", - "programs": "Programme", - "newReleases": "Neuerscheinungen", - "weeksCount": "{{count}} Wochen", - "timesPerWeek": "{{count}}x /Woche" - }, - "profile": { "title": "Profil", "guest": "Gast", @@ -115,24 +106,25 @@ "sectionAccount": "KONTO", "sectionWorkout": "WORKOUT", "sectionNotifications": "BENACHRICHTIGUNGEN", - "sectionAbout": "\u00dcBER", + "sectionAbout": "ÜBER", "sectionSubscription": "ABONNEMENT", "email": "E-Mail", "plan": "Plan", "freePlan": "Kostenlos", - "restorePurchases": "K\u00e4ufe wiederherstellen", + "restorePurchases": "Käufe wiederherstellen", "hapticFeedback": "Haptisches Feedback", "soundEffects": "Soundeffekte", "voiceCoaching": "Sprachcoaching", - "dailyReminders": "T\u00e4gliche Erinnerungen", + "dailyReminders": "Tägliche Erinnerungen", "reminderTime": "Erinnerungszeit", - "reminderFooter": "Erhalte eine t\u00e4gliche Erinnerung, um deine Serie zu halten", + "reminderFooter": "Erhalte eine tägliche Erinnerung, um deine Serie zu halten", "workoutSettingsFooter": "Passe dein Workout-Erlebnis an", "upgradeTitle": "TabataFit+ freischalten", "upgradeDescription": "Unbegrenzte Workouts, Offline-Downloads und mehr.", "learnMore": "Mehr erfahren", "version": "Version", "privacyPolicy": "Datenschutzrichtlinie", + "termsOfService": "Nutzungsbedingungen", "signOut": "Abmelden", "statsWorkouts": "Workouts", "statsStreak": "Tage in Folge", @@ -148,7 +140,6 @@ "enablePersonalization": "Personalisierung aktivieren", "deleteData": "Meine Daten löschen" }, - "sync": { "title": "Personalisierte Workouts freischalten", "benefits": { @@ -161,7 +152,6 @@ "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.", @@ -169,13 +159,15 @@ "deleteButton": "Meine Daten löschen", "cancelButton": "Meine Daten behalten" }, - "player": { "phases": { "prep": "BEREIT MACHEN", "work": "LOS", "rest": "PAUSE", - "complete": "FERTIG" + "complete": "FERTIG", + "warmup": "AUFWÄRMEN", + "stretch": "DEHNEN", + "trans": "ÜBERGANG" }, "current": "Aktuell", "next": "Nächste: ", @@ -186,9 +178,20 @@ "greatJob": "Super gemacht!", "rounds": "Runden", "calories": "Kalorien", - "minutes": "Minuten" + "minutes": "Minuten", + "mute": "Stumm", + "unmute": "Ton an", + "quitTitle": "Workout beenden?", + "quitMessage": "Dein Fortschritt geht verloren.", + "quitConfirm": "Beenden", + "quitCancel": "Abbrechen", + "warmupTitle": "Aufwärmen", + "stretchTitle": "Dehnen", + "nextBlock": "Nächster: Block {{num}}", + "sessionComplete": "Workout abgeschlossen!", + "greatWork": "Großartige Arbeit", + "blocks": "Blöcke" }, - "complete": { "title": "WORKOUT ABGESCHLOSSEN", "caloriesLabel": "KALORIEN", @@ -199,32 +202,39 @@ "streakTitle": "{{count}} Tage in Folge!", "streakSubtitle": "Bleib am Ball!", "shareWorkout": "Teile dein Workout", - "shareText": "Ich habe gerade {{title}} abgeschlossen! \uD83D\uDD25 {{calories}} Kalorien in {{duration}} Minuten.", + "shareText": "Ich habe gerade {{title}} abgeschlossen! 🔥 {{calories}} Kalorien in {{duration}} Minuten.", "recommendedNext": "Empfohlen als Nächstes", - "backToHome": "Zurück zur Startseite" + "backToHome": "Zurück zur Startseite", + "feedbackTitle": "Wie war das Workout?", + "feedbackEasy": "Einfach", + "feedbackPerfect": "Perfekt", + "feedbackHard": "Schwer", + "thisWeek": "Diese Woche", + "share": "Teilen", + "shareTitle": "Einheit abgeschlossen · {{minutes}} Min.", + "streakDays_one": "{{count}} Tag am Stück", + "streakDays_other": "{{count}} Tage am Stück", + "streakRecord_one": "Rekord: {{count}} Tag", + "streakRecord_other": "Rekord: {{count}} Tage" }, - "collection": { "notFound": "Sammlung nicht gefunden", "minTotal": "{{count}} Min insgesamt" }, - "category": { "allLevels": "Alle Stufen" }, - "workout": { "notFound": "Workout nicht gefunden", "whatYoullNeed": "Was du brauchst", "exercises": "Übungen ({{count}} Runden)", - "repeatRounds": "Wiederholen \u00D7 {{count}} Runden", + "repeatRounds": "Wiederholen × {{count}} Runden", "music": "Musik", "musicMix": "{{vibe}} Mix", "curatedForWorkout": "Zusammengestellt für dein Workout", "startWorkout": "WORKOUT STARTEN", "unlockWithPremium": "MIT TABATAFIT+ FREISCHALTEN" }, - "paywall": { "subtitle": "Schalte alle Funktionen frei und erreiche deine Ziele schneller", "features": { @@ -245,9 +255,10 @@ "trialCta": "Kostenlos Testen", "processing": "Verarbeitung...", "restore": "Käufe Wiederherstellen", - "terms": "Die Zahlung wird bei Bestätigung deiner Apple-ID belastet. Das Abonnement verlängert sich automatisch, sofern es nicht mindestens 24 Stunden vor Ablauf des Zeitraums gekündigt wird. Verwalte es in den Kontoeinstellungen." + "terms": "Die Zahlung wird bei Bestätigung deiner Apple-ID belastet. Das Abonnement verlängert sich automatisch, sofern es nicht mindestens 24 Stunden vor Ablauf des Zeitraums gekündigt wird. Verwalte es in den Kontoeinstellungen.", + "termsLink": "Nutzungsbedingungen", + "privacyLink": "Datenschutzrichtlinie" }, - "onboarding": { "problem": { "title": "Du hast keine Stunde\nfürs Fitnessstudio.", @@ -280,13 +291,13 @@ "title": "Deine App, vorab.", "subtitle": "Alles, was du brauchst, um dich zu verwandeln.", "card1Title": "Der perfekte Timer", - "card1Subtitle": "LOS, PAUSE, WIEDERHOLEN \u2014 pr\u00e4zise getaktete Phasen mit visuellem Feedback.", + "card1Subtitle": "LOS, PAUSE, WIEDERHOLEN — präzise getaktete Phasen mit visuellem Feedback.", "card2Title": "50+ Experten-Workouts", - "card2Subtitle": "Von 4-Minuten-Sprints bis 20-Minuten-Burns. Anf\u00e4nger bis Fortgeschrittene.", + "card2Subtitle": "Von 4-Minuten-Sprints bis 20-Minuten-Burns. Anfänger bis Fortgeschrittene.", "card3Title": "Intelligentes Coaching", "card3Subtitle": "Sprachhinweise und haptisches Feedback halten dich in der Zone.", "card4Title": "Verfolge deinen Fortschritt", - "card4Subtitle": "W\u00f6chentliche Serien, Kalorientracking und pers\u00f6nliche Rekorde." + "card4Subtitle": "Wöchentliche Serien, Kalorientracking und persönliche Rekorde." }, "personalization": { "title": "Lass uns deine\nerste Woche planen.", @@ -321,12 +332,11 @@ "monthlyPrice": "6,99 $", "savePercent": "42% sparen", "trialCta": "KOSTENLOS TESTEN (7 Tage)", - "guarantees": "Jederzeit k\u00fcndbar \u00B7 30-Tage-Geld-zur\u00fcck-Garantie", - "restorePurchases": "K\u00e4ufe wiederherstellen", + "guarantees": "Jederzeit kündbar · 30-Tage-Geld-zurück-Garantie", + "restorePurchases": "Käufe wiederherstellen", "skipButton": "Ohne Abo fortfahren" } }, - "programs": { "title": "Programme", "weeks": "Wochen", @@ -336,11 +346,11 @@ "minutes": "Minuten", "min": "min", "perWeek": "/Woche", - "equipment": "Ausr\u00fcstung", + "equipment": "Ausrüstung", "optional": "(optional)", - "bodyweightOnly": "Nur K\u00f6rpergewicht", + "bodyweightOnly": "Nur Körpergewicht", "focusAreas": "Schwerpunkte", - "exercises": "\u00dcbungen", + "exercises": "Übungen", "of": "von", "complete": "abgeschlossen", "completed": "Abgeschlossen", @@ -362,52 +372,6 @@ "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", - "tip1": "Bewegen Sie sich in Ihrem eigenen Tempo", - "tip2": "Achten Sie auf die Form, nicht auf die Geschwindigkeit", - "tip3": "Dies hilft uns, das beste Programm zu empfehlen", - "tip4": "Kein Urteil - nur ein Ausgangspunkt!", - "duration": "Dauer", - "exercises": "\u00dcbungen" - }, - - "kine": { - "programs": "Physio-Programme", - "recommendedNext": "Nächste empfohlene Einheit", - "continueSession": "Einheit fortsetzen", - "startSession": "Einheit starten", - "unlockPremium": "Mit Premium freischalten", - "free": "KOSTENLOS", - "premium": "PREMIUM", - "weeks": "Wochen", - "sessionsPerWeek": "Einheiten/Woche", - "sessions": "Einheiten", - "deload": "Entlastung", - "warmup": "Aufwärmen", - "cooldown": "Cool-down", - "block": "Block", - "rounds": "Runden" - }, - "workoutProgram": { "tabata": "Tabata", "exercise1": "Übung 1", @@ -418,5 +382,84 @@ "beginner": "Anfänger", "intermediate": "Mittelstufe", "advanced": "Fortgeschritten" + }, + "terms": { + "title": "Nutzungsbedingungen", + "lastUpdated": "Letzte Aktualisierung: März 2026", + "acceptance": { + "title": "Akzeptanz", + "content": "Durch die Nutzung von TabataFit akzeptierst du diese Nutzungsbedingungen." + }, + "service": { + "title": "Beschreibung des Dienstes", + "content": "TabataFit ist eine Fitness-App für Tabata-Training mit Timer, Programmen und Fortschrittsverfolgung." + }, + "subscription": { + "title": "Abonnement", + "content": "Premium-Funktionen erfordern ein Abonnement. Die Abrechnung erfolgt über dein Apple-ID-Konto. Die automatische Verlängerung kann in den Kontoeinstellungen deaktiviert werden." + }, + "liability": { + "title": "Haftungsbeschränkung", + "content": "TabataFit übernimmt keine Haftung für Verletzungen. Konsultiere vor Beginn eines Trainingsprogramms einen Arzt." + }, + "ip": { + "title": "Geistiges Eigentum", + "content": "Alle Inhalte sind durch das Urheberrecht geschützt." + }, + "contact": { + "title": "Kontakt", + "content": "Bei Fragen: support@tabatafit.app" + } + }, + "zone": { + "chooseYourFocus": "Wähle deinen Fokus", + "upperBody": { + "label": "Oberkörper", + "description": "Arme, Schultern, Brust und Rücken." + }, + "lowerBody": { + "label": "Unterkörper", + "description": "Beine, Gesäß und Rumpf." + }, + "fullBody": { + "label": "Ganzkörper", + "description": "Komplette Workouts, jede Muskelgruppe." + }, + "chooseLevel": "Wähle dein Level", + "emptyPrograms": "Noch keine Programme auf diesem Level." + }, + "settings": { + "title": "Einstellungen", + "sectionProfile": "PROFIL", + "name": "Name", + "subscription": "Abonnement", + "premium": "Premium", + "free": "Kostenlos", + "upgradePremium": "Auf Premium upgraden", + "sectionPrefs": "EINSTELLUNGEN", + "haptics": "Vibration", + "soundEffects": "Soundeffekte", + "voiceCoaching": "Sprach-Coach", + "music": "Musik", + "sectionLegal": "RECHTLICHES", + "terms": "Nutzungsbedingungen", + "privacy": "Datenschutz", + "sectionData": "DATEN", + "resetProgress": "Fortschritt zurücksetzen", + "resetTitle": "Fortschritt zurücksetzen?", + "resetMessage": "Diese Aktion ist unwiderruflich. Deine Serie, dein Verlauf und abgeschlossene Programme werden gelöscht.", + "resetConfirm": "Zurücksetzen", + "version": "TabataGo · v1.0" + }, + "program": { + "notFound": "Programm nicht gefunden", + "warmup": "Aufwärmen", + "stretch": "Dehnen", + "exercise1": "Übung 1", + "exercise2": "Übung 2", + "tabataLabel": "Tabata {{num}}", + "tabataSubtitle": "{{rounds}} Runden · {{work}}s / {{rest}}s", + "startSession": "Einheit starten", + "unlockPremium": "Mit Premium freischalten" } } diff --git a/src/shared/i18n/locales/en/common.json b/src/shared/i18n/locales/en/common.json index 56780f1..77ad88d 100644 --- a/src/shared/i18n/locales/en/common.json +++ b/src/shared/i18n/locales/en/common.json @@ -9,6 +9,7 @@ "cancel": "Cancel", "save": "Save", "loading": "Loading...", + "offline": "No internet connection", "levels": { "beginner": "Beginner", diff --git a/src/shared/i18n/locales/en/screens.json b/src/shared/i18n/locales/en/screens.json index 2ce3ba8..258a4d2 100644 --- a/src/shared/i18n/locales/en/screens.json +++ b/src/shared/i18n/locales/en/screens.json @@ -7,7 +7,6 @@ "progression": "Progress", "profile": "Profile" }, - "home": { "readyToCrush": "Ready to crush it today?", "featured": "FEATURED", @@ -26,6 +25,7 @@ "lowerBody": "Lower Body", "fullBody": "Full Body", "programsByZone": "Programs by Body Zone", + "filterAll": "All", "tabataCount": "{{count}} tabatas", "freeBadge": "FREE", "premiumBadge": "PREMIUM", @@ -34,9 +34,17 @@ "unlockPremium": "Unlock Premium", "tabataPrograms": "Physio Programs", "tabataProgramsSubtitle": "Rehabilitation and physiotherapy programs", - "recommendedNext": "Continue your session" + "recommendedNext": "Continue your session", + "mascotStreak": "{{count}}-day streak{{name}}! 🔥", + "mascotReady": "Ready to train{{name}}?", + "statsCompleted": "Programs", + "zoneSubtitle": "3 levels · Beginner → Advanced", + "zoneDescUpper": "Shoulders, chest, arms & core strength", + "zoneDescLower": "Legs, glutes & hip mobility", + "zoneDescFull": "Total body conditioning & cardio", + "zoneLevels": "3 levels", + "zonePrograms": "{{count}} programs" }, - "explore": { "title": "Explore", "collections": "Collections", @@ -72,7 +80,6 @@ "errorRetry": "Tap to retry", "featuredCollection": "Featured Collection" }, - "activity": { "title": "Activity", "dayStreak": "day streak", @@ -92,22 +99,6 @@ "emptySubtitle": "Complete your first workout and your stats will appear here.", "startFirstWorkout": "Start Your First Workout" }, - - "browse": { - "title": "Browse", - "searchPlaceholder": "Search workouts...", - "searchResults": "Results", - "allWorkouts": "All Workouts", - "noResults": "No workouts found", - "tryDifferentSearch": "Try a different search or category", - "featured": "FEATURED", - "collections": "Collections", - "programs": "Programs", - "newReleases": "New Releases", - "weeksCount": "{{count}} weeks", - "timesPerWeek": "{{count}}x /week" - }, - "profile": { "title": "Profile", "guest": "Guest", @@ -133,6 +124,7 @@ "learnMore": "Learn More", "version": "Version", "privacyPolicy": "Privacy Policy", + "termsOfService": "Terms of Service", "signOut": "Sign Out", "statsWorkouts": "workouts", "statsStreak": "day streak", @@ -148,7 +140,6 @@ "enablePersonalization": "Enable Personalization", "deleteData": "Delete My Data" }, - "sync": { "title": "Unlock Personalized Workouts", "benefits": { @@ -161,7 +152,6 @@ "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.", @@ -169,13 +159,15 @@ "deleteButton": "Delete My Data", "cancelButton": "Keep My Data" }, - "player": { "phases": { "prep": "GET READY", "work": "WORK", "rest": "REST", - "complete": "COMPLETE" + "complete": "COMPLETE", + "warmup": "WARMUP", + "stretch": "STRETCH", + "trans": "TRANSITION" }, "current": "Current", "next": "Next: ", @@ -186,9 +178,20 @@ "greatJob": "Great job!", "rounds": "Rounds", "calories": "Calories", - "minutes": "Minutes" + "minutes": "Minutes", + "mute": "Mute", + "unmute": "Unmute", + "quitTitle": "Quit Workout?", + "quitMessage": "Your progress won't be saved. Are you sure?", + "quitConfirm": "Quit", + "quitCancel": "Keep Going", + "warmupTitle": "Warm-up", + "stretchTitle": "Stretch", + "nextBlock": "Next: Block {{num}}", + "sessionComplete": "Workout complete!", + "greatWork": "Great work", + "blocks": "Blocks" }, - "complete": { "title": "WORKOUT COMPLETE", "caloriesLabel": "CALORIES", @@ -199,171 +202,105 @@ "streakTitle": "{{count}} Day Streak!", "streakSubtitle": "Keep the momentum going!", "shareWorkout": "Share Your Workout", - "shareText": "I just completed {{title}}! \uD83D\uDD25 {{calories}} calories in {{duration}} minutes.", + "shareText": "I just completed {{title}}! {{calories}} calories in {{duration}} minutes.", "recommendedNext": "Recommended Next", - "backToHome": "Back to Home" + "backToHome": "Back to Home", + "feedbackTitle": "How was it?", + "feedbackHard": "Hard", + "feedbackPerfect": "Perfect", + "feedbackEasy": "Too Easy", + "thisWeek": "This Week", + "share": "Share", + "shareTitle": "Session complete · {{minutes}} min", + "streakDays_one": "{{count}} day in a row", + "streakDays_other": "{{count}} days in a row", + "streakRecord_one": "Record: {{count}} day", + "streakRecord_other": "Record: {{count}} days" }, - - "collection": { - "notFound": "Collection not found", - "minTotal": "{{count}} min total" - }, - - "category": { - "allLevels": "All Levels" - }, - - "workout": { - "notFound": "Workout not found", - "whatYoullNeed": "What You'll Need", - "exercises": "Exercises ({{count}} rounds)", - "repeatRounds": "Repeat \u00D7 {{count}} rounds", - "music": "Music", - "musicMix": "{{vibe}} Mix", - "curatedForWorkout": "Curated for your workout", - "startWorkout": "START WORKOUT", - "unlockWithPremium": "UNLOCK WITH TABATAFIT+" - }, - - "paywall": { - "subtitle": "Unlock all features and reach your goals faster", - "features": { - "music": "Premium Music", - "workouts": "Unlimited Workouts", - "stats": "Advanced Stats", - "calories": "Calorie Tracking", - "reminders": "Daily Reminders", - "ads": "No Ads" + "terms": { + "title": "Terms of Service", + "lastUpdated": "Last Updated: April 2026", + "acceptance": { + "title": "Acceptance of Terms", + "content": "By downloading or using TabataFit, you agree to be bound by these Terms of Service." }, - "yearly": "Yearly", - "monthly": "Monthly", - "perYear": "per year", - "perMonth": "per month", - "save50": "SAVE 50%", - "equivalent": "Just {{price}}/month", - "subscribe": "Subscribe Now", - "trialCta": "Start Free Trial", - "processing": "Processing...", - "restore": "Restore Purchases", - "terms": "Payment will be charged to your Apple ID at confirmation. Subscription auto-renews unless cancelled at least 24 hours before end of period. Manage in Account Settings." - }, - - "onboarding": { - "problem": { - "title": "You don't have 1 hour\nfor the gym.", - "subtitle1": "Nobody does.", - "subtitle2": "Yet you want results. We have the solution.", - "cta": "Show me how" + "service": { + "title": "Description of Service", + "content": "TabataFit provides guided Tabata workout programs. The app is not a substitute for professional medical advice." }, - "empathy": { - "title": "What's holding you back?", - "chooseUpTo": "Choose up to 2", - "noTime": "No time", - "lowMotivation": "Low motivation", - "noKnowledge": "Don't know what to do", - "noGym": "No gym access" + "subscriptions": { + "title": "Subscriptions & Payments", + "content": "Some features require a paid subscription managed through the App Store or Google Play. Prices may vary by region. Free trials convert to paid subscriptions unless cancelled before the trial ends." }, - "solution": { - "title": "4 minutes.\nTruly transformative.", - "tabataCalories": "85 kcal", - "cardioCalories": "90 kcal", - "tabata": "Tabata", - "cardio": "Cardio", - "tabataDuration": "4 min", - "cardioDuration": "30 min", - "vs": "VS", - "citation": "\"The Tabata method increases aerobic and anaerobic capacity simultaneously.\"", - "citationAuthor": "— Dr. Izumi Tabata, 1996", - "cta": "I'm convinced" + "cancellation": { + "title": "Cancellation", + "content": "You can cancel your subscription at any time through your device settings. Cancellation takes effect at the end of the current billing period." }, - "wow": { - "title": "Your app, previewed.", - "subtitle": "Everything you need to transform.", - "card1Title": "The Perfect Timer", - "card1Subtitle": "WORK, REST, REPEAT — precisely timed phases with visual feedback.", - "card2Title": "50+ Expert Workouts", - "card2Subtitle": "From 4-minute blasts to 20-minute burns. Beginner to advanced.", - "card3Title": "Smart Coaching", - "card3Subtitle": "Voice cues and haptic feedback keep you in the zone.", - "card4Title": "Track Your Progress", - "card4Subtitle": "Weekly streaks, calorie tracking, and personal records." + "liability": { + "title": "Limitation of Liability", + "content": "TabataFit is provided as-is. We are not liable for any injuries resulting from workouts. Always consult a healthcare professional before starting any exercise program." }, - "personalization": { - "title": "Let's set up your\nfirst week.", - "yourName": "YOUR NAME", - "namePlaceholder": "Enter your name", - "fitnessLevel": "FITNESS LEVEL", - "yourGoal": "YOUR GOAL", - "weeklyFrequency": "WEEKLY FREQUENCY", - "readyMessage": "Your personalized program is ready.", - "goals": { - "weightLoss": "Weight Loss", - "cardio": "Cardio", - "strength": "Strength", - "wellness": "Wellness" - }, - "frequencies": { - "2x": "2x / week", - "3x": "3x / week", - "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", - "intro": { - "title": "Introduction", - "content": "TabataFit is committed to protecting your privacy. This policy explains how we collect, use, and safeguard your information when you use our fitness app." - }, - "dataCollection": { - "title": "Data We Collect", - "content": "We collect only the information necessary to provide you with the best workout experience:", - "items": { - "workouts": "Workout history and preferences", - "settings": "App settings and configurations", - "device": "Device type and OS version for optimization" - } - }, - "usage": { - "title": "How We Use Your Data", - "content": "Your data is used solely to: personalize your workout experience, track your progress and achievements, sync your data across devices, and improve our app functionality." - }, - "sharing": { - "title": "Data Sharing", - "content": "We do not sell or share your personal information with third parties. Your workout data remains private and secure on your device and in encrypted cloud storage." - }, - "security": { - "title": "Security", - "content": "We implement industry-standard security measures to protect your data, including encryption and secure authentication." - }, - "rights": { - "title": "Your Rights", - "content": "You have the right to access, modify, or delete your personal data at any time. You can export or delete your data from the app settings." - }, - "contact": { - "title": "Contact Us", - "content": "If you have questions about this privacy policy, please contact us at:" - } + "contact": { + "title": "Contact", + "content": "For questions about these terms, contact us at:" + } + }, + "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", + "termsLink": "Terms of Service", + "privacyLink": "Privacy Policy" + }, + "privacy": { + "title": "Privacy Policy", + "lastUpdated": "Last Updated: March 2026", + "intro": { + "title": "Introduction", + "content": "TabataFit is committed to protecting your privacy. This policy explains how we collect, use, and safeguard your information when you use our fitness app." + }, + "dataCollection": { + "title": "Data We Collect", + "content": "We collect only the information necessary to provide you with the best workout experience:", + "items": { + "workouts": "Workout history and preferences", + "settings": "App settings and configurations", + "device": "Device type and OS version for optimization" + } + }, + "usage": { + "title": "How We Use Your Data", + "content": "Your data is used solely to: personalize your workout experience, track your progress and achievements, sync your data across devices, and improve our app functionality." + }, + "sharing": { + "title": "Data Sharing", + "content": "We do not sell or share your personal information with third parties. Your workout data remains private and secure on your device and in encrypted cloud storage." + }, + "security": { + "title": "Security", + "content": "We implement industry-standard security measures to protect your data, including encryption and secure authentication." + }, + "rights": { + "title": "Your Rights", + "content": "You have the right to access, modify, or delete your personal data at any time. You can export or delete your data from the app settings." + }, + "contact": { + "title": "Contact Us", + "content": "If you have questions about this privacy policy, please contact us at:" } }, - "programs": { "title": "Programs", "weeks": "Weeks", @@ -399,52 +336,6 @@ "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", - "tip1": "Move at your own pace", - "tip2": "Focus on form, not speed", - "tip3": "This helps us recommend the best program", - "tip4": "No judgment - just a starting point!", - "duration": "Duration", - "exercises": "Exercises" - }, - - "kine": { - "programs": "Physio Programs", - "recommendedNext": "Recommended Next Session", - "continueSession": "Continue Session", - "startSession": "Start Session", - "unlockPremium": "Unlock with Premium", - "free": "FREE", - "premium": "PREMIUM", - "weeks": "weeks", - "sessionsPerWeek": "sessions/week", - "sessions": "sessions", - "deload": "Deload", - "warmup": "Warmup", - "cooldown": "Cooldown", - "block": "Block", - "rounds": "rounds" - }, - "workoutProgram": { "tabata": "Tabata", "exercise1": "Exercise 1", @@ -455,5 +346,56 @@ "beginner": "Beginner", "intermediate": "Intermediate", "advanced": "Advanced" + }, + "zone": { + "chooseYourFocus": "Choose your focus", + "upperBody": { + "label": "Upper Body", + "description": "Arms, shoulders, chest, and back." + }, + "lowerBody": { + "label": "Lower Body", + "description": "Legs, glutes, and core foundation." + }, + "fullBody": { + "label": "Full Body", + "description": "Complete workouts, every muscle group." + }, + "chooseLevel": "Choose your level", + "emptyPrograms": "No programs at this level yet." + }, + "settings": { + "title": "Settings", + "sectionProfile": "PROFILE", + "name": "Name", + "subscription": "Subscription", + "premium": "Premium", + "free": "Free", + "upgradePremium": "Upgrade to Premium", + "sectionPrefs": "PREFERENCES", + "haptics": "Haptics", + "soundEffects": "Sound Effects", + "voiceCoaching": "Voice Coaching", + "music": "Music", + "sectionLegal": "LEGAL", + "terms": "Terms of Service", + "privacy": "Privacy Policy", + "sectionData": "DATA", + "resetProgress": "Reset Progress", + "resetTitle": "Reset Progress?", + "resetMessage": "This action is irreversible. Your streak, history, and completed programs will be erased.", + "resetConfirm": "Reset", + "version": "TabataGo · v1.0" + }, + "program": { + "notFound": "Program not found", + "warmup": "Warm-up", + "stretch": "Stretch", + "exercise1": "Exercise 1", + "exercise2": "Exercise 2", + "tabataLabel": "Tabata {{num}}", + "tabataSubtitle": "{{rounds}} rounds · {{work}}s / {{rest}}s", + "startSession": "Start Session", + "unlockPremium": "Unlock with Premium" } } diff --git a/src/shared/i18n/locales/es/common.json b/src/shared/i18n/locales/es/common.json index 867ed43..71f0668 100644 --- a/src/shared/i18n/locales/es/common.json +++ b/src/shared/i18n/locales/es/common.json @@ -9,6 +9,7 @@ "cancel": "Cancelar", "save": "Guardar", "loading": "Cargando...", + "offline": "Sin conexi\u00f3n a internet", "levels": { "beginner": "Principiante", diff --git a/src/shared/i18n/locales/es/screens.json b/src/shared/i18n/locales/es/screens.json index 78b2c77..9b158f0 100644 --- a/src/shared/i18n/locales/es/screens.json +++ b/src/shared/i18n/locales/es/screens.json @@ -7,7 +7,6 @@ "progression": "Progresión", "profile": "Perfil" }, - "home": { "readyToCrush": "¿Listo para arrasar hoy?", "featured": "DESTACADO", @@ -15,7 +14,7 @@ "popularThisWeek": "Popular esta semana", "collections": "Colecciones", "chooseYourPath": "Elige tu camino", - "continueYourJourney": "Contin\u00faa tu viaje", + "continueYourJourney": "Continúa tu viaje", "yourPrograms": "Tus programas", "programsSubtitle": "Diseñados por fisioterapeutas", "switchProgram": "Cambiar programa", @@ -26,6 +25,7 @@ "lowerBody": "Parte inferior", "fullBody": "Cuerpo completo", "programsByZone": "Programas por zona", + "filterAll": "Todos", "tabataCount": "{{count}} tabatas", "freeBadge": "GRATIS", "premiumBadge": "PREMIUM", @@ -34,9 +34,17 @@ "unlockPremium": "Desbloquear Premium", "tabataPrograms": "Programas de fisio", "tabataProgramsSubtitle": "Programas de rehabilitación y fisioterapia", - "recommendedNext": "Continuar tu sesión" + "recommendedNext": "Continuar tu sesión", + "mascotStreak": "¡Racha de {{count}} días{{name}}! 🔥", + "mascotReady": "¿Listo para entrenar{{name}}?", + "statsCompleted": "Programas", + "zoneSubtitle": "3 niveles · Principiante → Avanzado", + "zoneDescUpper": "Hombros, pecho, brazos y core", + "zoneDescLower": "Piernas, glúteos y movilidad de cadera", + "zoneDescFull": "Acondicionamiento total y cardio", + "zoneLevels": "3 niveles", + "zonePrograms": "{{count}} programas" }, - "explore": { "title": "Explorar", "collections": "Colecciones", @@ -72,17 +80,16 @@ "errorRetry": "Toca para reintentar", "featuredCollection": "Colección destacada" }, - "activity": { "title": "Actividad", - "dayStreak": "d\u00edas consecutivos", - "longest": "M\u00c1S LARGO", + "dayStreak": "días consecutivos", + "longest": "MÁS LARGO", "workouts": "Entrenos", "minutes": "Minutos", - "calories": "Calor\u00edas", + "calories": "Calorías", "bestStreak": "Mejor racha", "thisWeek": "Esta semana", - "ofDays": "{{completed}} de 7 d\u00edas", + "ofDays": "{{completed}} de 7 días", "recent": "Recientes", "today": "Hoy", "yesterday": "Ayer", @@ -92,22 +99,6 @@ "emptySubtitle": "Completa tu primer entreno y tus estadísticas aparecerán aquí.", "startFirstWorkout": "Comienza tu primer entreno" }, - - "browse": { - "title": "Explorar", - "searchPlaceholder": "Buscar entrenos...", - "searchResults": "Resultados", - "allWorkouts": "Todos los entrenos", - "noResults": "No se encontraron entrenos", - "tryDifferentSearch": "Pruebe una búsqueda o categoría diferente", - "featured": "DESTACADO", - "collections": "Colecciones", - "programs": "Programas", - "newReleases": "Novedades", - "weeksCount": "{{count}} semanas", - "timesPerWeek": "{{count}}x /semana" - }, - "profile": { "title": "Perfil", "guest": "Invitado", @@ -116,12 +107,12 @@ "sectionWorkout": "ENTRENAMIENTO", "sectionNotifications": "NOTIFICACIONES", "sectionAbout": "ACERCA DE", - "sectionSubscription": "SUSCRIPCI\u00d3N", - "email": "Correo electr\u00f3nico", + "sectionSubscription": "SUSCRIPCIÓN", + "email": "Correo electrónico", "plan": "Plan", "freePlan": "Gratis", "restorePurchases": "Restaurar compras", - "hapticFeedback": "Retroalimentaci\u00f3n h\u00e1ptica", + "hapticFeedback": "Retroalimentación háptica", "soundEffects": "Efectos de sonido", "voiceCoaching": "Coaching por voz", "dailyReminders": "Recordatorios diarios", @@ -129,102 +120,121 @@ "reminderFooter": "Recibe un recordatorio diario para mantener tu racha", "workoutSettingsFooter": "Personaliza tu experiencia de entrenamiento", "upgradeTitle": "Desbloquear TabataFit+", - "upgradeDescription": "Obt\u00e9n entrenos ilimitados, descargas sin conexi\u00f3n y m\u00e1s.", - "learnMore": "M\u00e1s informaci\u00f3n", - "version": "Versi\u00f3n", - "privacyPolicy": "Pol\u00edtica de privacidad", - "signOut": "Cerrar sesi\u00f3n", + "upgradeDescription": "Obtén entrenos ilimitados, descargas sin conexión y más.", + "learnMore": "Más información", + "version": "Versión", + "privacyPolicy": "Política de privacidad", + "termsOfService": "Términos de servicio", + "signOut": "Cerrar sesión", "statsWorkouts": "entrenos", - "statsStreak": "d\u00edas consecutivos", - "statsCalories": "calor\u00edas", + "statsStreak": "días consecutivos", + "statsCalories": "calorías", "faq": "FAQ", "contactUs": "Contactarnos", "rateApp": "Calificar app", "sectionPremium": "Actualizar a Premium", - "sectionPersonalization": "PERSONALIZACI\u00d3N", - "personalization": "Personalizaci\u00f3n", + "sectionPersonalization": "PERSONALIZACIÓN", + "personalization": "Personalización", "personalizationEnabled": "Recomendaciones IA activas", "personalizationDisabled": "Active para entrenos personalizados", - "enablePersonalization": "Activar personalizaci\u00f3n", + "enablePersonalization": "Activar personalización", "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" + "secure": "Tus datos están cifrados y seguros" }, - "privacy": "Guardaremos tu historial de entrenos para crear tu programa personalizado. Puedes eliminar estos datos en cualquier momento en Configuraci\u00f3n.", + "privacy": "Guardaremos tu historial de entrenos para crear tu programa personalizado. Puedes eliminar estos datos en cualquier momento en Configuración.", "primaryButton": "Personalizar mi experiencia", - "secondaryButton": "Continuar con programas gen\u00e9ricos" + "secondaryButton": "Continuar con programas genéricos" }, - "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.", + "title": "¿Eliminar tus datos?", + "description": "Esto eliminará permanentemente tu historial de entrenos sincronizado y tus datos de personalización de nuestros servidores.", + "note": "Tu historial de entrenos local se mantendrá en este dispositivo. Continuarás con programas genéricos.", "deleteButton": "Eliminar mis datos", "cancelButton": "Conservar mis datos" }, - "player": { "phases": { - "prep": "PREP\u00c1RATE", + "prep": "PREPÁRATE", "work": "TRABAJO", "rest": "DESCANSO", - "complete": "COMPLETO" + "complete": "COMPLETO", + "warmup": "CALENTAMIENTO", + "stretch": "ESTIRAMIENTO", + "trans": "TRANSICIÓN" }, "current": "Actual", "next": "Siguiente: ", "round": "Ronda", "burnBar": "Barra de quema", "communityAvg": "Media comunidad: {{calories}} cal", - "workoutComplete": "\u00a1Entreno completo!", - "greatJob": "\u00a1Buen trabajo!", + "workoutComplete": "¡Entreno completo!", + "greatJob": "¡Buen trabajo!", "rounds": "Rondas", - "calories": "Calor\u00edas", - "minutes": "Minutos" + "calories": "Calorías", + "minutes": "Minutos", + "mute": "Silenciar", + "unmute": "Activar sonido", + "quitTitle": "¿Salir del entreno?", + "quitMessage": "Tu progreso se perderá.", + "quitConfirm": "Salir", + "quitCancel": "Cancelar", + "warmupTitle": "Calentamiento", + "stretchTitle": "Estiramiento", + "nextBlock": "Siguiente: Bloque {{num}}", + "sessionComplete": "¡Entrenamiento completo!", + "greatWork": "Excelente trabajo", + "blocks": "Bloques" }, - "complete": { "title": "ENTRENO COMPLETO", - "caloriesLabel": "CALOR\u00cdAS", + "caloriesLabel": "CALORÍAS", "minutesLabel": "MINUTOS", "completeLabel": "COMPLETO", "burnBar": "Barra de quema", - "burnBarResult": "\u00a1Superaste al {{percentile}}% de usuarios!", - "streakTitle": "\u00a1{{count}} d\u00edas consecutivos!", - "streakSubtitle": "\u00a1Mant\u00e9n el impulso!", + "burnBarResult": "¡Superaste al {{percentile}}% de usuarios!", + "streakTitle": "¡{{count}} días consecutivos!", + "streakSubtitle": "¡Mantén el impulso!", "shareWorkout": "Comparte tu entreno", - "shareText": "\u00a1Acabo de completar {{title}}! \uD83D\uDD25 {{calories}} calor\u00edas en {{duration}} minutos.", - "recommendedNext": "Recomendado a continuaci\u00f3n", - "backToHome": "Volver al inicio" + "shareText": "¡Acabo de completar {{title}}! 🔥 {{calories}} calorías en {{duration}} minutos.", + "recommendedNext": "Recomendado a continuación", + "backToHome": "Volver al inicio", + "feedbackTitle": "¿Cómo fue el entreno?", + "feedbackEasy": "Fácil", + "feedbackPerfect": "Perfecto", + "feedbackHard": "Difícil", + "thisWeek": "Esta semana", + "share": "Compartir", + "shareTitle": "Sesión completada · {{minutes}} min", + "streakDays_one": "{{count}} día seguido", + "streakDays_other": "{{count}} días seguidos", + "streakRecord_one": "Récord: {{count}} día", + "streakRecord_other": "Récord: {{count}} días" }, - "collection": { - "notFound": "Colecci\u00f3n no encontrada", + "notFound": "Colección no encontrada", "minTotal": "{{count}} min en total" }, - "category": { "allLevels": "Todos los niveles" }, - "workout": { "notFound": "Entreno no encontrado", - "whatYoullNeed": "Lo que necesitar\u00e1s", + "whatYoullNeed": "Lo que necesitarás", "exercises": "Ejercicios ({{count}} rondas)", - "repeatRounds": "Repetir \u00D7 {{count}} rondas", - "music": "M\u00fasica", + "repeatRounds": "Repetir × {{count}} rondas", + "music": "Música", "musicMix": "Mix {{vibe}}", "curatedForWorkout": "Seleccionado para tu entreno", "startWorkout": "EMPEZAR ENTRENO", "unlockWithPremium": "DESBLOQUEAR CON TABATAFIT+" }, - "paywall": { "subtitle": "Desbloquea todas las funciones y alcanza tus metas más rápido", "features": { @@ -245,22 +255,23 @@ "trialCta": "Empezar Prueba Gratis", "processing": "Procesando...", "restore": "Restaurar Compras", - "terms": "El pago se cargará a tu Apple ID al confirmar. La suscripción se renueva automáticamente a menos que se cancele al menos 24 horas antes del final del período. Gestiona en Ajustes de la cuenta." + "terms": "El pago se cargará a tu Apple ID al confirmar. La suscripción se renueva automáticamente a menos que se cancele al menos 24 horas antes del final del período. Gestiona en Ajustes de la cuenta.", + "termsLink": "Términos de servicio", + "privacyLink": "Política de privacidad" }, - "onboarding": { "problem": { "title": "No tienes 1 hora\npara el gimnasio.", "subtitle1": "Nadie la tiene.", - "subtitle2": "Pero quieres resultados. Tenemos la soluci\u00f3n.", - "cta": "Ens\u00e9\u00f1ame c\u00f3mo" + "subtitle2": "Pero quieres resultados. Tenemos la solución.", + "cta": "Enséñame cómo" }, "empathy": { - "title": "\u00bfQu\u00e9 te frena?", + "title": "¿Qué te frena?", "chooseUpTo": "Elige hasta 2", "noTime": "Sin tiempo", - "lowMotivation": "Poca motivaci\u00f3n", - "noKnowledge": "No s\u00e9 qu\u00e9 hacer", + "lowMotivation": "Poca motivación", + "noKnowledge": "No sé qué hacer", "noGym": "Sin acceso al gimnasio" }, "solution": { @@ -272,21 +283,21 @@ "tabataDuration": "4 min", "cardioDuration": "30 min", "vs": "VS", - "citation": "\"El m\u00e9todo Tabata aumenta la capacidad aer\u00f3bica y anaer\u00f3bica simult\u00e1neamente.\"", + "citation": "\"El método Tabata aumenta la capacidad aeróbica y anaeróbica simultáneamente.\"", "citationAuthor": "— Dr. Izumi Tabata, 1996", "cta": "Estoy convencido/a" }, "wow": { "title": "Tu app, en vista previa.", "subtitle": "Todo lo que necesitas para transformarte.", - "card1Title": "El cron\u00f3metro perfecto", - "card1Subtitle": "TRABAJO, DESCANSO, REPETIR \u2014 fases cronometradas con retroalimentaci\u00f3n visual.", + "card1Title": "El cronómetro perfecto", + "card1Subtitle": "TRABAJO, DESCANSO, REPETIR — fases cronometradas con retroalimentación visual.", "card2Title": "50+ entrenos expertos", "card2Subtitle": "De 4 minutos intensos a 20 minutos de quema. Principiante a avanzado.", "card3Title": "Coaching inteligente", - "card3Subtitle": "Indicaciones de voz y retroalimentaci\u00f3n h\u00e1ptica para mantenerte en la zona.", + "card3Subtitle": "Indicaciones de voz y retroalimentación háptica para mantenerte en la zona.", "card4Title": "Sigue tu progreso", - "card4Subtitle": "Rachas semanales, seguimiento de calor\u00edas y r\u00e9cords personales." + "card4Subtitle": "Rachas semanales, seguimiento de calorías y récords personales." }, "personalization": { "title": "Configuremos tu\nprimera semana.", @@ -295,9 +306,9 @@ "fitnessLevel": "NIVEL DE FORMA", "yourGoal": "TU OBJETIVO", "weeklyFrequency": "FRECUENCIA SEMANAL", - "readyMessage": "Tu programa personalizado est\u00e1 listo.", + "readyMessage": "Tu programa personalizado está listo.", "goals": { - "weightLoss": "P\u00e9rdida de peso", + "weightLoss": "Pérdida de peso", "cardio": "Cardio", "strength": "Fuerza", "wellness": "Bienestar" @@ -309,24 +320,23 @@ } }, "paywall": { - "title": "Mant\u00e9n el impulso.\nSin l\u00edmites.", + "title": "Mantén el impulso.\nSin límites.", "features": { "unlimited": "Entrenos ilimitados", - "offline": "Descargas sin conexi\u00f3n", - "stats": "Estad\u00edsticas avanzadas y Apple Watch", + "offline": "Descargas sin conexión", + "stats": "Estadísticas avanzadas y Apple Watch", "noAds": "Sin anuncios + Compartir en familia" }, "bestValue": "MEJOR OFERTA", "yearlyPrice": "$49.99", "monthlyPrice": "$6.99", "savePercent": "Ahorra 42%", - "trialCta": "EMPEZAR PRUEBA GRATIS (7 d\u00edas)", - "guarantees": "Cancela cuando quieras \u00B7 Garant\u00eda de devoluci\u00f3n de 30 d\u00edas", + "trialCta": "EMPEZAR PRUEBA GRATIS (7 días)", + "guarantees": "Cancela cuando quieras · Garantía de devolución de 30 días", "restorePurchases": "Restaurar compras", - "skipButton": "Continuar sin suscripci\u00f3n" + "skipButton": "Continuar sin suscripción" } }, - "programs": { "title": "Programas", "weeks": "Semanas", @@ -339,14 +349,14 @@ "equipment": "Equipo", "optional": "(opcional)", "bodyweightOnly": "Solo peso corporal", - "focusAreas": "\u00c1reas de enfoque", + "focusAreas": "Áreas de enfoque", "exercises": "ejercicios", "of": "de", "complete": "completado", "completed": "Completado", "notStarted": "No iniciado", "inProgress": "En progreso", - "allWorkoutsComplete": "\u00a1Todos los entrenamientos completados!", + "allWorkoutsComplete": "¡Todos los entrenamientos completados!", "status": { "notStarted": "No iniciado", "inProgress": "En progreso", @@ -362,52 +372,6 @@ "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", - "tip1": "Muévete a tu propio ritmo", - "tip2": "Concéntrate en la forma, no en la velocidad", - "tip3": "Esto nos ayuda a recomendar el mejor programa", - "tip4": "Sin juicios - ¡solo un punto de partida!", - "duration": "Duración", - "exercises": "Ejercicios" - }, - - "kine": { - "programs": "Programas de Fisio", - "recommendedNext": "Próxima sesión recomendada", - "continueSession": "Continuar sesión", - "startSession": "Comenzar sesión", - "unlockPremium": "Desbloquear con Premium", - "free": "GRATIS", - "premium": "PREMIUM", - "weeks": "semanas", - "sessionsPerWeek": "sesiones/sem", - "sessions": "sesiones", - "deload": "Descarga", - "warmup": "Calentamiento", - "cooldown": "Enfriamiento", - "block": "Bloque", - "rounds": "rondas" - }, - "workoutProgram": { "tabata": "Tabata", "exercise1": "Ejercicio 1", @@ -418,5 +382,84 @@ "beginner": "Principiante", "intermediate": "Intermedio", "advanced": "Avanzado" + }, + "terms": { + "title": "Términos de servicio", + "lastUpdated": "Última actualización: marzo 2026", + "acceptance": { + "title": "Aceptación", + "content": "Al usar TabataFit, aceptas estos términos de servicio." + }, + "service": { + "title": "Descripción del servicio", + "content": "TabataFit es una app de fitness para entrenamiento Tabata con temporizador, programas y seguimiento de progreso." + }, + "subscription": { + "title": "Suscripción", + "content": "Las funciones premium requieren suscripción. El cobro se realiza a través de tu Apple ID. La renovación automática se puede desactivar en Ajustes de la cuenta." + }, + "liability": { + "title": "Limitación de responsabilidad", + "content": "TabataFit no se hace responsable de lesiones. Consulta a un médico antes de comenzar cualquier programa de ejercicios." + }, + "ip": { + "title": "Propiedad intelectual", + "content": "Todo el contenido está protegido por derechos de autor." + }, + "contact": { + "title": "Contacto", + "content": "Para consultas: support@tabatafit.app" + } + }, + "zone": { + "chooseYourFocus": "Elige tu enfoque", + "upperBody": { + "label": "Tren superior", + "description": "Brazos, hombros, pecho y espalda." + }, + "lowerBody": { + "label": "Tren inferior", + "description": "Piernas, glúteos y core." + }, + "fullBody": { + "label": "Cuerpo completo", + "description": "Entrenamientos completos, cada grupo muscular." + }, + "chooseLevel": "Elige tu nivel", + "emptyPrograms": "Ningún programa en este nivel por ahora." + }, + "settings": { + "title": "Ajustes", + "sectionProfile": "PERFIL", + "name": "Nombre", + "subscription": "Suscripción", + "premium": "Premium", + "free": "Gratuito", + "upgradePremium": "Actualizar a Premium", + "sectionPrefs": "PREFERENCIAS", + "haptics": "Vibración", + "soundEffects": "Efectos de sonido", + "voiceCoaching": "Entrenador de voz", + "music": "Música", + "sectionLegal": "LEGAL", + "terms": "Términos de servicio", + "privacy": "Política de privacidad", + "sectionData": "DATOS", + "resetProgress": "Restablecer progreso", + "resetTitle": "¿Restablecer progreso?", + "resetMessage": "Esta acción es irreversible. Tu racha, historial y programas completados serán borrados.", + "resetConfirm": "Restablecer", + "version": "TabataGo · v1.0" + }, + "program": { + "notFound": "Programa no encontrado", + "warmup": "Calentamiento", + "stretch": "Estiramientos", + "exercise1": "Ejercicio 1", + "exercise2": "Ejercicio 2", + "tabataLabel": "Tabata {{num}}", + "tabataSubtitle": "{{rounds}} rondas · {{work}}s / {{rest}}s", + "startSession": "Iniciar sesión", + "unlockPremium": "Desbloquear con Premium" } } diff --git a/src/shared/i18n/locales/fr/common.json b/src/shared/i18n/locales/fr/common.json index 248cb95..1b25354 100644 --- a/src/shared/i18n/locales/fr/common.json +++ b/src/shared/i18n/locales/fr/common.json @@ -9,6 +9,7 @@ "cancel": "Annuler", "save": "Enregistrer", "loading": "Chargement...", + "offline": "Pas de connexion internet", "levels": { "beginner": "D\u00e9butant", diff --git a/src/shared/i18n/locales/fr/screens.json b/src/shared/i18n/locales/fr/screens.json index 647c212..b50e05b 100644 --- a/src/shared/i18n/locales/fr/screens.json +++ b/src/shared/i18n/locales/fr/screens.json @@ -7,11 +7,10 @@ "progression": "Progression", "profile": "Profil" }, - "home": { "readyToCrush": "Prêt à tout casser aujourd'hui ?", - "featured": "\u00c0 LA UNE", - "recent": "R\u00e9cents", + "featured": "À LA UNE", + "recent": "Récents", "popularThisWeek": "Populaires cette semaine", "collections": "Collections", "chooseYourPath": "Choisissez votre parcours", @@ -19,13 +18,14 @@ "yourPrograms": "Vos programmes", "programsSubtitle": "Conçus par des kinésithérapeutes", "switchProgram": "Changer de programme", - "statsStreak": "S\u00e9rie", + "statsStreak": "Série", "statsThisWeek": "Cette semaine", "statsMinutes": "Minutes", "upperBody": "Haut du corps", "lowerBody": "Bas du corps", "fullBody": "Corps entier", "programsByZone": "Programmes par zone", + "filterAll": "Tous", "tabataCount": "{{count}} tabatas", "freeBadge": "GRATUIT", "premiumBadge": "PREMIUM", @@ -34,9 +34,17 @@ "unlockPremium": "Débloquer Premium", "tabataPrograms": "Programmes Tabata", "tabataProgramsSubtitle": "Programmes de rééducation et physiothérapie", - "recommendedNext": "Continuer votre séance" + "recommendedNext": "Continuer votre séance", + "mascotStreak": "Streak de {{count}} jours{{name}} ! 🔥", + "mascotReady": "Prêt à bouger{{name}} ?", + "statsCompleted": "Programmes", + "zoneSubtitle": "3 niveaux · Débutant → Avancé", + "zoneDescUpper": "Épaules, poitrine, bras & gainage", + "zoneDescLower": "Jambes, fessiers & mobilité des hanches", + "zoneDescFull": "Renforcement complet & cardio", + "zoneLevels": "3 niveaux", + "zonePrograms": "{{count}} programmes" }, - "explore": { "title": "Explorer", "collections": "Collections", @@ -72,7 +80,6 @@ "errorRetry": "Appuyez pour réessayer", "featuredCollection": "Collection en vedette" }, - "activity": { "title": "Activité", "dayStreak": "jours consécutifs", @@ -92,22 +99,6 @@ "emptySubtitle": "Terminez votre premier entraînement et vos statistiques apparaîtront ici.", "startFirstWorkout": "Commencez votre premier entraînement" }, - - "browse": { - "title": "Explorer", - "searchPlaceholder": "Rechercher des exercices...", - "searchResults": "Résultats", - "allWorkouts": "Tous les exercices", - "noResults": "Aucun exercice trouvé", - "tryDifferentSearch": "Essayez une recherche ou catégorie différente", - "featured": "À LA UNE", - "collections": "Collections", - "programs": "Programmes", - "newReleases": "Nouveautés", - "weeksCount": "{{count}} semaines", - "timesPerWeek": "{{count}}x /semaine" - }, - "profile": { "title": "Profil", "guest": "Invité", @@ -133,6 +124,7 @@ "learnMore": "En savoir plus", "version": "Version", "privacyPolicy": "Politique de confidentialité", + "termsOfService": "Conditions d'utilisation", "signOut": "Se déconnecter", "statsWorkouts": "entraînements", "statsStreak": "jours consécutifs", @@ -148,7 +140,6 @@ "enablePersonalization": "Activer la personnalisation", "deleteData": "Supprimer mes données" }, - "sync": { "title": "Débloquez les entraînements personnalisés", "benefits": { @@ -161,7 +152,6 @@ "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.", @@ -169,13 +159,15 @@ "deleteButton": "Supprimer mes données", "cancelButton": "Conserver mes données" }, - "player": { "phases": { "prep": "PRÉPAREZ-VOUS", "work": "EFFORT", "rest": "REPOS", - "complete": "TERMINÉ" + "complete": "TERMINÉ", + "warmup": "ÉCHAUFFEMENT", + "stretch": "ÉTIREMENT", + "trans": "TRANSITION" }, "current": "En cours", "next": "Suivant : ", @@ -186,9 +178,20 @@ "greatJob": "Bien joué !", "rounds": "Rounds", "calories": "Calories", - "minutes": "Minutes" + "minutes": "Minutes", + "mute": "Muet", + "unmute": "Son", + "quitTitle": "Quitter la séance ?", + "quitMessage": "Votre progression ne sera pas sauvegardée. Êtes-vous sûr(e) ?", + "quitConfirm": "Quitter", + "quitCancel": "Continuer", + "warmupTitle": "Échauffement", + "stretchTitle": "Étirement", + "nextBlock": "Prochain : Bloc {{num}}", + "sessionComplete": "Séance terminée !", + "greatWork": "Excellent travail", + "blocks": "Blocs" }, - "complete": { "title": "ENTRAÎNEMENT TERMINÉ", "caloriesLabel": "CALORIES", @@ -201,18 +204,54 @@ "shareWorkout": "Partagez votre entraînement", "shareText": "Je viens de terminer {{title}} ! 🔥 {{calories}} calories en {{duration}} minutes.", "recommendedNext": "Recommandé ensuite", - "backToHome": "Retour à l'accueil" + "backToHome": "Retour à l'accueil", + "feedbackTitle": "Comment c'était ?", + "feedbackHard": "Dur", + "feedbackPerfect": "Parfait", + "feedbackEasy": "Trop facile", + "thisWeek": "Cette semaine", + "share": "Partager", + "shareTitle": "Séance terminée · {{minutes}} min", + "streakDays_one": "{{count}} jour d'affilée", + "streakDays_other": "{{count}} jours d'affilée", + "streakRecord_one": "Record : {{count}} jour", + "streakRecord_other": "Record : {{count}} jours" + }, + "terms": { + "title": "Conditions Générales d'Utilisation", + "lastUpdated": "Dernière mise à jour : Avril 2026", + "acceptance": { + "title": "Acceptation des conditions", + "content": "En téléchargeant ou en utilisant TabataFit, vous acceptez d'être lié par ces Conditions Générales d'Utilisation." + }, + "service": { + "title": "Description du service", + "content": "TabataFit propose des programmes d'entraînement Tabata guidés. L'application ne remplace pas un avis médical professionnel." + }, + "subscriptions": { + "title": "Abonnements et paiements", + "content": "Certaines fonctionnalités nécessitent un abonnement payant géré via l'App Store ou Google Play. Les prix peuvent varier selon la région. Les essais gratuits se convertissent en abonnements payants sauf annulation avant la fin de l'essai." + }, + "cancellation": { + "title": "Annulation", + "content": "Vous pouvez annuler votre abonnement à tout moment via les paramètres de votre appareil. L'annulation prend effet à la fin de la période de facturation en cours." + }, + "liability": { + "title": "Limitation de responsabilité", + "content": "TabataFit est fourni tel quel. Nous ne sommes pas responsables des blessures résultant des entraînements. Consultez toujours un professionnel de santé avant de commencer un programme d'exercice." + }, + "contact": { + "title": "Contact", + "content": "Pour toute question sur ces conditions, contactez-nous à :" + } }, - "collection": { "notFound": "Collection introuvable", "minTotal": "{{count}} min au total" }, - "category": { "allLevels": "Tous les niveaux" }, - "workout": { "notFound": "Entraînement introuvable", "whatYoullNeed": "Ce qu'il vous faut", @@ -224,7 +263,6 @@ "startWorkout": "COMMENCER L'ENTRAÎNEMENT", "unlockWithPremium": "DÉBLOQUER AVEC TABATAFIT+" }, - "paywall": { "subtitle": "Débloquez toutes les fonctionnalités et atteignez vos objectifs plus vite", "features": { @@ -247,7 +285,6 @@ "restore": "Restaurer les achats", "terms": "Le paiement sera débité sur votre identifiant Apple à la confirmation. L'abonnement se renouvelle automatiquement sauf annulation au moins 24h avant la fin de la période. Gérez dans les réglages du compte." }, - "onboarding": { "problem": { "title": "Vous n'avez pas 1 heure\npour la salle.", @@ -323,7 +360,9 @@ "trialCta": "ESSAI GRATUIT (7 jours)", "guarantees": "Annulez à tout moment · Garantie satisfait ou remboursé 30 jours", "restorePurchases": "Restaurer les achats", - "skipButton": "Continuer sans abonnement" + "skipButton": "Continuer sans abonnement", + "termsLink": "Conditions d'utilisation", + "privacyLink": "Politique de confidentialité" }, "privacy": { "title": "Politique de Confidentialité", @@ -363,7 +402,6 @@ } } }, - "programs": { "title": "Programmes", "weeks": "Semaines", @@ -399,52 +437,6 @@ "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", - "tip1": "Bougez à votre rythme", - "tip2": "Concentrez-vous sur la forme, pas la vitesse", - "tip3": "Cela nous aide à recommander le meilleur programme", - "tip4": "Sans jugement - juste un point de départ !", - "duration": "Durée", - "exercises": "Exercices" - }, - - "kine": { - "programs": "Programmes Tabata", - "recommendedNext": "Prochaine séance recommandée", - "continueSession": "Continuer la séance", - "startSession": "Commencer la séance", - "unlockPremium": "Débloquer avec Premium", - "free": "GRATUIT", - "premium": "PREMIUM", - "weeks": "semaines", - "sessionsPerWeek": "séances/sem", - "sessions": "séances", - "deload": "Décharge", - "warmup": "Échauffement", - "cooldown": "Retour au calme", - "block": "Bloc", - "rounds": "rounds" - }, - "workoutProgram": { "tabata": "Tabata", "exercise1": "Exercice 1", @@ -455,5 +447,56 @@ "beginner": "Débutant", "intermediate": "Intermédiaire", "advanced": "Avancé" + }, + "zone": { + "chooseYourFocus": "Choisis ta zone", + "upperBody": { + "label": "Haut du corps", + "description": "Bras, épaules, pectoraux et dos." + }, + "lowerBody": { + "label": "Bas du corps", + "description": "Jambes, fessiers et gainage." + }, + "fullBody": { + "label": "Corps complet", + "description": "Séances complètes, tous les groupes musculaires." + }, + "chooseLevel": "Choisis ton niveau", + "emptyPrograms": "Aucun programme à ce niveau pour le moment." + }, + "settings": { + "title": "Réglages", + "sectionProfile": "PROFIL", + "name": "Nom", + "subscription": "Abonnement", + "premium": "Premium", + "free": "Gratuit", + "upgradePremium": "Passer à Premium", + "sectionPrefs": "PRÉFÉRENCES", + "haptics": "Vibrations", + "soundEffects": "Effets sonores", + "voiceCoaching": "Coach vocal", + "music": "Musique", + "sectionLegal": "LÉGAL", + "terms": "Conditions d'utilisation", + "privacy": "Confidentialité", + "sectionData": "DONNÉES", + "resetProgress": "Réinitialiser la progression", + "resetTitle": "Réinitialiser la progression ?", + "resetMessage": "Cette action est irréversible. Ton streak, ton historique et tes programmes complétés seront effacés.", + "resetConfirm": "Réinitialiser", + "version": "TabataGo · v1.0" + }, + "program": { + "notFound": "Programme introuvable", + "warmup": "Échauffement", + "stretch": "Étirements", + "exercise1": "Exercice 1", + "exercise2": "Exercice 2", + "tabataLabel": "Tabata {{num}}", + "tabataSubtitle": "{{rounds}} rounds · {{work}}s / {{rest}}s", + "startSession": "Commencer la séance", + "unlockPremium": "Débloquer avec Premium" } -} \ No newline at end of file +} diff --git a/src/shared/services/music.ts b/src/shared/services/music.ts index 2aa7ede..e031c27 100644 --- a/src/shared/services/music.ts +++ b/src/shared/services/music.ts @@ -150,6 +150,30 @@ class MusicService { return tracks[randomIndex] } + /** + * Returns N distinct random tracks for a given vibe. + * Used to pick one track per tabata block (3 blocks = 3 tracks). + * Falls back to repeating tracks if fewer than N are available. + */ + async getRandomTracksForStyle(vibe: MusicVibe, count: number = 3): Promise { + const pool = await this.loadTracksForVibe(vibe) + if (pool.length === 0) return [] + if (pool.length <= count) { + // Not enough distinct tracks — repeat with shuffle + const shuffled = [...pool].sort(() => Math.random() - 0.5) + const picked: MusicTrack[] = [] + for (let i = 0; i < count; i++) picked.push(shuffled[i % shuffled.length]) + return picked + } + // Fisher-Yates partial shuffle + const arr = [...pool] + for (let i = arr.length - 1; i > arr.length - 1 - count; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[arr[i], arr[j]] = [arr[j], arr[i]] + } + return arr.slice(-count) + } + getNextTrack(tracks: MusicTrack[], currentTrackId: string, shuffle: boolean = false): MusicTrack | null { if (tracks.length === 0) return null if (tracks.length === 1) return tracks[0] diff --git a/src/shared/stores/index.ts b/src/shared/stores/index.ts index 53717df..0dde417 100644 --- a/src/shared/stores/index.ts +++ b/src/shared/stores/index.ts @@ -3,8 +3,6 @@ */ export { useUserStore } from './userStore' -export { useActivityStore, getWeeklyActivity } from './activityStore' export { usePlayerStore } from './playerStore' -export { useProgramStore } from './programStore' -export { useTabataProgramStore } from './tabataProgramStore' export { useWorkoutProgramStore } from './workoutProgramStore' +export { useProgressStore } from './progressStore' diff --git a/src/shared/stores/userStore.ts b/src/shared/stores/userStore.ts index 2968718..44050d1 100644 --- a/src/shared/stores/userStore.ts +++ b/src/shared/stores/userStore.ts @@ -70,6 +70,7 @@ export const useUserStore = create()( musicVolume: 0.5, reminders: false, reminderTime: '09:00', + hasPromptedReview: false, }, savedWorkouts: [], diff --git a/src/shared/types/user.ts b/src/shared/types/user.ts index f7d24a2..51df21f 100644 --- a/src/shared/types/user.ts +++ b/src/shared/types/user.ts @@ -14,6 +14,7 @@ export interface UserSettings { musicVolume: number reminders: boolean reminderTime: string + hasPromptedReview: boolean } export type FitnessLevel = 'beginner' | 'intermediate' | 'advanced' diff --git a/src/shared/types/workoutProgram.ts b/src/shared/types/workoutProgram.ts index 09e4f37..740a850 100644 --- a/src/shared/types/workoutProgram.ts +++ b/src/shared/types/workoutProgram.ts @@ -3,7 +3,8 @@ import type { MusicVibe } from './workout' /** * Workout Program Types * Body Zone + Difficulty model - * Program = 3 Tabatas, each Tabata = 2 exercises × 8 rounds + * Program = Warmup → 3 Tabatas → Stretch + * Tabata = 2 exercises × 8 rounds (20s work / 10s rest) */ // ─── Enums ────────────────────────────────────────────────────── @@ -23,6 +24,28 @@ export interface ProgramExercise { modificationEn?: string progression?: string progressionEn?: string + videoUrl?: string | null +} + +// ─── Timed Exercise (warmup / stretch) ────────────────────────── + +export interface TimedExercise { + name: string + nameEn: string + duration: number // seconds + videoUrl: string | null + tip?: string + tipEn?: string +} + +export interface WarmupBlock { + exercises: TimedExercise[] + totalDuration: number // seconds (sum of exercises.duration) +} + +export interface StretchBlock { + exercises: TimedExercise[] + totalDuration: number } // ─── Tabata ───────────────────────────────────────────────────── @@ -52,7 +75,9 @@ export interface WorkoutProgram { icon: string | null accentColor: string | null sortOrder: number + warmup: WarmupBlock tabatas: WorkoutTabata[] + stretch: StretchBlock createdAt: string updatedAt: string } @@ -88,6 +113,7 @@ export interface WorkoutTabataRow { exercise_1_modification_en: string | null exercise_1_progression: string | null exercise_1_progression_en: string | null + exercise_1_video_url: string | null exercise_2_name: string exercise_2_name_en: string | null exercise_2_tip: string | null @@ -96,12 +122,26 @@ export interface WorkoutTabataRow { exercise_2_modification_en: string | null exercise_2_progression: string | null exercise_2_progression_en: string | null + exercise_2_video_url: string | null rounds: number work_time: number rest_time: number created_at: string } +export interface WorkoutTimedExerciseRow { + id: string + program_id: string + position: number + name: string + name_en: string | null + tip: string | null + tip_en: string | null + duration: number + video_url: string | null + created_at: string +} + // ─── Display Metadata ─────────────────────────────────────────── export const BODY_ZONE_META: Record = { 'upper-body': { label: 'Haut du corps', labelEn: 'Upper Body', icon: 'figure.strengthtraining.traditional', color: '#4A90D9', + descKey: 'screens:home.zoneDescUpper', }, 'lower-body': { label: 'Bas du corps', labelEn: 'Lower Body', icon: 'figure.run', color: '#9B59B6', + descKey: 'screens:home.zoneDescLower', }, 'full-body': { label: 'Corps entier', labelEn: 'Full Body', icon: 'figure.cooldown', color: '#00C896', + descKey: 'screens:home.zoneDescFull', }, }