i18n vocabulaire exos (fr/pt-BR) + Web Push rappels de série
Build & Deploy / build (push) Successful in 17s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 03:46:43 +02:00
co-authored by Claude Opus 4.8
parent af32669931
commit fa3e59704b
23 changed files with 954 additions and 31 deletions
@@ -30,6 +30,8 @@ class ExercisesController
->orderBy('name')
->paginate((int) $request->query('per_page', self::DEFAULT_PER_PAGE));
ExerciseListResource::$locale = $this->resolveLocale($request);
return ExerciseListResource::collection($exercises);
}
@@ -39,14 +41,24 @@ class ExercisesController
return new ExerciseResource($exercise, $this->resolveLocale($request));
}
/** GET /api/exercises/facets — valeurs distinctes pour alimenter les filtres du front. */
public function facets(): JsonResponse
/**
* GET /api/exercises/facets valeurs distinctes pour les filtres.
* Chaque valeur = { value: anglais (canonique, envoyé au filtre), label: traduit }.
*/
public function facets(Request $request): JsonResponse
{
$locale = $this->resolveLocale($request);
$facet = fn (string $column) => Exercise::query()
->whereNotNull($column)->distinct()->orderBy($column)->pluck($column)
->map(fn ($v) => ['value' => $v, 'label' => Exercise::localizeTerm($v, $locale)])
->values();
return response()->json([
'body_parts' => Exercise::query()->whereNotNull('body_part')->distinct()->orderBy('body_part')->pluck('body_part')->values(),
'equipments' => Exercise::query()->whereNotNull('equipment')->distinct()->orderBy('equipment')->pluck('equipment')->values(),
'targets' => Exercise::query()->whereNotNull('target')->distinct()->orderBy('target')->pluck('target')->values(),
'categories' => Exercise::query()->whereNotNull('category')->distinct()->orderBy('category')->pluck('category')->values(),
'body_parts' => $facet('body_part'),
'equipments' => $facet('equipment'),
'targets' => $facet('target'),
'categories' => $facet('category'),
]);
}
@@ -2,23 +2,29 @@
namespace Functional\Exercises\Http\Resources;
use Functional\Exercises\Models\Exercise;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* Forme allégée d'un exercice pour les listes (GET /api/exercises).
* Forme allégée d'un exercice pour les listes (GET /api/exercises), libellés localisés.
* La locale est posée par le contrôleur avant `collection()` (collection() n'accepte pas d'argument par item).
*/
class ExerciseListResource extends JsonResource
{
public static string $locale = 'fr';
public function toArray(Request $request): array
{
$l = self::$locale;
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'body_part' => $this->body_part,
'equipment' => $this->equipment,
'target' => $this->target,
'body_part' => Exercise::localizeTerm($this->body_part, $l),
'equipment' => Exercise::localizeTerm($this->equipment, $l),
'target' => Exercise::localizeTerm($this->target, $l),
'image_url' => $this->image_url,
'gif_url' => $this->gif_url,
];
@@ -17,16 +17,20 @@ class ExerciseResource extends JsonResource
public function toArray(Request $request): array
{
$l = $this->locale;
$model = $this->resource;
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'category' => $this->category,
'equipment' => $this->equipment,
'target' => $this->target,
'muscle_group' => $this->muscle_group,
'secondary_muscles' => $this->secondary_muscles ?? [],
'instructions' => $this->resource->instructionsFor($this->locale),
'body_part' => $model::localizeTerm($this->body_part, $l),
'category' => $model::localizeTerm($this->category, $l),
'equipment' => $model::localizeTerm($this->equipment, $l),
'target' => $model::localizeTerm($this->target, $l),
'muscle_group' => $model::localizeTerm($this->muscle_group, $l),
'secondary_muscles' => $model::localizeTerms($this->secondary_muscles, $l),
'instructions' => $model->instructionsFor($l),
'image_url' => $this->image_url,
'gif_url' => $this->gif_url,
'attribution' => $this->attribution,
@@ -82,6 +82,24 @@ class Exercise extends Model
return [];
}
/** Traduit un terme du vocabulaire contrôlé (zone/équipement/muscle) selon la locale. Repli = anglais. */
public static function localizeTerm(?string $value, string $locale): ?string
{
if ($value === null || $value === '') {
return $value;
}
$map = config('exercise_i18n.' . $locale, []);
return $map[mb_strtolower($value)] ?? $value;
}
/** @param array<int,string>|null $values @return array<int,string> */
public static function localizeTerms(?array $values, string $locale): array
{
return array_values(array_map(fn ($v) => self::localizeTerm($v, $locale), $values ?? []));
}
/** Recherche plein-texte simple (nom / muscle cible / équipement). */
public function scopeSearch(Builder $query, ?string $term): Builder
{
@@ -28,5 +28,6 @@ class ExercisesServiceProvider extends LayerServiceProvider
public function register(): void
{
$this->mergeConfigFrom(__DIR__ . '/../../config/exercises.php', 'exercises');
$this->mergeConfigFrom(__DIR__ . '/../../config/exercise_i18n.php', 'exercise_i18n');
}
}