Files
lifetrack/apps/web/src/modules/health/strings.ts
T
MeeJayandClaude Opus 5 fa3db7ff22 Fix 20+ defects found by real Docker/PostgreSQL deployment
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>
2026-08-14 11:40:05 +02:00

440 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}