feat: i18n infrastructure with 4-locale support (en, fr, es, de)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Millian Lamiaux
2026-02-21 00:04:40 +01:00
parent 6a94d545f2
commit d6bc7f5a4c
23 changed files with 1853 additions and 2 deletions

View File

@@ -0,0 +1,123 @@
/**
* TabataFit Translated Data Hooks
* 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<T extends { id: string; title: string }>(
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<string, string> = {
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 {
const { t } = useTranslation('common')
const vibeKeyMap: Record<string, string> = {
electronic: 'musicVibes.electronic',
'hip-hop': 'musicVibes.hipHop',
pop: 'musicVibes.pop',
rock: 'musicVibes.rock',
chill: 'musicVibes.chill',
}
return t(vibeKeyMap[vibe] ?? vibe, { defaultValue: vibe })
}