feat(i18n): translate hardcoded strings across print, public recipe, and recipe management UI

Wires dozens of components and server pages to next-intl instead of hardcoded
English text, and adds a lightweight getMessages/formatMessage helper for
server components (print pages, public recipe page, cook metadata) since
next-intl/server isn't wired into this app's routing. Backfills missing
en/fr message keys that existing code already referenced but fr.json lacked.
This commit is contained in:
Arnaud
2026-07-02 07:58:18 +02:00
parent afff6cf9eb
commit 01fdbb880b
32 changed files with 515 additions and 187 deletions
+179 -5
View File
@@ -28,6 +28,32 @@
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer",
"noIngredientsSteps": "Pas encore d'ingrédients ni d'étapes.",
"generateContent": "Générer avec l'IA",
"editManually": "Modifier manuellement",
"deleted": "Recette supprimée",
"deleteFailed": "Échec de la suppression",
"imported": "Recette importée ! Vérifiez avant de publier.",
"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",
"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",
"pairingMealFailed": "Échec de la suggestion d'accompagnements",
"pairingDrinkFailed": "Échec de la suggestion de boissons",
"pairingGenerateFailed": "Échec de la génération de « {name} »",
"pairingSaveFailed": "Échec de l'enregistrement de « {name} »",
"pairingSuccess": "Recette générée — vérifiez avant de publier",
"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.",
"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",
"visibility": {
"private": "Privée",
"unlisted": "Non répertoriée",
@@ -52,14 +78,32 @@
"generate": {
"title": "Générer une recette avec l'IA",
"description": "Décrivez ce que vous voulez cuisiner et l'IA créera une recette complète.",
"descriptionFull": "Décrivez un plat ou prenez une photo — l'IA construit la recette complète.",
"tabDescribe": "Décrire",
"tabPhoto": "Depuis une photo",
"surpriseMe": "Surprends-moi",
"prompt": "Que voulez-vous cuisiner ?",
"placeholder": "ex. Un risotto crémeux aux champignons et parmesan, 4 portions, niveau facile",
"language": "Langue",
"language": "Langue de la recette",
"difficulty": "Difficulté",
"anyDifficulty": "Toutes",
"button": "Générer",
"generating": "Génération en cours…",
"success": "Recette générée ! Vérifiez et modifiez avant de publier.",
"error": "Échec de la génération de la recette",
"saveError": "Échec de l'enregistrement de la recette générée"
"saveError": "Échec de l'enregistrement de la recette générée",
"uploadClick": "Cliquez pour importer",
"uploadDrag": "ou glissez une photo du plat",
"uploadFormats": "JPEG, PNG ou WebP · l'IA reconstituera la recette",
"analyzing": "Analyse en cours…",
"analyzePhoto": "Analyse de la photo du plat…",
"recognizeDish": "Reconnaître le plat",
"photoSuccess": "Recette reconnue ! Vérifiez et modifiez avant de publier.",
"photoError": "Échec de l'analyse de la photo",
"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…"
},
"variations": {
"title": "Variations de recette par IA",
@@ -96,16 +140,79 @@
"metric": "Métrique",
"imperial": "Impérial"
},
"print": {
"footer": "Imprimé depuis Epicure",
"day": "Jour",
"srv": " · {count} pers.",
"days": {
"mon": "Lundi",
"tue": "Mardi",
"wed": "Mercredi",
"thu": "Jeudi",
"fri": "Vendredi",
"sat": "Samedi",
"sun": "Dimanche"
}
},
"publicRecipe": {
"publicRecipeBy": "Recette publique par",
"viewInLibrary": "Voir dans votre bibliothèque",
"signUpFree": "S'inscrire gratuitement",
"logIn": "Se connecter",
"wantToSave": "Envie de sauvegarder cette recette ?",
"wantToSaveDescription": "Créez un compte gratuit pour sauvegarder des recettes, ajuster les portions et construire votre propre bibliothèque.",
"getStartedFree": "Commencer gratuitement"
},
"substitute": {
"fetchError": "Impossible de récupérer les substituts.",
"titleFor": "Substitut pour {ingredient}",
"substitutesFor": "Substituts pour",
"finding": "Recherche de substituts…",
"noneFound": "Aucune substitution trouvée."
},
"recipeChat": {
"sorry": "Désolé, je n'ai pas pu répondre à cela.",
"error": "Une erreur s'est produite. Veuillez réessayer.",
"suggestion1": "Puis-je substituer des ingrédients ?",
"suggestion2": "Comment savoir quand c'est prêt ?",
"suggestion3": "Puis-je préparer cela à l'avance ?",
"suggestion4": "Avec quoi puis-je servir ce plat ?",
"ariaLabel": "Demander à l'IA à propos de cette recette",
"title": "Poser une question sur cette recette",
"intro": "Demandez n'importe quoi sur cette recette — ingrédients, techniques, substitutions, minutage…",
"inputPlaceholder": "Posez une question…"
},
"servingScaler": {
"servings": "Portions",
"reset": "Réinitialiser",
"aiScale": "Ajuster avec l'IA",
"scaling": "Ajustement…",
"aiScaledNote": "Quantités ajustées par l'IA affichées ci-dessous"
},
"explore": {
"searchPlaceholder": "Rechercher des recettes publiques…",
"maxMinutes": "Minutes max",
"aiSearchPlaceholder": "ex. dîners rapides en semaine, cuisine réconfortante italienne…"
},
"common": {
"loading": "Chargement…",
"error": "Une erreur s'est produite",
"save": "Enregistrer",
"saved": "Enregistré",
"saveFailed": "Échec de l'enregistrement",
"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",
"confirm": "Confirmer",
"back": "Retour"
"back": "Retour",
"difficulty": "Difficulté",
"anyDifficulty": "Toutes",
"editManually": "Modifier manuellement",
"surpriseMe": "Surprends-moi",
"generatingContent": "Génération du contenu de la recette…"
},
"auth": {
"signIn": "Se connecter",
@@ -162,7 +269,8 @@
"createFirst": "Créer votre première recette",
"resultSingular": "résultat",
"resultPlural": "résultats",
"for": "pour"
"for": "pour",
"filterByTag": "Filtrer par tag…"
},
"recipeForm": {
"titleLabel": "Titre *",
@@ -183,6 +291,9 @@
"ingredient": "Ingrédient",
"note": "Note",
"addIngredient": "Ajouter un ingrédient",
"setCover": "Définir comme couverture",
"removePhoto": "Supprimer",
"expand": "Développer",
"steps": "Étapes",
"stepPlaceholder": "Étape {n}…",
"timerSeconds": "Minuteur (s)",
@@ -212,7 +323,16 @@
"addTo": "Ajouter à {day} {meal}",
"searchRecipes": "Rechercher des recettes…",
"servings": "Portions",
"difficulty": "Difficulté",
"noRecipes": "Aucune recette trouvée",
"addFailed": "Échec de l'ajout",
"addedToPantry": "{count, plural, one {1 article ajouté au garde-manger} other {{count} articles ajoutés au garde-manger}}",
"moveToPantryFailed": "Échec du déplacement des articles vers le garde-manger",
"aisleOther": "Autre",
"listEmptyState": "Cette liste est vide.",
"checkedCount": "{checked}/{total} cochés",
"moveToPantry": "Déplacer {count} vers le garde-manger",
"listCreated": "Liste créée",
"days": {
"mon": "Lun",
"tue": "Mar",
@@ -261,7 +381,21 @@
"empty": "Aucune liste de courses",
"listEmpty": "Vide",
"generated": "Générée",
"items": "{checked}/{total} articles"
"items": "{checked}/{total} articles",
"itemsChecked": "{checked}/{total} articles cochés",
"fromMealPlan": " · Depuis le planning repas",
"addToList": "Ajouter à la liste",
"dialogTitle": "Ajouter à la liste de courses",
"dialogDescription": "Ajouter les ingrédients de <strong>{title}</strong> à une liste de courses.",
"scaledSuffix": "ajustée)",
"loadingLists": "Chargement de vos listes…",
"modeExisting": "Ajouter à une liste existante",
"modeNew": "Créer une nouvelle liste",
"listNamePlaceholder": "Nom de la liste",
"ingredientsWillBeAdded": "{count, plural, one {1 ingrédient sera ajouté.} other {{count} ingrédients seront ajoutés.}}",
"adding": "Ajout…",
"addIngredients": "Ajouter les ingrédients",
"addedCount": "{count} ingrédients ajoutés à la liste"
},
"collections": {
"title": "Collections",
@@ -271,6 +405,19 @@
"recipeCount": "{count} recette",
"recipeCountPlural": "{count} recettes"
},
"social": {
"followed": "Abonné",
"unfollowed": "Désabonné",
"commentFailed": "Échec de la publication du commentaire",
"deleted": "Supprimé",
"ratingSaved": "Note enregistrée",
"collectionCreated": "Collection créée",
"collectionForked": "Collection dupliquée dans votre bibliothèque",
"inviteSent": "Invitation envoyée",
"memberRemoved": "Membre supprimé",
"replyPlaceholder": "Répondre…",
"commentPlaceholder": "Partagez vos impressions…"
},
"cookingMode": {
"cooking": "En cuisine",
"stepOf": "Étape {current} sur {total}",
@@ -287,6 +434,8 @@
"enableVoice": "Activer les commandes vocales",
"stopVoice": "Désactiver les commandes vocales",
"toggleIngredients": "Afficher les ingrédients",
"pageTitle": "Cuisiner : {title}",
"pageTitleFallback": "Mode cuisine",
"shortcutsTitle": "Raccourcis et commandes",
"keyboardSection": "Clavier",
"voiceSection": "Commandes vocales",
@@ -340,6 +489,31 @@
"changePasswordButton": "Changer le mot de passe",
"passwordChanged": "Mot de passe modifié avec succès.",
"passwordChangeFailed": "Échec de la modification du mot de passe.",
"webhookCreateFailed": "Échec de la création du webhook",
"webhookDeleteFailed": "Échec de la suppression du webhook",
"webhookUpdateFailed": "Échec de la mise à jour du webhook",
"webhookUrlPlaceholder": "https://exemple.fr/webhook",
"webhookAdding": "Ajout…",
"webhookAddButton": "Ajouter un webhook",
"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",
"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é",
"modelSaved": "Préférences de modèle enregistrées",
"modelSaveFailed": "Échec de l'enregistrement",
"modelDefaultPlaceholder": "Par défaut",
"modelNamePlaceholder": "ex. llama3.2",
"userUpdated": "Utilisateur mis à jour avec succès",
"userUpdateFailed": "Échec de l'enregistrement",
"adminSettingsSaved": "Paramètres enregistrés",
"adminSettingsFailed": "Échec de l'enregistrement des paramètres",
"nutritionGoalsSaved": "Objectifs nutritionnels enregistrés",
"bio": "Bio",
"publicBio": "Bio publique",
"publicBioDescription": "Affichée sur votre profil public.",