fix: batch of collections/i18n/translate-button fixes (v0.53.1)

- Drag-reorder used verticalListSortingStrategy on a multi-column grid,
  which computes wrong transforms for grid reflow — swapped to
  rectSortingStrategy so cards actually animate live while dragging.
- Grip handle was rendered as a sibling of (not a descendant of) the
  `group` element its `group-hover:opacity-100` depended on, so it was
  permanently invisible. Fixed the DOM nesting and made it always
  partially visible instead of hover-only.
- common.edit was missing from both locales (not just French) —
  edit-collection-dialog.tsx was the first caller to hit it.
- Root cause of "generated in my language but Translate still shows":
  generate-meal, meal-plan/generate, and adapt never set recipes.language
  on the row they inserted, so the button's `!recipe.language || ...`
  check always fell back to "show it". Fixed at all three insert sites.
- Translate dialog was entirely hardcoded English (title, description,
  language names, buttons) despite i18n keys already existing for most of
  it — now uses them, plus new translated language-name keys.
- Recipe tags now render on the recipe detail page (previously grid-card
  only).
- Collection header actions converted to icon-only + tooltip, matching
  the recipe page's pattern instead of icon+label buttons.
- Collections list search now also matches recipe titles inside each
  collection, not just the collection's own name/description.
- Explore page links to /collections/explore next to its tabs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-19 19:16:03 +02:00
parent e8c687e53a
commit b1f745da66
20 changed files with 227 additions and 94 deletions
+14
View File
@@ -2,6 +2,20 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
## 0.53.1 — 2026-07-19 16:10
### Fixed
- Drag-reorder in a collection now animates live as you drag instead of only updating on drop, and the grip handle is now visible instead of an invisible hover target.
- "Edit" was untranslated everywhere it's used (missing from both languages, not just French).
- Recipes generated via "Generate meal", weekly meal plan, or "Adapt recipe" never had their language recorded — this made the Translate button appear even though the recipe was already in your language. Fixed at the source for all three.
- The Translate dialog itself was hardcoded in English regardless of app language — now fully translated.
### Added
- Recipe tags now show on the recipe detail page.
- Collection header actions are now icon-only with tooltips, matching the recipe page.
- Searching collections now also matches recipes inside them.
- Explore links to "Discover collections" next to its tabs.
## 0.53.0 — 2026-07-19 15:20
### Added
+40 -33
View File
@@ -14,6 +14,7 @@ import { EditCollectionDialog } from "@/components/collections/edit-collection-d
import { DeleteCollectionDialog } from "@/components/collections/delete-collection-dialog";
import { Badge } from "@/components/ui/badge";
import { buttonVariants } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { ExportMarkdownButton } from "@/components/shared/export-markdown-button";
import { EmptyState } from "@/components/shared/empty-state";
@@ -71,40 +72,46 @@ export default async function CollectionPage({ params }: Params) {
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
{recipeList.length > 0 && (
<>
<Link href={`/print/collection/${id}`} target="_blank" className={cn(buttonVariants({ variant: "outline", size: "sm" }))}>
<Printer className="h-4 w-4" />
{m.collections.exportPdf}
</Link>
<ExportMarkdownButton
markdown={collectionToMarkdown({
name: col.name,
description: col.description,
recipes: recipeList,
})}
filename={col.name}
<TooltipProvider>
<div className="flex flex-wrap items-center gap-1">
{recipeList.length > 0 && (
<>
<Tooltip>
<TooltipTrigger render={
<Link href={`/print/collection/${id}`} target="_blank" className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}>
<Printer className="h-4 w-4" />
</Link>
} />
<TooltipContent>{m.collections.exportPdf}</TooltipContent>
</Tooltip>
<ExportMarkdownButton
markdown={collectionToMarkdown({
name: col.name,
description: col.description,
recipes: recipeList,
})}
filename={col.name}
/>
</>
)}
{isOwner && <GenerateMealDialog collectionId={id} />}
{isOwner && <ShareCollectionButton collectionId={id} />}
{isOwner && (
<EditCollectionDialog
collectionId={id}
initialName={col.name}
initialDescription={col.description}
initialNotes={col.notes}
initialTags={col.tags}
initialIsPublic={col.isPublic}
/>
</>
)}
{isOwner && <GenerateMealDialog collectionId={id} />}
{isOwner && <ShareCollectionButton collectionId={id} />}
{isOwner && (
<EditCollectionDialog
collectionId={id}
initialName={col.name}
initialDescription={col.description}
initialNotes={col.notes}
initialTags={col.tags}
initialIsPublic={col.isPublic}
/>
)}
{isOwner && <DeleteCollectionDialog collectionId={id} />}
{!isOwner && col.isPublic && (
<ForkCollectionButton collectionId={id} />
)}
</div>
)}
{isOwner && <DeleteCollectionDialog collectionId={id} />}
{!isOwner && col.isPublic && (
<ForkCollectionButton collectionId={id} />
)}
</div>
</TooltipProvider>
</div>
{recipeList.length === 0 ? (
+12 -1
View File
@@ -19,7 +19,18 @@ export default async function CollectionsPage({
const query = q?.trim();
const where = query
? and(eq(collections.userId, session.user.id), or(ilike(collections.name, `%${query}%`), ilike(collections.description, `%${query}%`)))
? and(
eq(collections.userId, session.user.id),
or(
ilike(collections.name, `%${query}%`),
ilike(collections.description, `%${query}%`),
sql`exists (
select 1 from collection_recipes cr
inner join recipes r on r.id = cr.recipe_id
where cr.collection_id = ${collections.id} and r.title ilike ${`%${query}%`}
)`
)
)
: eq(collections.userId, session.user.id);
const [userCollections, countRows] = await Promise.all([
+8
View File
@@ -353,6 +353,14 @@ export default async function RecipePage({ params }: Params) {
))}
</div>
)}
{recipe.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{recipe.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">{tag}</Badge>
))}
</div>
)}
</div>
{/* Cover photo */}
+4 -1
View File
@@ -52,6 +52,8 @@ export async function POST(req: NextRequest, { params }: Params) {
if (!configResult.ok) return configResult.response;
const aiConfig = configResult.data;
const locale = (session!.user as { locale?: string }).locale ?? "en";
const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "family", () =>
adaptRecipe(
{
@@ -66,7 +68,7 @@ export async function POST(req: NextRequest, { params }: Params) {
extraConstraints: parsed.data.extraConstraints,
},
{ ...aiConfig, userContext: privateBio ?? undefined },
(session!.user as { locale?: string }).locale ?? "en"
locale
), { skipQuota: aiConfig.isByok }
);
if (!result.ok) return result.response;
@@ -100,6 +102,7 @@ export async function POST(req: NextRequest, { params }: Params) {
description: adapted.description,
baseServings: adapted.baseServings,
visibility: "private",
language: locale,
difficulty: adapted.difficulty,
prepMins: adapted.prepMins,
cookMins: adapted.cookMins,
@@ -8,6 +8,7 @@ import { withAiQuota, resolveAiConfigOrError } from "@/lib/ai/ai-error";
import { checkAndIncrementTierLimit, incrementUsage, TierLimitError } from "@/lib/tiers";
import { generateMeal, MEAL_COURSES } from "@/lib/ai/features/generate-meal";
import { getUserPrivateBio } from "@/lib/ai/user-bio";
import { getMessages } from "@/lib/i18n/server";
const Schema = z.object({
collectionId: z.string().min(1),
@@ -89,12 +90,13 @@ export async function POST(req: NextRequest) {
baseServings: recipe.baseServings,
recipeType: recipe.recipeType,
visibility: "private",
language: locale,
aiGenerated: true,
difficulty: recipe.difficulty ?? null,
prepMins: recipe.prepMins ?? null,
cookMins: recipe.cookMins ?? null,
dietaryTags: recipe.dietaryTags ?? {},
tags: [recipe.course],
tags: [getMessages(locale).collections.course[recipe.course]],
});
if (recipe.ingredients.length > 0) {
@@ -134,6 +134,7 @@ export async function POST(req: NextRequest) {
description: entry.recipe.description,
baseServings: entry.servings,
visibility: "private",
language: locale,
aiGenerated: true,
difficulty: entry.recipe.difficulty ?? null,
prepMins: entry.recipe.prepMins ?? null,
@@ -16,7 +16,7 @@ import {
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
rectSortingStrategy,
arrayMove,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
@@ -49,6 +49,7 @@ function SortableRecipeCard({
onToggle: () => void;
dragDisabled: boolean;
}) {
const t = useTranslations("collections");
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: recipe.id,
disabled: dragDisabled,
@@ -60,7 +61,7 @@ function SortableRecipeCard({
<div
ref={setNodeRef}
style={style}
className={cn("relative", selectMode && "cursor-pointer", isDragging && "z-10 opacity-70")}
className={cn("relative group", selectMode && "cursor-pointer", isDragging && "z-10 opacity-70")}
onClick={selectMode ? onToggle : undefined}
>
{selectMode && (
@@ -74,18 +75,20 @@ function SortableRecipeCard({
</div>
)}
{!selectMode && !dragDisabled && (
// Always at least partly visible (not hover-only) — a fully hidden-until-hover
// handle is exactly what made dragging undiscoverable before.
<button
type="button"
{...attributes}
{...listeners}
className="absolute top-2 right-2 z-10 h-6 w-6 rounded-md bg-background/90 border shadow-sm flex items-center justify-center text-muted-foreground cursor-grab active:cursor-grabbing opacity-0 group-hover:opacity-100 hover:text-foreground transition-opacity"
className="absolute top-2 right-2 z-10 h-6 w-6 rounded-md bg-background/90 border shadow-sm flex items-center justify-center text-muted-foreground cursor-grab active:cursor-grabbing opacity-60 group-hover:opacity-100 hover:text-foreground hover:border-foreground/30 transition-opacity"
onClick={(e) => e.preventDefault()}
aria-label="Drag to reorder"
aria-label={t("dragToReorder")}
>
<GripVertical className="h-3.5 w-3.5" />
</button>
)}
<div className={cn("group", selectMode && "pointer-events-none", selectMode && selected && "rounded-xl ring-2 ring-primary")}>
<div className={cn(selectMode && "pointer-events-none", selectMode && selected && "rounded-xl ring-2 ring-primary")}>
{selectMode ? (
<RecipeGridCard recipe={recipe} />
) : (
@@ -207,7 +210,7 @@ export function CollectionRecipesGrid({ collectionId, recipes: initialRecipes }:
<p className="text-sm text-muted-foreground py-8 text-center">{t("noRecipeSearchResults")}</p>
) : (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={filtered.map((r) => r.id)} strategy={verticalListSortingStrategy}>
<SortableContext items={filtered.map((r) => r.id)} strategy={rectSortingStrategy}>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{filtered.map((recipe) => (
<SortableRecipeCard
@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
AlertDialog,
AlertDialogAction,
@@ -41,10 +42,16 @@ export function DeleteCollectionDialog({ collectionId }: { collectionId: string
return (
<>
<Button variant="outline" size="sm" className="gap-1.5 text-destructive hover:text-destructive" onClick={() => setOpen(true)}>
<Trash2 className="h-4 w-4" />
{tCommon("delete")}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={
<Button variant="ghost" size="icon" className="text-destructive hover:text-destructive" onClick={() => setOpen(true)} aria-label={tCommon("delete")}>
<Trash2 className="h-4 w-4" />
</Button>
} />
<TooltipContent>{tCommon("delete")}</TooltipContent>
</Tooltip>
</TooltipProvider>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { Pencil, Tag, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
@@ -88,10 +89,16 @@ export function EditCollectionDialog({
return (
<>
<Button variant="outline" size="sm" className="gap-1.5" onClick={() => setOpen(true)}>
<Pencil className="h-4 w-4" />
{tCommon("edit")}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={
<Button variant="ghost" size="icon" onClick={() => setOpen(true)} aria-label={tCommon("edit")}>
<Pencil className="h-4 w-4" />
</Button>
} />
<TooltipContent>{tCommon("edit")}</TooltipContent>
</Tooltip>
</TooltipProvider>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg">
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { GitFork } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { toast } from "sonner";
export function ForkCollectionButton({ collectionId }: { collectionId: string }) {
@@ -31,9 +32,15 @@ export function ForkCollectionButton({ collectionId }: { collectionId: string })
}
return (
<Button variant="outline" size="sm" onClick={() => { void handleFork(); }} disabled={forking} className="gap-2 shrink-0">
<GitFork className="h-4 w-4" />
{forking ? t("forking") : t("forkCollection")}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={
<Button variant="ghost" size="icon" onClick={() => { void handleFork(); }} disabled={forking} aria-label={t("forkCollection")}>
<GitFork className="h-4 w-4" />
</Button>
} />
<TooltipContent>{forking ? t("forking") : t("forkCollection")}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { ChefHat, Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
@@ -67,10 +68,16 @@ export function GenerateMealDialog({ collectionId }: { collectionId: string }) {
return (
<>
<Button variant="outline" size="sm" className="gap-1.5" onClick={() => setOpen(true)}>
<ChefHat className="h-4 w-4" />
{t("generateMeal")}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={
<Button variant="ghost" size="icon" onClick={() => setOpen(true)} aria-label={t("generateMeal")}>
<ChefHat className="h-4 w-4" />
</Button>
} />
<TooltipContent>{t("generateMeal")}</TooltipContent>
</Tooltip>
</TooltipProvider>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg">
@@ -12,6 +12,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Input } from "@/components/ui/input";
import {
Select,
@@ -116,10 +117,16 @@ export function ShareCollectionButton({ collectionId }: Props) {
return (
<>
<Button variant="outline" size="sm" onClick={() => handleOpenChange(true)}>
<UserPlus className="h-4 w-4 mr-2" />
{tCommon("share")}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={
<Button variant="ghost" size="icon" onClick={() => handleOpenChange(true)} aria-label={tCommon("share")}>
<UserPlus className="h-4 w-4" />
</Button>
} />
<TooltipContent>{tCommon("share")}</TooltipContent>
</Tooltip>
</TooltipProvider>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
+14 -25
View File
@@ -17,24 +17,15 @@ import {
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
const LANGUAGES = [
{ code: "French", label: "French" },
{ code: "Spanish", label: "Spanish" },
{ code: "German", label: "German" },
{ code: "Italian", label: "Italian" },
{ code: "Portuguese", label: "Portuguese" },
{ code: "English", label: "English" },
{ code: "Japanese", label: "Japanese" },
{ code: "Chinese", label: "Chinese" },
{ code: "Arabic", label: "Arabic" },
{ code: "Dutch", label: "Dutch" },
{ code: "Polish", label: "Polish" },
{ code: "Russian", label: "Russian" },
];
const LANGUAGE_CODES = [
"French", "Spanish", "German", "Italian", "Portuguese", "English",
"Japanese", "Chinese", "Arabic", "Dutch", "Polish", "Russian",
] as const;
export function TranslateButton({ recipeId }: { recipeId: string }) {
const t = useTranslations("ai.translate");
const tRecipe = useTranslations("recipe");
const tCommon = useTranslations("common");
const router = useRouter();
const [open, setOpen] = useState(false);
const [targetLanguage, setTargetLanguage] = useState("French");
@@ -82,46 +73,44 @@ export function TranslateButton({ recipeId }: { recipeId: string }) {
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Languages className="h-5 w-5 text-primary" />
Translate Recipe
{t("title")}
</DialogTitle>
<DialogDescription>
AI will translate the title, description, ingredients, and steps. Quantities and units are preserved.
</DialogDescription>
<DialogDescription>{t("description")}</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Target language</Label>
<Label>{t("targetLanguage")}</Label>
<Select value={targetLanguage} onValueChange={(v) => v && setTargetLanguage(v)} disabled={translating}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{LANGUAGES.map((l) => (
<SelectItem key={l.code} value={l.code}>{l.label}</SelectItem>
{LANGUAGE_CODES.map((code) => (
<SelectItem key={code} value={code}>{t(`languages.${code}`)}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{translating && (
<p className="text-sm text-muted-foreground">This may take 2030 seconds</p>
<p className="text-sm text-muted-foreground">{t("wait")}</p>
)}
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={() => setOpen(false)} disabled={translating}>
Cancel
{tCommon("cancel")}
</Button>
<Button onClick={handleTranslate} disabled={translating}>
{translating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Translating
{t("translating")}
</>
) : (
<>
<Languages className="h-4 w-4" />
Translate
{t("button")}
</>
)}
</Button>
@@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Flame, Clock, ChefHat, Search, Sparkles, Wand2, ArrowRight, Shuffle, Users, Rss, UserPlus } from "lucide-react";
import { Flame, Clock, ChefHat, Search, Sparkles, Wand2, ArrowRight, Shuffle, Users, Rss, UserPlus, FolderOpen } from "lucide-react";
import { useTranslations } from "next-intl";
import type { RecipeResult as ExploreRecipeResult } from "@/app/(app)/explore/page";
import { RecipeGridCard, type GridCardRecipe } from "@/components/recipe/recipe-grid-card";
@@ -184,6 +184,7 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
const tCommon = useTranslations("common");
const tRecipe = useTranslations("recipe");
const tPeople = useTranslations("people");
const tCollections = useTranslations("collections");
const [inputValue, setInputValue] = useState(initialQuery);
const [query, setQuery] = useState(initialQuery);
@@ -512,7 +513,8 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
{/* Explore tabs — shown when no query */}
{!hasQuery && (
<>
<div className="flex gap-1 border-b">
<div className="flex items-center justify-between gap-2 border-b">
<div className="flex gap-1">
<button
onClick={() => setTab("discover")}
className={cn(
@@ -542,6 +544,14 @@ export function ExplorePageContent({ trending, recent, initialQuery, popularTags
{tFeed("forYou")}
</button>
</div>
<Link
href="/collections/explore"
className="pb-2 text-sm text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1.5 shrink-0"
>
<FolderOpen className="h-3.5 w-3.5" />
<span className="hidden sm:inline">{tCollections("exploreLink")}</span>
</Link>
</div>
{tab === "discover" && (
<>
+17 -1
View File
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together.
export const APP_VERSION = "0.53.0";
export const APP_VERSION = "0.53.1";
export type ChangelogEntry = {
version: string;
@@ -11,6 +11,22 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.53.1",
date: "2026-07-19 16:10",
fixed: [
"Drag-reorder in a collection now animates live as you drag instead of only updating on drop (was using a list-only sorting strategy on a multi-column grid), and the grip handle is now visible instead of an invisible hover target.",
"\"Edit\" was untranslated everywhere it's used (missing from both languages, not just French).",
"Recipes generated via \"Generate meal\" or a weekly meal plan, or produced by \"Adapt recipe\", never had their language recorded — this made the Translate button appear even though the recipe was already in your language. Fixed at the source for all three.",
"The Translate dialog itself was hardcoded in English regardless of app language — title, description, language list, and buttons are now fully translated.",
],
added: [
"Recipe tags now show on the recipe detail page (previously only visible on grid cards).",
"Collection header actions (print, share, edit, delete, generate meal, fork) are now icon-only with tooltips, matching the recipe page.",
"Searching collections now also matches recipes inside them, not just the collection's own name/description.",
"Explore now links to \"Discover collections\" next to its tabs.",
],
},
{
version: "0.53.0",
date: "2026-07-19 15:20",
+18 -1
View File
@@ -387,7 +387,21 @@
"success": "Translation saved as new draft recipe.",
"error": "Failed to translate recipe",
"saveError": "Failed to save translated recipe",
"wait": "This may take 2030 seconds"
"wait": "This may take 2030 seconds",
"languages": {
"French": "French",
"Spanish": "Spanish",
"German": "German",
"Italian": "Italian",
"Portuguese": "Portuguese",
"English": "English",
"Japanese": "Japanese",
"Chinese": "Chinese",
"Arabic": "Arabic",
"Dutch": "Dutch",
"Polish": "Polish",
"Russian": "Russian"
}
}
},
"settings": {
@@ -702,6 +716,7 @@
"deleted": "Deleted",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"confirm": "Confirm",
"back": "Back",
"difficulty": "Difficulty",
@@ -1184,6 +1199,7 @@
"recipeCount": "{count} recipe",
"recipeCountPlural": "{count} recipes",
"discoverTitle": "Discover",
"exploreLink": "Discover collections",
"discoverSubtitle": "Trending and recently shared public collections",
"myCollectionsLink": "My collections",
"trendingThisWeek": "Trending this week",
@@ -1214,6 +1230,7 @@
"removeFromCollectionConfirmTitle": "{count, plural, one {Remove 1 recipe from this collection?} other {Remove {count} recipes from this collection?}}",
"removeFromCollectionConfirmDescription": "The recipe itself won't be deleted, just removed from this collection.",
"reorderFailed": "Failed to save the new order",
"dragToReorder": "Drag to reorder",
"editTitle": "Edit collection",
"editSuccess": "Collection updated",
"editFailed": "Failed to update collection",
+18 -1
View File
@@ -387,7 +387,21 @@
"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"
"wait": "Cela peut prendre 20 à 30 secondes",
"languages": {
"French": "Français",
"Spanish": "Espagnol",
"German": "Allemand",
"Italian": "Italien",
"Portuguese": "Portugais",
"English": "Anglais",
"Japanese": "Japonais",
"Chinese": "Chinois",
"Arabic": "Arabe",
"Dutch": "Néerlandais",
"Polish": "Polonais",
"Russian": "Russe"
}
}
},
"settings": {
@@ -702,6 +716,7 @@
"deleted": "Supprimé",
"cancel": "Annuler",
"delete": "Supprimer",
"edit": "Modifier",
"confirm": "Confirmer",
"back": "Retour",
"difficulty": "Difficulté",
@@ -1175,6 +1190,7 @@
"recipeCount": "{count} recette",
"recipeCountPlural": "{count} recettes",
"discoverTitle": "Découvrir",
"exploreLink": "Découvrir des collections",
"discoverSubtitle": "Collections publiques tendances et récemment partagées",
"myCollectionsLink": "Mes collections",
"trendingThisWeek": "Tendance cette semaine",
@@ -1205,6 +1221,7 @@
"removeFromCollectionConfirmTitle": "{count, plural, one {Retirer 1 recette de cette collection ?} other {Retirer {count} recettes de cette collection ?}}",
"removeFromCollectionConfirmDescription": "La recette elle-même ne sera pas supprimée, seulement retirée de cette collection.",
"reorderFailed": "Échec de l'enregistrement du nouvel ordre",
"dragToReorder": "Glisser pour réorganiser",
"editTitle": "Modifier la collection",
"editSuccess": "Collection mise à jour",
"editFailed": "Échec de la mise à jour de la collection",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@epicure/web",
"version": "0.53.0",
"version": "0.53.1",
"private": true,
"scripts": {
"dev": "next dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "epicure",
"version": "0.53.0",
"version": "0.53.1",
"private": true,
"scripts": {
"dev": "pnpm --filter web dev",