feat: cooking history/gallery, unit conversion, nutrition diary, pantry scan, digest cron, nutrition-targeted meal plans
Six M-sized items from HANDOFF.md's new-features backlog: - Profile tabs: cooking-history stats (total cooked, last-cooked, streak) and a "cooked it" photo gallery, both owner-only - Display-time unit conversion (metric<->imperial) for recipe ingredients, respecting each user's unitPref; original value always shown alongside the conversion - Nutrition daily diary: per-day macro totals computed from cooking history x recipe nutritionData, compared against user goals - Pantry scan: real barcode lookup (zxing + Open Food Facts, no API key) with an AI-vision fallback for unbarcoded items, always confirm-before- insert, both paths tier/rate-limited like other AI features - Weekly digest email: new followers/comments/ratings + trending recipes, sent via a new `cron` Docker stage (alpine+crond+curl) and `digest-cron` compose service hitting a bearer-token-protected internal route - Meal-plan generation can now target a user's nutrition goals as a prompt-level nudge (recipes are AI-invented, not DB-sourced, so this can't be a hard macro constraint) Caught a real deploy-breaking issue while adding the cron stage: appending it after `runner` silently changed the Dockerfile's default build target, and `web`'s compose config didn't pin one — fixed by pinning `target: runner` explicitly. Verified with typecheck, lint, and three separate `docker build --target` runs (runner/cron/migrator) plus `docker compose config` validation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"feed": "Fil d'actualité",
|
||||
"collections": "Collections",
|
||||
"mealPlan": "Planning repas",
|
||||
"nutrition": "Nutrition",
|
||||
"pantry": "Garde-manger",
|
||||
"shopping": "Courses",
|
||||
"settings": "Paramètres",
|
||||
@@ -303,7 +304,8 @@
|
||||
},
|
||||
"nutritionGoals": {
|
||||
"title": "Objectifs nutritionnels quotidiens",
|
||||
"description": "Définissez vos objectifs quotidiens. Ils apparaissent sous forme de barres de progression dans votre planning repas."
|
||||
"description": "Définissez vos objectifs quotidiens. Ils apparaissent sous forme de barres de progression dans votre planning repas.",
|
||||
"viewDiaryCta": "Voir le journal nutritionnel"
|
||||
},
|
||||
"webhooksPage": {
|
||||
"title": "Webhooks",
|
||||
@@ -398,6 +400,27 @@
|
||||
"fiber": "Fibres",
|
||||
"sodium": "Sodium"
|
||||
},
|
||||
"nutritionDiary": {
|
||||
"title": "Journal nutritionnel",
|
||||
"subtitle": "Votre apport nutritionnel quotidien, calculé à partir de ce que vous avez cuisiné.",
|
||||
"dateLabel": "Date",
|
||||
"today": "Aujourd'hui",
|
||||
"loading": "Chargement…",
|
||||
"loadError": "Impossible de charger votre journal nutritionnel. Veuillez réessayer.",
|
||||
"goalNotSet": "Vous n'avez pas encore défini d'objectifs nutritionnels quotidiens.",
|
||||
"setGoalsCta": "Définir les objectifs",
|
||||
"calories": "Calories",
|
||||
"protein": "Protéines",
|
||||
"carbs": "Glucides",
|
||||
"fat": "Lipides",
|
||||
"fiber": "Fibres",
|
||||
"sodium": "Sodium",
|
||||
"entriesTitle": "Recettes cuisinées",
|
||||
"noEntries": "Rien n'est enregistré pour ce jour. Marquez une recette comme « cuisinée » pour la voir ici.",
|
||||
"servingsLabel": "{count, plural, one {1 portion} other {{count} portions}}",
|
||||
"nutritionUnknownBadge": "Nutrition inconnue",
|
||||
"unknownNote": "{count, plural, one {1 entrée n'a pas de données nutritionnelles et n'est pas incluse dans les totaux ci-dessus.} other {{count} entrées n'ont pas de données nutritionnelles et ne sont pas incluses dans les totaux ci-dessus.}}"
|
||||
},
|
||||
"explore": {
|
||||
"title": "Explorer",
|
||||
"searchPlaceholder": "Rechercher des recettes publiques…",
|
||||
@@ -430,12 +453,11 @@
|
||||
"copyMarkdown": "Copier en Markdown",
|
||||
"downloadMarkdown": "Télécharger en Markdown",
|
||||
"copiedToClipboard": "Copié dans le presse-papiers",
|
||||
"copyFailed": "Échec de la copie",
|
||||
"copyFailed": "Échec de la copie dans le presse-papiers",
|
||||
"print": "Imprimer",
|
||||
"share": "Partager",
|
||||
"deleteFailed": "Échec de la suppression",
|
||||
"updateFailed": "Échec de la mise à jour",
|
||||
"copyFailed": "Échec de la copie dans le presse-papiers",
|
||||
"deleted": "Supprimé",
|
||||
"cancel": "Annuler",
|
||||
"delete": "Supprimer",
|
||||
@@ -634,6 +656,8 @@
|
||||
"includePantryItems": "Inclure les articles du garde-manger",
|
||||
"pantryMode": "Mode garde-manger — maximiser ce que vous avez",
|
||||
"pantryModeDescription": "Préférer les repas utilisant plusieurs ingrédients du garde-manger et limiter les achats supplémentaires.",
|
||||
"targetNutritionGoals": "Viser mes objectifs nutritionnels",
|
||||
"targetNutritionGoalsDescription": "Inciter l'IA à viser vos objectifs quotidiens de calories et de macronutriments sur l'ensemble des repas de la journée.",
|
||||
"removeFailed": "Échec de la suppression",
|
||||
"generationFailed": "Échec de la génération",
|
||||
"anyDifficulty": "Toute",
|
||||
@@ -679,7 +703,26 @@
|
||||
"colItem": "Article",
|
||||
"colQuantity": "Quantité",
|
||||
"colExpires": "Expire",
|
||||
"expiredDaysAgo": "Expiré il y a {days}j"
|
||||
"expiredDaysAgo": "Expiré il y a {days}j",
|
||||
"scan": "Scanner",
|
||||
"scanTitle": "Ajouter par scan",
|
||||
"scanDescription": "Scannez un code-barres ou prenez une photo pour ajouter rapidement des articles.",
|
||||
"scanBarcodeOption": "Scanner un code-barres",
|
||||
"scanPhotoOption": "Prendre/importer une photo",
|
||||
"scanBarcodeTitle": "Scanner un code-barres",
|
||||
"scanBarcodeDescription": "Pointez votre caméra vers un code-barres.",
|
||||
"scanConfirmTitle": "Confirmer l'article",
|
||||
"scanConfirmDescription": "Vérifiez et modifiez avant d'ajouter au garde-manger.",
|
||||
"scanAnalyzingTitle": "Analyse de la photo",
|
||||
"scanAnalyzingDescription": "Identification des articles dans votre photo…",
|
||||
"scanBack": "Retour",
|
||||
"scanAddToPantry": "Ajouter au garde-manger",
|
||||
"scanAdded": "Ajouté au garde-manger",
|
||||
"scanLookupFailed": "Échec de la recherche du code-barres",
|
||||
"scanNotFound": "Produit introuvable. Essayez un scan photo.",
|
||||
"scanPhotoFailed": "Échec du scan photo",
|
||||
"scanNoItemsFound": "Aucun article reconnu sur cette photo",
|
||||
"cameraError": "Impossible d'accéder à la caméra"
|
||||
},
|
||||
"feed": {
|
||||
"title": "Fil d'actualité",
|
||||
@@ -972,5 +1015,21 @@
|
||||
"privateBio": "Contexte IA (privé)",
|
||||
"privateBioDescription": "Jamais visible publiquement. Injecté dans les prompts IA pour personnaliser les suggestions — ajoutez vos préférences alimentaires, équipements, niveau de cuisine, allergies, etc.",
|
||||
"privateBioPlaceholder": "ex. Je suis végétarien, j'ai un robot pâtissier et une friteuse à air, niveau intermédiaire, allergique aux fruits à coque, je préfère les saveurs méditerranéennes…"
|
||||
},
|
||||
"profilePage": {
|
||||
"tabRecipes": "Recettes",
|
||||
"tabHistory": "Historique de cuisine",
|
||||
"tabPhotos": "Photos",
|
||||
"statTotalCooked": "Total de fois cuisinées",
|
||||
"statStreak": "Série en cours",
|
||||
"statLastCooked": "Dernière cuisson",
|
||||
"streakDays": "{count, plural, one {# jour} other {# jours}}",
|
||||
"noneYet": "Aucune pour le moment",
|
||||
"recentActivity": "Activité récente",
|
||||
"noHistoryYet": "Aucun historique de cuisine pour le moment.",
|
||||
"noPhotosYet": "Aucune photo pour le moment.",
|
||||
"previous": "Précédent",
|
||||
"next": "Suivant",
|
||||
"pageOf": "Page {page} sur {total}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user