feat(web): Next.js 15 app shell, auth, i18n, base UI

App Router setup with next-intl (en/fr), Better Auth wiring, shadcn/ui components,
Tailwind, AES-256-GCM encrypt util, Redis rate limiter, tier limit checker,
site_settings helper for runtime .env overrides.
This commit is contained in:
Arnaud
2026-07-01 08:09:31 +02:00
parent add9365250
commit 5b40968c9d
50 changed files with 4274 additions and 0 deletions
+341
View File
@@ -0,0 +1,341 @@
{
"nav": {
"recipes": "Recettes",
"explore": "Explorer",
"search": "Rechercher",
"feed": "Fil d'actualité",
"collections": "Collections",
"mealPlan": "Planning repas",
"pantry": "Garde-manger",
"shopping": "Courses",
"settings": "Paramètres",
"admin": "Admin",
"signOut": "Se déconnecter",
"apiKeys": "Clés API",
"webhooks": "Webhooks",
"language": "Langue"
},
"recipe": {
"ingredients": "Ingrédients",
"instructions": "Instructions",
"photos": "Photos",
"servings": "{count} portions",
"prep": "{mins}m prép.",
"cook": "{mins}m cuisson",
"total": "{mins}m total",
"edit": "Modifier",
"variations": "Variations",
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer",
"visibility": {
"private": "Privée",
"unlisted": "Non répertoriée",
"public": "Publique"
},
"difficulty": {
"easy": "Facile",
"medium": "Moyen",
"hard": "Difficile"
},
"dietary": {
"vegan": "Végétalien",
"vegetarian": "Végétarien",
"glutenFree": "Sans gluten",
"dairyFree": "Sans lactose",
"nutFree": "Sans noix",
"halal": "Halal",
"kosher": "Casher"
}
},
"ai": {
"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.",
"prompt": "Que voulez-vous cuisiner ?",
"placeholder": "ex. Un risotto crémeux aux champignons et parmesan, 4 portions, niveau facile",
"language": "Langue",
"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"
},
"variations": {
"title": "Variations de recette par IA",
"description": "Générez des variations créatives de cette recette — adaptations diététiques, profils de saveurs, changements de techniques.",
"generate": "Générer 3 variations",
"generating": "Génération des variations…",
"regenerate": "Régénérer",
"apply": "Appliquer",
"applying": "Application…",
"ingredientChanges": "Changements d'ingrédients",
"stepChanges": "Changements d'étapes",
"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"
},
"translate": {
"title": "Traduire la recette",
"description": "Traduisez le titre, la description, les ingrédients et les étapes de cette recette dans une autre langue.",
"targetLanguage": "Langue cible",
"button": "Traduire",
"translating": "Traduction en cours…",
"success": "Traduction enregistrée comme nouveau brouillon.",
"error": "Échec de la traduction de la recette",
"saveError": "Échec de l'enregistrement de la recette traduite",
"wait": "Cela peut prendre 20 à 30 secondes"
}
},
"settings": {
"title": "Paramètres",
"profile": "Profil",
"language": "Langue",
"units": "Unités",
"metric": "Métrique",
"imperial": "Impérial"
},
"common": {
"loading": "Chargement…",
"error": "Une erreur s'est produite",
"save": "Enregistrer",
"saved": "Enregistré",
"saveFailed": "Échec de l'enregistrement",
"cancel": "Annuler",
"delete": "Supprimer",
"confirm": "Confirmer",
"back": "Retour"
},
"auth": {
"signIn": "Se connecter",
"signInTitle": "Connectez-vous à votre bibliothèque de recettes",
"signInLoading": "Connexion en cours…",
"signUp": "S'inscrire",
"signUpTitle": "Créer un compte",
"signUpSubtitle": "Commencez à construire votre bibliothèque de recettes",
"signUpLoading": "Création du compte…",
"continueWithGoogle": "Continuer avec Google",
"email": "E-mail",
"emailPlaceholder": "vous@exemple.fr",
"password": "Mot de passe",
"name": "Nom",
"namePlaceholder": "Votre nom",
"forgotPassword": "Mot de passe oublié ?",
"forgotPasswordTitle": "Mot de passe oublié",
"forgotPasswordSent": "Vérifiez vos e-mails",
"forgotPasswordDescription": "Saisissez votre e-mail et nous vous enverrons un lien de réinitialisation.",
"forgotPasswordSentDescription": "Si un compte existe pour cet e-mail, vous recevrez un lien sous peu.",
"sendResetLink": "Envoyer le lien",
"sendingLink": "Envoi en cours…",
"resetPasswordTitle": "Réinitialiser le mot de passe",
"resetPasswordDescription": "Saisissez un nouveau mot de passe pour votre compte.",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"updatePassword": "Mettre à jour le mot de passe",
"updatingPassword": "Mise à jour…",
"invalidToken": "Jeton de réinitialisation invalide ou manquant.",
"noAccount": "Pas de compte ? S'inscrire",
"alreadyHaveAccount": "Déjà un compte ? Se connecter",
"backToSignIn": "Retour à la connexion",
"emailNotVerified": "E-mail non vérifié",
"checkInboxVerification": "Vérifiez votre boîte de réception pour l'e-mail de vérification.",
"resendVerification": "Renvoyer l'e-mail de vérification",
"resendingSending": "Envoi en cours…",
"or": "ou"
},
"recipes": {
"title": "Recettes",
"subtitle": "Votre bibliothèque personnelle de recettes",
"new": "Nouvelle recette",
"newTitle": "Nouvelle recette",
"newSubtitle": "Créez votre recette depuis zéro",
"importUrl": "Importer une URL",
"generate": "Générer",
"search": "Rechercher des recettes…",
"noRecipes": "Aucune recette pour l'instant",
"noMatch": "Aucune recette ne correspond",
"clearSearch": "Effacer la recherche",
"createFirst": "Créer votre première recette",
"resultSingular": "résultat",
"resultPlural": "résultats",
"for": "pour"
},
"recipeForm": {
"titleLabel": "Titre *",
"titleRequired": "Le titre est requis",
"titlePlaceholder": "Ma super recette",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Ce qui rend cette recette spéciale…",
"servings": "Portions",
"prepMins": "Prép. (min)",
"cookMins": "Cuisson (min)",
"difficulty": "Difficulté",
"visibility": "Visibilité",
"dietaryTags": "Tags alimentaires",
"photos": "Photos",
"ingredients": "Ingrédients",
"amount": "Quantité",
"unit": "Unité",
"ingredient": "Ingrédient",
"note": "Note",
"addIngredient": "Ajouter un ingrédient",
"steps": "Étapes",
"stepPlaceholder": "Étape {n}…",
"timerSeconds": "Minuteur (s)",
"addStep": "Ajouter une étape",
"saving": "Enregistrement…",
"saveChanges": "Enregistrer les modifications",
"createRecipe": "Créer la recette",
"updateSuccess": "Recette mise à jour",
"createSuccess": "Recette créée",
"saveError": "Échec de l'enregistrement de la recette"
},
"canCook": {
"title": "Que puis-je cuisiner ?",
"subtitle": "Recettes réalisables avec votre garde-manger",
"emptyPantry": "Votre garde-manger est vide",
"addPantryItems": "Ajouter des articles",
"readyToCook": "Prêt à cuisiner",
"almostThere": "Presque là (7099 %)",
"partialMatches": "Correspondances partielles",
"noMatches": "Aucune correspondance trouvée",
"missing": "Manquant",
"ingredientProgress": "{matched}/{total} ingrédients"
},
"mealPlan": {
"title": "Planning repas",
"shoppingLists": "Listes de courses",
"addTo": "Ajouter à {day} {meal}",
"searchRecipes": "Rechercher des recettes…",
"servings": "Portions",
"noRecipes": "Aucune recette trouvée",
"days": {
"mon": "Lun",
"tue": "Mar",
"wed": "Mer",
"thu": "Jeu",
"fri": "Ven",
"sat": "Sam",
"sun": "Dim"
},
"meals": {
"breakfast": "Petit-déjeuner",
"lunch": "Déjeuner",
"dinner": "Dîner",
"snack": "Collation"
},
"generateWithAi": "Générer avec l'IA",
"aiModalDescription": "L'IA va générer une semaine complète de repas et créer des recettes brouillons.",
"dietaryPrefs": "Préférences alimentaires",
"dietaryPrefsPlaceholder": "ex. végétarien, sans gluten, low-carb…",
"aiPantryNote": "Les articles de votre garde-manger seront pris en compte.",
"generating": "Génération…",
"generate": "Générer",
"cancel": "Annuler",
"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."
},
"pantry": {
"title": "Garde-manger",
"subtitle": "Suivez ce que vous avez en stock",
"canCook": "Que puis-je cuisiner ?"
},
"feed": {
"title": "Fil d'actualité",
"followEmpty": "Suivez des chefs pour voir leurs recettes ici.",
"noNew": "Aucune nouvelle recette de vos abonnements.",
"following": "Abonnements",
"trending": "Tendances",
"trendingEmpty": "Aucune recette tendance cette semaine.",
"loading": "Chargement…"
},
"shoppingLists": {
"title": "Listes de courses",
"subtitle": "Planifiez vos courses",
"empty": "Aucune liste de courses",
"listEmpty": "Vide",
"generated": "Générée",
"items": "{checked}/{total} articles"
},
"collections": {
"title": "Collections",
"subtitle": "Organisez vos recettes favorites en collections",
"empty": "Aucune collection pour l'instant",
"public": "Publique",
"recipeCount": "{count} recette",
"recipeCountPlural": "{count} recettes"
},
"cookingMode": {
"cooking": "En cuisine",
"stepOf": "Étape {current} sur {total}",
"ingredients": "Ingrédients",
"startTimer": "Démarrer le minuteur",
"timerDone": "Terminé",
"pause": "Pause",
"reset": "Réinitialiser",
"previous": "Précédent",
"next": "Suivant",
"finish": "Terminé !",
"voiceNotSupported": "Commandes vocales non supportées dans ce navigateur",
"voiceError": "Erreur vocale — réessayez",
"enableVoice": "Activer les commandes vocales",
"stopVoice": "Désactiver les commandes vocales",
"toggleIngredients": "Afficher les ingrédients",
"shortcutsTitle": "Raccourcis et commandes",
"keyboardSection": "Clavier",
"voiceSection": "Commandes vocales",
"enableMicFirst": "Activez le micro via le bouton micro d'abord.",
"stepRepeatPrefix": "Étape {n} : ",
"shortcuts": {
"nextStep": "Étape suivante",
"prevStep": "Étape précédente",
"startTimer": "Démarrer le minuteur",
"toggleHelp": "Afficher/masquer ce panneau",
"exitMode": "Quitter le mode cuisine"
},
"voiceLabels": {
"nextCmd": "suivant / continuer",
"prevCmd": "précédent / retour",
"timerCmd": "minuteur / démarrer",
"stopCmd": "arrêter / pause",
"resetCmd": "réinitialiser",
"repeatCmd": "répéter / encore",
"nextLabel": "Étape suivante",
"prevLabel": "Étape précédente",
"timerLabel": "Démarrer le minuteur",
"stopLabel": "Mettre en pause",
"resetLabel": "Réinitialiser le minuteur",
"repeatLabel": "Relire l'étape"
}
},
"settingsForm": {
"profile": "Profil",
"displayName": "Nom d'affichage",
"email": "E-mail",
"emailReadOnly": "L'e-mail ne peut pas être modifié ici.",
"saving": "Enregistrement…",
"title": "Paramètres",
"subtitle": "Gérez votre compte et vos préférences",
"language": "Langue",
"languageDescription": "Choisissez la langue de l'application et du contenu généré par l'IA.",
"changeEmail": "Changer l'e-mail",
"newEmail": "Nouvelle adresse e-mail",
"changeEmailDescription": "Un lien de vérification sera envoyé à votre nouvelle adresse avant que la modification soit prise en compte.",
"sendVerification": "Envoyer la vérification",
"sendingVerification": "Envoi…",
"emailChangeSent": "E-mail de vérification envoyé. Vérifiez votre boîte de réception.",
"emailChangeFailed": "Échec de l'envoi de l'e-mail de vérification.",
"changePassword": "Changer le mot de passe",
"currentPassword": "Mot de passe actuel",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le nouveau mot de passe",
"passwordMismatch": "Les mots de passe ne correspondent pas.",
"changingPassword": "Modification…",
"changePasswordButton": "Changer le mot de passe",
"passwordChanged": "Mot de passe modifié avec succès.",
"passwordChangeFailed": "Échec de la modification du mot de passe."
}
}