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": "Language"
},
"recipe": {
"share": "Share",
"shareLinkCopied": "Link copied to clipboard",
"shareCopyFailed": "Failed to copy link",
"shareNotPublicNotice": "This link will only work once the recipe is set to Public — link copied anyway.",
"print": "Print",
"ingredients": "Ingredients",
"instructions": "Instructions",
"photos": "Photos",
@@ -37,6 +42,17 @@
"adapted": "Adapted recipe saved as draft",
"adaptFailed": "Failed to adapt recipe",
"adaptConstraintRequired": "Select at least one ingredient to exclude or add a constraint",
"adaptTooltip": "Adapt",
"adaptTitle": "Adapt this recipe",
"adaptDescription": "Tap ingredients to exclude them. AI will find the best substitutes while preserving the dish.",
"excludeIngredients": "Exclude ingredients",
"excludedCount": "{count} excluded",
"additionalConstraints": "Additional constraints",
"optional": "optional",
"adapting": "Adapting recipe — this may take 2030 seconds…",
"adaptingButton": "Adapting…",
"adaptButton": "Adapt recipe",
"clearExclusions": "Clear",
"variationsDirectionsPlaceholder": "e.g. make it vegan, use only pantry staples, reduce sugar…",
"historyRestoreFailed": "Failed to restore version",
"pairingMealFailed": "Failed to suggest pairings",
@@ -48,12 +64,30 @@
"cookAction": "Cook",
"adaptConstraintPlaceholder": "e.g. Make it vegan, lower the calories, use pantry staples only, gluten-free…",
"photoImportFailed": "Failed to import recipe from photo.",
"importFromPhoto": "Import from Photo",
"analyzingPhoto": "Analyzing photo…",
"pairMealTooltip": "Pair meal",
"historyTooltip": "History",
"translateTooltip": "Translate",
"drinksTooltip": "Drinks",
"variationsTooltip": "Variations",
"viewPublicly": "View publicly",
"shoppingListCreateFailed": "Failed to create list",
"shoppingListAddFailed": "Failed to add ingredients",
"bulkDeleted": "{count, plural, one {1 recipe deleted} other {{count} recipes deleted}}",
"bulkVisibility": "{count, plural, one {1 recipe set to {visibility}} other {{count} recipes set to {visibility}}}",
"bulkDeleteFailed": "Delete failed",
"bulkUpdateFailed": "Update failed",
"bulkDeleteConfirm": "{count, plural, one {Delete 1 recipe?} other {Delete {count} recipes?}} This cannot be undone.",
"deselectAll": "Deselect all",
"selectAll": "Select all",
"clickToSelect": "Click cards to select",
"selectedCount": "{count} selected",
"select": "Select",
"visibility": "Visibility",
"makePublic": "Make public",
"makeUnlisted": "Make unlisted",
"makePrivate": "Make private",
"visibility": {
"private": "Private",
"unlisted": "Unlisted",
@@ -103,7 +137,8 @@
"contentGenerated": "Ingredients and steps generated!",
"contentGenerateFailed": "Generation failed",
"contentSaveFailed": "Failed to save generated content",
"generatingContent": "Generating recipe content…"
"generatingContent": "Generating recipe content…",
"photoAnalysisNote": "AI will analyze this dish and infer ingredients, quantities, and cooking steps."
},
"variations": {
"title": "AI Recipe Variations",
@@ -118,7 +153,10 @@
"success": "Variation created — review and edit before publishing.",
"error": "Failed to generate variations",
"saveError": "Failed to save variation",
"wait": "This may take 2030 seconds"
"wait": "This may take 2030 seconds",
"directions": "Directions",
"optional": "optional",
"stepLabel": "Step {n}"
},
"translate": {
"title": "Translate Recipe",
@@ -138,7 +176,37 @@
"language": "Language",
"units": "Units",
"metric": "Metric",
"imperial": "Imperial"
"imperial": "Imperial",
"security": "Security",
"aiModels": "AI & Models",
"notifications": "Notifications",
"nutrition": "Nutrition",
"apiKeys": "API Keys",
"webhooks": "Webhooks",
"byok": {
"title": "Your API Keys (BYOK)",
"description": "Use your own API keys instead of the app's shared quota. Keys are encrypted at rest."
},
"modelPrefs": {
"title": "Model Preferences",
"description": "Choose which model to use for each task. Defaults to your first configured provider."
},
"apiKeysPage": {
"title": "API Keys",
"description": "Manage API keys for programmatic access to the Epicure API."
},
"pushNotifications": {
"title": "Push Notifications",
"description": "Get notified when someone comments on your recipes or likes your content."
},
"nutritionGoals": {
"title": "Daily Nutrition Goals",
"description": "Set your daily targets. These are shown as progress bars on your meal plan."
},
"webhooksPage": {
"title": "Webhooks",
"description": "Receive HTTP callbacks when events happen in your Epicure account."
}
},
"print": {
"footer": "Printed from Epicure",
@@ -190,9 +258,26 @@
"aiScaledNote": "AI-scaled quantities shown below"
},
"explore": {
"title": "Explore",
"searchPlaceholder": "Search public recipes…",
"maxMinutes": "Max minutes",
"aiSearchPlaceholder": "e.g. quick weeknight dinners, Italian comfort food…"
"aiSearchPlaceholder": "e.g. quick weeknight dinners, Italian comfort food…",
"anyDifficulty": "Any difficulty",
"resultsFoundSingular": "{count} recipe found",
"resultsFoundPlural": "{count} recipes found",
"noResultsTitle": "No recipes found for “{query}”",
"noResultsHint": "Try different keywords or remove filters",
"loadMore": "Load more",
"recipeIdeas": "Recipe ideas",
"recipeIdeasHint": "Tell the AI what you're in the mood for, or let it surprise you.",
"generating": "Generating…",
"getIdeas": "Get ideas",
"surpriseMe": "Surprise me",
"generateFullRecipe": "Generate full recipe",
"trendingThisWeek": "Trending this week",
"noTrending": "No trending recipes yet. Be the first to share one!",
"recentlyAdded": "Recently added",
"noRecent": "No public recipes yet."
},
"common": {
"loading": "Loading…",
@@ -200,6 +285,8 @@
"save": "Save",
"saved": "Saved",
"saveFailed": "Failed to save",
"print": "Print",
"share": "Share",
"deleteFailed": "Delete failed",
"updateFailed": "Update failed",
"copyFailed": "Failed to copy to clipboard",
@@ -276,10 +363,25 @@
"noMatch": "No recipes match",
"clearSearch": "Clear search",
"createFirst": "Create your first recipe",
"resultSingular": "result",
"resultPlural": "results",
"resultSingular": "{count} recipe",
"resultPlural": "{count} recipes",
"for": "for",
"filterByTag": "Filter by tag…"
"filterByTag": "Filter by tag…",
"sortRecentlyUpdated": "Recently updated",
"sortOldestUpdated": "Oldest updated",
"sortNewestFirst": "Newest first",
"sortOldestFirst": "Oldest first",
"sortTitleAZ": "Title AZ",
"sortTitleZA": "Title ZA",
"sort": "Sort",
"sortBy": "Sort by",
"filter": "Filter",
"all": "All",
"anyDifficulty": "Any difficulty",
"visibilityLabel": "Visibility",
"difficultyLabel": "Difficulty",
"tagLabel": "Tag",
"clearFilters": "Clear filters"
},
"recipeForm": {
"titleLabel": "Title *",
@@ -333,6 +435,13 @@
"mealPlan": {
"title": "Meal Plan",
"shoppingLists": "Shopping lists",
"print": "Print",
"shareTitle": "Share this week's plan",
"shareDescription": "Invite household members to view or edit this week's meal plan.",
"sharedWithYou": "Shared with you",
"sharedPlanOf": "{name}'s plan",
"weekOf": "Week of {date}",
"sharedMealPlanTitle": "{name}'s Meal Plan",
"addTo": "Add to {day} {meal}",
"searchRecipes": "Search recipes…",
"servings": "Servings",
@@ -373,7 +482,26 @@
"aiGenerated": "Meal plan generated!",
"includePantryItems": "Include pantry items",
"pantryMode": "Pantry mode — maximize use of what you have",
"pantryModeDescription": "Prefer meals that use multiple pantry ingredients and minimize extra shopping."
"pantryModeDescription": "Prefer meals that use multiple pantry ingredients and minimize extra shopping.",
"removeFailed": "Failed to remove",
"generationFailed": "Generation failed",
"anyDifficulty": "Any",
"aiPhaseAnalyzing": "Analyzing your preferences…",
"aiPhasePlanning": "Planning breakfast, lunch & dinner…",
"aiPhaseSelecting": "Selecting recipes for each day…",
"aiPhaseBalancing": "Balancing nutrition across the week…",
"aiPhaseFinalizing": "Finalizing your meal plan…",
"newList": "New list",
"newListTitle": "New shopping list",
"listNameLabel": "Name",
"listNamePlaceholder": "Weekly groceries",
"generateFromWeek": "Generate from meal plan week (optional)",
"generateFromWeekHint": "Picks Monday of the selected week",
"listCreateFailed": "Failed to create",
"listCreating": "Creating…",
"listCreate": "Create",
"pickRecipe": "Pick recipe",
"addEntry": "+ Add"
},
"pantry": {
"title": "Pantry",
@@ -397,9 +525,20 @@
"empty": "No shopping lists yet",
"listEmpty": "Empty",
"generated": "Generated",
"shareTitle": "Share shopping list",
"shareDescription": "Invite household members to view or edit this list.",
"sharedWithYou": "Shared with you",
"sendToGroceryDelivery": "Send to grocery delivery",
"copyAsText": "Copy list as text",
"sendToInstacart": "Send to Instacart",
"copiedToClipboard": "List copied to clipboard",
"exportBuildFailed": "Could not build export",
"instacartNotConfigured": "Instacart isn't configured yet",
"items": "{checked}/{total} items",
"itemsChecked": "{checked}/{total} items checked",
"fromMealPlan": " · From meal plan",
"itemCount": "{count} item",
"itemCountPlural": "{count} items",
"addToList": "Add to list",
"dialogTitle": "Add to shopping list",
"dialogDescription": "Add the ingredients of <strong>{title}</strong> to a shopping list.",
@@ -419,7 +558,40 @@
"empty": "No collections yet",
"public": "Public",
"recipeCount": "{count} recipe",
"recipeCountPlural": "{count} recipes"
"recipeCountPlural": "{count} recipes",
"exportPdf": "Export as PDF",
"shareTitle": "Share collection",
"shareDescription": "Invite people to view or edit this collection.",
"new": "New collection",
"newTitle": "New collection",
"nameLabel": "Name",
"namePlaceholder": "Weekend dinners",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Optional…",
"makePublic": "Make public",
"creating": "Creating…",
"create": "Create",
"createFailed": "Failed to create",
"forking": "Forking…",
"forkCollection": "Fork collection",
"forkFailed": "Fork failed"
},
"shareDialog": {
"invite": "Invite",
"emailPlaceholder": "Email address",
"owner": "Owner",
"viewer": "Viewer",
"editor": "Editor",
"loadingMembers": "Loading members…",
"noMembers": "No members yet.",
"invitationSent": "Invitation sent",
"alreadyMember": "Already a member",
"userNotFound": "User not found",
"inviteFailed": "Could not invite user",
"loadMembersFailed": "Could not load members",
"memberRemoved": "Member removed",
"removeMemberFailed": "Could not remove member",
"enterEmail": "Enter an email address"
},
"social": {
"followed": "Following",
@@ -513,9 +685,36 @@
"apiKeyCreateFailed": "Failed to create API key",
"apiKeyRevokeFailed": "Failed to revoke API key",
"apiKeyNamePlaceholder": "e.g. My app",
"apiKeyNameLabel": "Key name",
"apiKeyCreating": "Creating…",
"apiKeyCreate": "Create",
"apiKeyRevealNotice": "Save this key — it will not be shown again.",
"copy": "Copy",
"copied": "Copied!",
"dismiss": "Dismiss",
"noApiKeys": "No API keys yet.",
"createdOn": "Created {date}",
"lastUsedOn": "Last used {date}",
"neverUsed": "Never used",
"revoke": "Revoke",
"webhookUrlPlaceholder": "https://example.com/webhook",
"webhookAdding": "Adding…",
"webhookAddButton": "Add webhook",
"webhookDocsLink": "View webhook docs & Zapier integration →",
"endpointUrlLabel": "Endpoint URL",
"eventsLabel": "Events",
"eventsHint": "Select which events trigger this webhook. Leave all unchecked to receive all events.",
"secretRevealNotice": "Save this signing secret — it will not be shown again.",
"secretUsageHint": "Use it to verify the X-Epicure-Signature header on incoming requests.",
"noWebhooks": "No webhooks yet.",
"addedOn": "Added {date}",
"active": "Active",
"inactive": "Inactive",
"allEvents": "All events",
"deliveries": "Deliveries",
"disable": "Disable",
"enable": "Enable",
"noDeliveriesYet": "No deliveries yet.",
"modelDefaultPlaceholder": "Default",
"modelNamePlaceholder": "e.g. llama3.2",
"copyFailed": "Failed to copy to clipboard",
@@ -523,8 +722,25 @@
"byokSaveFailed": "Failed to save key",
"byokRemoved": "{provider} key removed",
"byokRemoveFailed": "Failed to remove key",
"byokConfigured": "Configured",
"byokOllamaHint": "Set via OLLAMA_BASE_URL env var",
"byokSaving": "Saving…",
"byokSave": "Save",
"byokFooter": "Your keys are encrypted at rest. When set, they override the app's default keys for your account.",
"modelSaved": "Model preferences saved",
"modelSaveFailed": "Failed to save",
"useCaseText": "Text generation",
"useCaseTextDesc": "Recipe generation, variations, translations, adapt",
"useCaseVision": "Vision / photo import",
"useCaseVisionDesc": "Dish recognition, recipe import from photo",
"useCaseMealPlan": "Meal planning",
"useCaseMealPlanDesc": "Weekly meal plan generation",
"providerLabel": "Provider",
"modelLabel": "Model",
"defaultAuto": "Default (auto)",
"modelDefault": "Default",
"modelSaving": "Saving…",
"saveModelPrefs": "Save model preferences",
"userUpdated": "User updated successfully",
"userUpdateFailed": "Failed to save",
"adminSettingsSaved": "Settings saved",