fix: mobile layout fixes, i18n coverage, and recipe share link

Mobile:
- Recipes search bar full-width on mobile instead of capped narrow
- Cook mode ingredients panel stacks above the step instead of
  squeezing it into a narrow column
- Version history Compare/Restore buttons wrap onto their own row
- Recipe edit ingredient fields wrap instead of forcing horizontal
  scroll on narrow viewports

i18n: translates remaining hardcoded strings across recipes
filter/sort, adapt-recipe and AI variations dialogs, the full
settings section (sidebar + 6 sub-pages + BYOK/model-prefs/
API-keys/webhooks managers), explore tab, collections (new/fork/
share dialogs), meal planning (planner, AI generation phases, new
shopping list, shared-plan view), photo import, recipe bulk-select
toolbar, and recipe action-button tooltips. Also fixes the recipes
page subtitle, which wasn't just unworded but missing its {count}
interpolation entirely — it always rendered as the bare word
"results" regardless of how many recipes existed.

Feature: adds a ShareRecipeButton that copies the public /r/{id}
link to the clipboard, with a notice when the recipe isn't Public
yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-02 15:13:51 +02:00
parent b07bada291
commit eb424d8c04
44 changed files with 932 additions and 376 deletions
+225 -9
View File
@@ -16,6 +16,11 @@
"language": "Langue"
},
"recipe": {
"share": "Partager",
"shareLinkCopied": "Lien copié dans le presse-papiers",
"shareCopyFailed": "Échec de la copie du lien",
"shareNotPublicNotice": "Ce lien ne fonctionnera qu'une fois la recette définie sur Public — lien copié quand même.",
"print": "Imprimer",
"ingredients": "Ingrédients",
"instructions": "Instructions",
"photos": "Photos",
@@ -37,6 +42,17 @@
"adapted": "Recette adaptée enregistrée comme brouillon",
"adaptFailed": "Échec de l'adaptation de la recette",
"adaptConstraintRequired": "Sélectionnez au moins un ingrédient à exclure ou ajoutez une contrainte",
"adaptTooltip": "Adapter",
"adaptTitle": "Adapter cette recette",
"adaptDescription": "Touchez les ingrédients pour les exclure. L'IA trouvera les meilleurs substituts tout en préservant le plat.",
"excludeIngredients": "Exclure des ingrédients",
"excludedCount": "{count} exclus",
"additionalConstraints": "Contraintes supplémentaires",
"optional": "facultatif",
"adapting": "Adaptation de la recette — cela peut prendre 20 à 30 secondes…",
"adaptingButton": "Adaptation…",
"adaptButton": "Adapter la recette",
"clearExclusions": "Effacer",
"adaptConstraintPlaceholder": "ex. Rendre végétalien, réduire les calories, utiliser seulement les produits du garde-manger, sans gluten…",
"variationsDirectionsPlaceholder": "ex. rendre végétalien, utiliser uniquement les produits du garde-manger, réduire le sucre…",
"historyRestoreFailed": "Échec de la restauration de la version",
@@ -48,12 +64,30 @@
"pairingBulkSuccess": "{count} recettes générées — retrouvez-les dans votre bibliothèque",
"cookAction": "Cuisiner",
"photoImportFailed": "Échec de l'importation de la recette depuis la photo.",
"importFromPhoto": "Importer depuis une photo",
"analyzingPhoto": "Analyse de la photo…",
"pairMealTooltip": "Accorder un plat",
"historyTooltip": "Historique",
"translateTooltip": "Traduire",
"drinksTooltip": "Boissons",
"variationsTooltip": "Variations",
"viewPublicly": "Voir publiquement",
"shoppingListCreateFailed": "Échec de la création de la liste",
"shoppingListAddFailed": "Échec de l'ajout des ingrédients",
"bulkDeleted": "{count, plural, one {1 recette supprimée} other {{count} recettes supprimées}}",
"bulkVisibility": "{count, plural, one {1 recette définie sur {visibility}} other {{count} recettes définies sur {visibility}}}",
"bulkDeleteFailed": "Échec de la suppression",
"bulkUpdateFailed": "Échec de la mise à jour",
"bulkDeleteConfirm": "{count, plural, one {Supprimer 1 recette ?} other {Supprimer {count} recettes ?}} Cette action est irréversible.",
"deselectAll": "Tout désélectionner",
"selectAll": "Tout sélectionner",
"clickToSelect": "Cliquez sur les cartes pour sélectionner",
"selectedCount": "{count} sélectionnée(s)",
"select": "Sélectionner",
"visibility": "Visibilité",
"makePublic": "Rendre publique",
"makeUnlisted": "Rendre non répertoriée",
"makePrivate": "Rendre privée",
"visibility": {
"private": "Privée",
"unlisted": "Non répertoriée",
@@ -103,7 +137,8 @@
"contentGenerated": "Ingrédients et étapes générés !",
"contentGenerateFailed": "Échec de la génération",
"contentSaveFailed": "Échec de l'enregistrement du contenu généré",
"generatingContent": "Génération du contenu de la recette…"
"generatingContent": "Génération du contenu de la recette…",
"photoAnalysisNote": "L'IA analysera ce plat et déduira les ingrédients, quantités et étapes de préparation."
},
"variations": {
"title": "Variations de recette par IA",
@@ -118,7 +153,10 @@
"success": "Variation créée — vérifiez et modifiez avant de publier.",
"error": "Échec de la génération des variations",
"saveError": "Échec de l'enregistrement de la variation",
"wait": "Cela peut prendre 20 à 30 secondes"
"wait": "Cela peut prendre 20 à 30 secondes",
"directions": "Instructions",
"optional": "facultatif",
"stepLabel": "Étape {n}"
},
"translate": {
"title": "Traduire la recette",
@@ -138,7 +176,37 @@
"language": "Langue",
"units": "Unités",
"metric": "Métrique",
"imperial": "Impérial"
"imperial": "Impérial",
"security": "Sécurité",
"aiModels": "IA et modèles",
"notifications": "Notifications",
"nutrition": "Nutrition",
"apiKeys": "Clés API",
"webhooks": "Webhooks",
"byok": {
"title": "Vos clés API (BYOK)",
"description": "Utilisez vos propres clés API au lieu du quota partagé de l'application. Les clés sont chiffrées au repos."
},
"modelPrefs": {
"title": "Préférences de modèle",
"description": "Choisissez le modèle à utiliser pour chaque tâche. Par défaut, votre premier fournisseur configuré."
},
"apiKeysPage": {
"title": "Clés API",
"description": "Gérez les clés API pour un accès programmatique à l'API Epicure."
},
"pushNotifications": {
"title": "Notifications push",
"description": "Soyez averti lorsqu'on commente vos recettes ou aime votre contenu."
},
"nutritionGoals": {
"title": "Objectifs nutritionnels quotidiens",
"description": "Définissez vos objectifs quotidiens. Ils apparaissent sous forme de barres de progression dans votre planning repas."
},
"webhooksPage": {
"title": "Webhooks",
"description": "Recevez des appels HTTP lorsque des événements se produisent sur votre compte Epicure."
}
},
"print": {
"footer": "Imprimé depuis Epicure",
@@ -190,9 +258,26 @@
"aiScaledNote": "Quantités ajustées par l'IA affichées ci-dessous"
},
"explore": {
"title": "Explorer",
"searchPlaceholder": "Rechercher des recettes publiques…",
"maxMinutes": "Minutes max",
"aiSearchPlaceholder": "ex. dîners rapides en semaine, cuisine réconfortante italienne…"
"aiSearchPlaceholder": "ex. dîners rapides en semaine, cuisine réconfortante italienne…",
"anyDifficulty": "Toute difficulté",
"resultsFoundSingular": "{count} recette trouvée",
"resultsFoundPlural": "{count} recettes trouvées",
"noResultsTitle": "Aucune recette trouvée pour « {query} »",
"noResultsHint": "Essayez d'autres mots-clés ou retirez des filtres",
"loadMore": "Charger plus",
"recipeIdeas": "Idées de recettes",
"recipeIdeasHint": "Dites à l'IA ce dont vous avez envie, ou laissez-la vous surprendre.",
"generating": "Génération…",
"getIdeas": "Obtenir des idées",
"surpriseMe": "Surprenez-moi",
"generateFullRecipe": "Générer la recette complète",
"trendingThisWeek": "Tendances cette semaine",
"noTrending": "Aucune recette tendance pour l'instant. Soyez le premier à en partager une !",
"recentlyAdded": "Récemment ajoutées",
"noRecent": "Aucune recette publique pour l'instant."
},
"common": {
"loading": "Chargement…",
@@ -200,6 +285,8 @@
"save": "Enregistrer",
"saved": "Enregistré",
"saveFailed": "Échec de l'enregistrement",
"print": "Imprimer",
"share": "Partager",
"deleteFailed": "Échec de la suppression",
"updateFailed": "Échec de la mise à jour",
"copyFailed": "Échec de la copie dans le presse-papiers",
@@ -267,10 +354,25 @@
"noMatch": "Aucune recette ne correspond",
"clearSearch": "Effacer la recherche",
"createFirst": "Créer votre première recette",
"resultSingular": "résultat",
"resultPlural": "résultats",
"resultSingular": "{count} recette",
"resultPlural": "{count} recettes",
"for": "pour",
"filterByTag": "Filtrer par tag…"
"filterByTag": "Filtrer par tag…",
"sortRecentlyUpdated": "Mise à jour récemment",
"sortOldestUpdated": "Mise à jour la plus ancienne",
"sortNewestFirst": "Plus récentes",
"sortOldestFirst": "Plus anciennes",
"sortTitleAZ": "Titre AZ",
"sortTitleZA": "Titre ZA",
"sort": "Trier",
"sortBy": "Trier par",
"filter": "Filtrer",
"all": "Toutes",
"anyDifficulty": "Toute difficulté",
"visibilityLabel": "Visibilité",
"difficultyLabel": "Difficulté",
"tagLabel": "Tag",
"clearFilters": "Effacer les filtres"
},
"recipeForm": {
"titleLabel": "Titre *",
@@ -321,6 +423,13 @@
"mealPlan": {
"title": "Planning repas",
"shoppingLists": "Listes de courses",
"print": "Imprimer",
"shareTitle": "Partager le planning de cette semaine",
"shareDescription": "Invitez des membres du foyer à voir ou modifier le planning repas de cette semaine.",
"sharedWithYou": "Partagé avec vous",
"sharedPlanOf": "Planning de {name}",
"weekOf": "Semaine du {date}",
"sharedMealPlanTitle": "Planning repas de {name}",
"addTo": "Ajouter à {day} {meal}",
"searchRecipes": "Rechercher des recettes…",
"servings": "Portions",
@@ -361,7 +470,26 @@
"aiGenerated": "Plan de repas généré !",
"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."
"pantryModeDescription": "Préférer les repas utilisant plusieurs ingrédients du garde-manger et limiter les achats supplémentaires.",
"removeFailed": "Échec de la suppression",
"generationFailed": "Échec de la génération",
"anyDifficulty": "Toute",
"aiPhaseAnalyzing": "Analyse de vos préférences…",
"aiPhasePlanning": "Planification du petit-déjeuner, déjeuner et dîner…",
"aiPhaseSelecting": "Sélection des recettes pour chaque jour…",
"aiPhaseBalancing": "Équilibrage nutritionnel de la semaine…",
"aiPhaseFinalizing": "Finalisation de votre planning repas…",
"newList": "Nouvelle liste",
"newListTitle": "Nouvelle liste de courses",
"listNameLabel": "Nom",
"listNamePlaceholder": "Courses de la semaine",
"generateFromWeek": "Générer depuis une semaine du planning (facultatif)",
"generateFromWeekHint": "Sélectionne le lundi de la semaine choisie",
"listCreateFailed": "Échec de la création",
"listCreating": "Création…",
"listCreate": "Créer",
"pickRecipe": "Choisir une recette",
"addEntry": "+ Ajouter"
},
"pantry": {
"title": "Garde-manger",
@@ -385,9 +513,20 @@
"empty": "Aucune liste de courses",
"listEmpty": "Vide",
"generated": "Générée",
"shareTitle": "Partager la liste de courses",
"shareDescription": "Invitez des membres du foyer à voir ou modifier cette liste.",
"sharedWithYou": "Partagées avec vous",
"sendToGroceryDelivery": "Envoyer à un service de livraison",
"copyAsText": "Copier la liste en texte",
"sendToInstacart": "Envoyer à Instacart",
"copiedToClipboard": "Liste copiée dans le presse-papiers",
"exportBuildFailed": "Impossible de générer l'export",
"instacartNotConfigured": "Instacart n'est pas encore configuré",
"items": "{checked}/{total} articles",
"itemsChecked": "{checked}/{total} articles cochés",
"fromMealPlan": " · Depuis le planning repas",
"itemCount": "{count} article",
"itemCountPlural": "{count} articles",
"addToList": "Ajouter à la liste",
"dialogTitle": "Ajouter à la liste de courses",
"dialogDescription": "Ajouter les ingrédients de <strong>{title}</strong> à une liste de courses.",
@@ -407,7 +546,40 @@
"empty": "Aucune collection pour l'instant",
"public": "Publique",
"recipeCount": "{count} recette",
"recipeCountPlural": "{count} recettes"
"recipeCountPlural": "{count} recettes",
"exportPdf": "Exporter en PDF",
"shareTitle": "Partager la collection",
"shareDescription": "Invitez des personnes à voir ou modifier cette collection.",
"new": "Nouvelle collection",
"newTitle": "Nouvelle collection",
"nameLabel": "Nom",
"namePlaceholder": "Dîners du week-end",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Facultatif…",
"makePublic": "Rendre publique",
"creating": "Création…",
"create": "Créer",
"createFailed": "Échec de la création",
"forking": "Duplication…",
"forkCollection": "Dupliquer la collection",
"forkFailed": "Échec de la duplication"
},
"shareDialog": {
"invite": "Inviter",
"emailPlaceholder": "Adresse e-mail",
"owner": "Propriétaire",
"viewer": "Lecteur",
"editor": "Éditeur",
"loadingMembers": "Chargement des membres…",
"noMembers": "Aucun membre pour l'instant.",
"invitationSent": "Invitation envoyée",
"alreadyMember": "Déjà membre",
"userNotFound": "Utilisateur introuvable",
"inviteFailed": "Impossible d'inviter cet utilisateur",
"loadMembersFailed": "Impossible de charger les membres",
"memberRemoved": "Membre retiré",
"removeMemberFailed": "Impossible de retirer le membre",
"enterEmail": "Saisissez une adresse e-mail"
},
"social": {
"followed": "Abonné",
@@ -499,18 +671,62 @@
"webhookUrlPlaceholder": "https://exemple.fr/webhook",
"webhookAdding": "Ajout…",
"webhookAddButton": "Ajouter un webhook",
"webhookDocsLink": "Voir la doc webhook et l'intégration Zapier →",
"endpointUrlLabel": "URL de destination",
"eventsLabel": "Événements",
"eventsHint": "Sélectionnez les événements qui déclenchent ce webhook. Laissez tout décoché pour recevoir tous les événements.",
"secretRevealNotice": "Enregistrez ce secret de signature — il ne sera plus affiché.",
"secretUsageHint": "Utilisez-le pour vérifier l'en-tête X-Epicure-Signature des requêtes entrantes.",
"noWebhooks": "Aucun webhook pour l'instant.",
"addedOn": "Ajouté le {date}",
"active": "Actif",
"inactive": "Inactif",
"allEvents": "Tous les événements",
"deliveries": "Livraisons",
"disable": "Désactiver",
"enable": "Activer",
"noDeliveriesYet": "Aucune livraison pour l'instant.",
"redeliverySuccess": "Renvoi mis en file d'attente",
"redeliveryFailed": "Échec du renvoi",
"apiKeyCreateFailed": "Échec de la création de la clé API",
"apiKeyRevokeFailed": "Échec de la révocation de la clé API",
"apiKeyNamePlaceholder": "ex. Mon application",
"apiKeyNameLabel": "Nom de la clé",
"apiKeyCreating": "Création…",
"apiKeyCreate": "Créer",
"apiKeyRevealNotice": "Enregistrez cette clé — elle ne sera plus affichée.",
"copy": "Copier",
"copied": "Copié !",
"dismiss": "Ignorer",
"noApiKeys": "Aucune clé API pour l'instant.",
"createdOn": "Créée le {date}",
"lastUsedOn": "Utilisée le {date}",
"neverUsed": "Jamais utilisée",
"revoke": "Révoquer",
"copyFailed": "Échec de la copie dans le presse-papiers",
"byokSaved": "Clé {provider} enregistrée",
"byokSaveFailed": "Échec de l'enregistrement de la clé",
"byokRemoved": "Clé {provider} supprimée",
"byokRemoveFailed": "Échec de la suppression de la clé",
"byokConfigured": "Configurée",
"byokOllamaHint": "Définie via la variable d'environnement OLLAMA_BASE_URL",
"byokSaving": "Enregistrement…",
"byokSave": "Enregistrer",
"byokFooter": "Vos clés sont chiffrées au repos. Une fois définies, elles remplacent les clés par défaut de l'application pour votre compte.",
"modelSaved": "Préférences de modèle enregistrées",
"modelSaveFailed": "Échec de l'enregistrement",
"useCaseText": "Génération de texte",
"useCaseTextDesc": "Génération de recettes, variations, traductions, adaptation",
"useCaseVision": "Vision / import photo",
"useCaseVisionDesc": "Reconnaissance de plats, import de recette depuis une photo",
"useCaseMealPlan": "Planning repas",
"useCaseMealPlanDesc": "Génération du planning repas hebdomadaire",
"providerLabel": "Fournisseur",
"modelLabel": "Modèle",
"defaultAuto": "Par défaut (auto)",
"modelDefault": "Par défaut",
"modelSaving": "Enregistrement…",
"saveModelPrefs": "Enregistrer les préférences de modèle",
"modelDefaultPlaceholder": "Par défaut",
"modelNamePlaceholder": "ex. llama3.2",
"userUpdated": "Utilisateur mis à jour avec succès",