Première exécution réelle de la stack (build des images, PostgreSQL 16, parcours fonctionnels en HTTP) : 28 tables, 105 index, extension pg_trgm, et 181 assertions rejouées après correction. Santé : filtres enum invalides renvoyaient 500 au lieu d'une erreur française ; plan_line s'arrêtait à la fin de la fenêtre du graphique au lieu de la date d'atteinte de l'objectif ; deficit_target_kcal était recalculé après le plancher calorique ; « dernière pesée » affichait deux valeurs différentes selon l'endpoint ; objectif protéines absent. Vape : durée de vie moyenne des résistances incluait la résistance en cours ; archiver la recette active la laissait active ; coût théorique inventé avant la date d'arrêt ; économies projetées dans le futur ; €/ml arrondi à 2 décimales écrasait le modèle de coût DIY. Finances : le sankey compensait crédits et débits non catégorisés ; rows_total excluait les lignes filtrées, faussant l'arithmétique du rapport d'import. Socle : les erreurs HTTP du framework fuitaient en anglais dans l'enveloppe française ; nginx renvoyait sa page 413 HTML au lieu du JSON français ; fins de ligne normalisées en LF. 348 tests pytest (+7), ruff, tsc et vite build au vert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
440 lines
14 KiB
TypeScript
440 lines
14 KiB
TypeScript
/**
|
||
* French UI strings of the health module (CONVENTIONS C5.3).
|
||
* Code stays English, every user-facing label lives here.
|
||
*/
|
||
|
||
export const S = {
|
||
module: {
|
||
title: "Santé",
|
||
},
|
||
|
||
nav: {
|
||
weight: "Poids & Objectif",
|
||
nutrition: "Nutrition",
|
||
activity: "Activité & Sport",
|
||
energy: "Balance énergétique",
|
||
},
|
||
|
||
actions: {
|
||
add: "Ajouter",
|
||
addWeight: "+ Ajouter une pesée",
|
||
addFood: "+ Ajouter un aliment",
|
||
addWorkout: "+ Ajouter une séance",
|
||
addMeasurement: "+ Ajouter des mensurations",
|
||
save: "Enregistrer",
|
||
cancel: "Annuler",
|
||
edit: "Modifier",
|
||
delete: "Supprimer",
|
||
close: "Fermer",
|
||
today: "Aujourd'hui",
|
||
replace: "Remplacer",
|
||
addAnyway: "Ajouter quand même",
|
||
addAndContinue: "Ajouter et continuer",
|
||
resetFilters: "Réinitialiser les filtres",
|
||
importCsv: "Importer un CSV",
|
||
importFoodvisor: "Importer Foodvisor",
|
||
seeImports: "Voir les imports",
|
||
goToWeight: "Aller à Poids",
|
||
goToNutrition: "Aller à Nutrition",
|
||
noteWeighIn: "Noter ma pesée",
|
||
noteWorkout: "Noter ma séance",
|
||
editGoal: "Modifier l'objectif",
|
||
useThisTdee: "Utiliser cette valeur comme TDEE",
|
||
},
|
||
|
||
common: {
|
||
loading: "Chargement…",
|
||
none: "—",
|
||
date: "Date",
|
||
time: "Heure",
|
||
note: "Note",
|
||
source: "Source",
|
||
total: "Total",
|
||
average: "Moyenne",
|
||
week: "Semaine",
|
||
day: "Jour",
|
||
kcal: "kcal",
|
||
actionsColumn: "Actions",
|
||
errorTitle: "Impossible de charger ces données",
|
||
retry: "Réessayer",
|
||
deleteConfirmTitle: "Supprimer cet élément ?",
|
||
deleteConfirmMessage: "Cette action est irréversible.",
|
||
noDataOnPeriod: "Pas de données sur cette période",
|
||
widenPeriod: "Élargir la période",
|
||
optional: "optionnel",
|
||
},
|
||
|
||
errors: {
|
||
weightRange: "Le poids doit être compris entre 20 et 300 kg.",
|
||
required: "Ce champ est obligatoire.",
|
||
invalidNumber: "Valeur numérique invalide.",
|
||
invalidDuration: "Durée invalide (format hh:mm).",
|
||
positive: "La valeur doit être positive.",
|
||
},
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* Poids & Objectif */
|
||
/* ---------------------------------------------------------------- */
|
||
weight: {
|
||
title: "Poids & Objectif",
|
||
kpi: {
|
||
current: "Poids actuel",
|
||
trend: "Tendance (EMA)",
|
||
weeklyRate: "Rythme hebdo",
|
||
bmi: "IMC",
|
||
goal: "Objectif",
|
||
projection: "Atteinte estimée",
|
||
adherence: "Assiduité 30 j",
|
||
},
|
||
charts: {
|
||
evolution: "Évolution du poids",
|
||
weeklyRate: "Rythme hebdomadaire",
|
||
bmi: "IMC",
|
||
measurements: "Mensurations",
|
||
adherence: "Calendrier des pesées",
|
||
},
|
||
series: {
|
||
raw: "Pesées",
|
||
trend: "Tendance",
|
||
projection: "Projection",
|
||
goal: "Objectif",
|
||
rate: "Rythme",
|
||
},
|
||
table: {
|
||
title: "Historique des pesées",
|
||
weight: "Poids",
|
||
trend: "Tendance",
|
||
variation: "Variation",
|
||
},
|
||
goalCard: {
|
||
title: "Objectif",
|
||
none: "Aucun objectif défini.",
|
||
progress: "Progression",
|
||
start: "Départ",
|
||
target: "Objectif",
|
||
remaining: "Reste",
|
||
},
|
||
modal: {
|
||
title: "Ajouter une pesée",
|
||
editTitle: "Modifier la pesée",
|
||
weightLabel: "Poids (kg)",
|
||
dateLabel: "Date",
|
||
timeLabel: "Heure",
|
||
noteLabel: "Note",
|
||
notePlaceholder: "ex. après le sport",
|
||
measurementsSection: "Mensurations (optionnel)",
|
||
saved: "Pesée enregistrée",
|
||
},
|
||
measurements: {
|
||
title: "Mensurations",
|
||
waist: "Tour de taille",
|
||
hips: "Hanches",
|
||
chest: "Poitrine",
|
||
neck: "Cou",
|
||
arm: "Bras",
|
||
thigh: "Cuisse",
|
||
calf: "Mollet",
|
||
bodyFat: "Masse grasse estimée",
|
||
empty: "Aucune mensuration enregistrée.",
|
||
},
|
||
empty: {
|
||
title: "Aucune pesée pour l'instant",
|
||
hint: "Ajoutez votre première pesée ou importez un historique — la tendance et les projections apparaîtront dès 3 pesées.",
|
||
},
|
||
bmiCategories: {
|
||
underweight: "Maigreur",
|
||
normal: "Corpulence normale",
|
||
overweight: "Surpoids",
|
||
obese: "Obésité",
|
||
},
|
||
notConverging: "rythme insuffisant",
|
||
},
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* Nutrition */
|
||
/* ---------------------------------------------------------------- */
|
||
nutrition: {
|
||
title: "Nutrition",
|
||
kpi: {
|
||
today: "Aujourd'hui",
|
||
avg7: "Moyenne 7 j",
|
||
protein: "Protéines aujourd'hui",
|
||
split: "Répartition du jour",
|
||
daysInBudget: "Jours dans le budget",
|
||
avgGap: "Écart moyen au budget",
|
||
},
|
||
charts: {
|
||
kcalPerDay: "Calories par jour",
|
||
macrosPerDay: "Macronutriments par jour",
|
||
todaySplit: "Répartition du jour",
|
||
perMeal: "Répartition par repas",
|
||
topFoods: "Top aliments",
|
||
},
|
||
series: {
|
||
intake: "Apports",
|
||
budget: "Budget",
|
||
protein: "Protéines",
|
||
carbs: "Glucides",
|
||
fat: "Lipides",
|
||
kcal: "Calories",
|
||
},
|
||
journal: {
|
||
title: "Journal du jour",
|
||
totals: "Total",
|
||
remaining: "Reste",
|
||
addToMeal: "+ Ajouter un aliment",
|
||
quantity: "Quantité",
|
||
duplicate: "Dupliquer",
|
||
filteredOn: "Filtré sur",
|
||
clearFilter: "Retirer le filtre",
|
||
},
|
||
meals: {
|
||
breakfast: "Petit-déjeuner",
|
||
lunch: "Déjeuner",
|
||
dinner: "Dîner",
|
||
snack: "Collations",
|
||
},
|
||
modal: {
|
||
title: "Ajouter un aliment",
|
||
editTitle: "Modifier l'aliment",
|
||
search: "Rechercher un aliment",
|
||
searchPlaceholder: "ex. pain complet",
|
||
favorites: "Favoris",
|
||
recents: "Récents",
|
||
catalog: "Catalogue",
|
||
noResult: "Aucun aliment trouvé — saisissez les valeurs manuellement.",
|
||
name: "Nom",
|
||
meal: "Repas",
|
||
quantity: "Quantité",
|
||
unit: "Unité",
|
||
kcal: "Calories (kcal)",
|
||
protein: "Protéines (g)",
|
||
carbs: "Glucides (g)",
|
||
fat: "Lipides (g)",
|
||
remember: "Mémoriser cet aliment dans ma bibliothèque",
|
||
saved: "Aliment ajouté",
|
||
},
|
||
empty: {
|
||
title: "Rien dans le journal aujourd'hui",
|
||
hint: "Ajoutez votre premier aliment ou importez votre historique Foodvisor depuis la page Imports.",
|
||
},
|
||
},
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* Activité & Sport */
|
||
/* ---------------------------------------------------------------- */
|
||
activity: {
|
||
title: "Activité & Sport",
|
||
kpi: {
|
||
stepsToday: "Pas aujourd'hui",
|
||
stepsAvg: "Moyenne pas (7 j)",
|
||
activeKcalToday: "Kcal actives aujourd'hui",
|
||
distance: "Distance (période)",
|
||
sessionsWeek: "Séances cette semaine",
|
||
goalReached: "Objectif atteint",
|
||
adherence: "Assiduité 30 j",
|
||
},
|
||
charts: {
|
||
steps: "Pas par jour",
|
||
activeKcal: "Calories actives par jour",
|
||
weekly: "Entraînement par semaine",
|
||
distance: "Distance cumulée",
|
||
adherence: "Calendrier des séances",
|
||
},
|
||
series: {
|
||
steps: "Pas",
|
||
movingAvg: "Moyenne 7 j",
|
||
activeKcal: "Calories actives",
|
||
distance: "Distance cumulée",
|
||
},
|
||
records: {
|
||
title: "Records",
|
||
maxSteps: "Max pas en un jour",
|
||
longestWorkout: "Plus longue séance",
|
||
bestDistance: "Meilleure distance en séance",
|
||
bestWeek: "Meilleure semaine",
|
||
},
|
||
table: {
|
||
title: "Séances",
|
||
type: "Type",
|
||
duration: "Durée",
|
||
distance: "Distance",
|
||
kcal: "Kcal",
|
||
avgHr: "FC moy.",
|
||
allTypes: "Tous les types",
|
||
allSources: "Toutes les sources",
|
||
},
|
||
modal: {
|
||
title: "Ajouter une séance",
|
||
editTitle: "Modifier la séance",
|
||
type: "Type",
|
||
date: "Date",
|
||
startTime: "Heure de début",
|
||
duration: "Durée (hh:mm)",
|
||
distance: "Distance (km)",
|
||
kcal: "Calories (kcal)",
|
||
kcalPlaceholder: "estimées automatiquement si vide",
|
||
avgHr: "FC moyenne (bpm)",
|
||
saved: "Séance enregistrée",
|
||
},
|
||
provenance: "Provenance des données du jour",
|
||
stepsGoal: "Objectif de pas",
|
||
empty: {
|
||
title: "Aucune activité enregistrée",
|
||
hint: "Connectez l'application compagnon Health Connect, importez un export FitShow, ou saisissez une séance manuellement.",
|
||
},
|
||
},
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* Balance énergétique */
|
||
/* ---------------------------------------------------------------- */
|
||
energy: {
|
||
title: "Balance énergétique",
|
||
kpi: {
|
||
today: "Balance aujourd'hui",
|
||
avgDeficit: "Déficit moyen (7 j)",
|
||
cumulative: "Cumul (période)",
|
||
tdee: "TDEE estimé",
|
||
budget: "Budget quotidien",
|
||
realVsModel: "Réel vs théorique",
|
||
},
|
||
charts: {
|
||
inOut: "Entrées vs sorties",
|
||
net: "Balance nette quotidienne",
|
||
cumulative: "Déficit cumulé",
|
||
weightModel: "Poids théorique vs poids réel",
|
||
},
|
||
series: {
|
||
intake: "Apports",
|
||
expenditure: "Dépense énergétique",
|
||
net: "Balance nette",
|
||
cumulative: "Déficit cumulé",
|
||
realWeight: "Poids réel (tendance)",
|
||
theoreticalWeight: "Poids théorique",
|
||
targetDeficit: "Cible",
|
||
},
|
||
/** Sous-titre du KPI « TDEE estimé » — comment la dépense a été obtenue (§5.2). */
|
||
tdeeMode: {
|
||
measured_total: "Dépense totale mesurée",
|
||
bmr_plus_active: (bmr: string) => `BMR ${bmr} + calories actives mesurées`,
|
||
estimated: (bmr: string, factor: string) => `BMR ${bmr} × ${factor}`,
|
||
},
|
||
method: {
|
||
title: "Méthode & calibration",
|
||
text: "Le métabolisme de base (BMR) est calculé avec la formule Mifflin-St Jeor à partir du poids de tendance du jour. La dépense totale (TDEE) applique ensuite votre facteur d'activité, ou ajoute vos calories actives mesurées. Les conversions énergie ↔ masse utilisent 7 700 kcal par kilogramme.",
|
||
workoutsNote:
|
||
"Les séances de sport ne sont pas ajoutées à la dépense du jour : seule l'activité quotidienne (pas, calories actives, dépense totale) alimente le TDEE. Une séance saisie sans donnée d'activité correspondante reste donc invisible dans cette balance.",
|
||
calibration: "Calibration",
|
||
insufficient: "Pas encore assez de jours suivis pour calibrer le modèle (21 jours minimum).",
|
||
},
|
||
incomplete: "Données incomplètes",
|
||
incompleteHint: (days: number) =>
|
||
`${days} jour${days > 1 ? "s" : ""} sans journal alimentaire sur la période. Les cumuls excluent ces jours.`,
|
||
noLog: "journal incomplet",
|
||
deficit: "déficit",
|
||
surplus: "surplus",
|
||
empty: {
|
||
title: "Il manque des données",
|
||
hint: "La balance énergétique a besoin de vos pesées et de votre journal alimentaire. Complétez ces deux modules pour débloquer cette page.",
|
||
},
|
||
},
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* Planning (addendum-planning.md) */
|
||
/* ---------------------------------------------------------------- */
|
||
planning: {
|
||
title: "Planning",
|
||
scheduleTitle: (habit: string) => `Planning — ${habit}`,
|
||
scheduleHint: "Choisis les jours où tu prévois cette habitude.",
|
||
enabled: "Planning activé",
|
||
weekdays: ["L", "M", "M", "J", "V", "S", "D"],
|
||
weekdaysAbbr: ["lun.", "mar.", "mer.", "jeu.", "ven.", "sam.", "dim."],
|
||
weekdaysFull: ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"],
|
||
saved: "Planning enregistré",
|
||
emptyPlan: "Aucun jour planifié. Choisis tes jours de pesée pour suivre ton assiduité.",
|
||
emptyPlanWorkout: "Aucun jour planifié. Choisis tes jours de séance pour suivre ton assiduité.",
|
||
nothingToday: "Rien de prévu aujourd'hui. Profites-en bien !",
|
||
todayCard: {
|
||
weighIn: "Pesée du jour",
|
||
workout: "Séance du jour",
|
||
},
|
||
status: {
|
||
done: "Faite",
|
||
planned: "Planifiée",
|
||
missed: "Manquée",
|
||
rest: "Repos",
|
||
notPlanned: "Non planifiée",
|
||
},
|
||
streak: (days: number) => `${days} jour${days > 1 ? "s" : ""} d'affilée`,
|
||
bestStreak: "Record",
|
||
adherence: "Assiduité",
|
||
streakLabel: "Série en cours",
|
||
legend: {
|
||
done: "Faite",
|
||
missed: "Planifiée manquée",
|
||
planned: "Planifiée",
|
||
off: "Non planifiée",
|
||
},
|
||
habits: {
|
||
weigh_in: "Pesée",
|
||
workout: "Séance de sport",
|
||
food_log: "Journal alimentaire",
|
||
},
|
||
},
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* Enum labels */
|
||
/* ---------------------------------------------------------------- */
|
||
sportTypes: {
|
||
treadmill_walk: "Marche sur tapis",
|
||
treadmill_run: "Course sur tapis",
|
||
walking: "Marche",
|
||
running: "Course à pied",
|
||
cycling: "Vélo",
|
||
swimming: "Natation",
|
||
strength: "Renforcement",
|
||
hiit: "Fractionné (HIIT)",
|
||
yoga: "Yoga",
|
||
hiking: "Randonnée",
|
||
other: "Autre",
|
||
},
|
||
|
||
sources: {
|
||
manual: "Manuel",
|
||
health_connect: "Health Connect",
|
||
fitshow: "FitShow",
|
||
foodvisor: "Foodvisor",
|
||
csv_import: "Import CSV",
|
||
api: "API",
|
||
},
|
||
|
||
units: {
|
||
g: "g",
|
||
ml: "ml",
|
||
portion: "portion",
|
||
piece: "pièce",
|
||
},
|
||
} as const;
|
||
|
||
export type MealKey = keyof typeof S.nutrition.meals;
|
||
|
||
const UNIT_LABELS: Record<string, string> = { ...S.units };
|
||
const SOURCE_LABELS: Record<string, string> = { ...S.sources };
|
||
const SPORT_LABELS: Record<string, string> = { ...S.sportTypes };
|
||
|
||
/** French label of a quantity unit (falls back to the raw value). */
|
||
export function unitLabel(unit: string): string {
|
||
return UNIT_LABELS[unit] ?? unit;
|
||
}
|
||
|
||
/** French label of a data source badge. */
|
||
export function sourceLabel(source: string): string {
|
||
return SOURCE_LABELS[source] ?? source;
|
||
}
|
||
|
||
/** French label of a sport type. */
|
||
export function sportLabel(sportType: string, custom?: string | null): string {
|
||
if (sportType === "other" && custom) return custom;
|
||
return SPORT_LABELS[sportType] ?? sportType;
|
||
}
|