Files
Epicure/apps/web/components/recipe/translate-button.tsx
T
Arnaud eb424d8c04 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>
2026-07-02 15:13:51 +02:00

135 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { Languages, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
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" },
];
export function TranslateButton({ recipeId }: { recipeId: string }) {
const t = useTranslations("ai.translate");
const tRecipe = useTranslations("recipe");
const router = useRouter();
const [open, setOpen] = useState(false);
const [targetLanguage, setTargetLanguage] = useState("French");
const [translating, setTranslating] = useState(false);
async function handleTranslate() {
setTranslating(true);
try {
const res = await fetch(`/api/v1/ai/translate/${recipeId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetLanguage }),
});
if (!res.ok) {
const err = await res.json() as { error?: string };
toast.error(err.error ?? t("error"));
return;
}
const { id } = await res.json() as { id: string };
toast.success(t("success"));
setOpen(false);
router.push(`/recipes/${id}/edit`);
} finally {
setTranslating(false);
}
}
return (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={
<Button variant="ghost" size="icon" onClick={() => setOpen(true)}>
<Languages className="h-4 w-4" />
</Button>
} />
<TooltipContent>{tRecipe("translateTooltip")}</TooltipContent>
</Tooltip>
</TooltipProvider>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Languages className="h-5 w-5 text-primary" />
Translate Recipe
</DialogTitle>
<DialogDescription>
AI will translate the title, description, ingredients, and steps. Quantities and units are preserved.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Target language</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>
))}
</SelectContent>
</Select>
</div>
{translating && (
<p className="text-sm text-muted-foreground">This may take 2030 seconds</p>
)}
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={() => setOpen(false)} disabled={translating}>
Cancel
</Button>
<Button onClick={handleTranslate} disabled={translating}>
{translating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Translating
</>
) : (
<>
<Languages className="h-4 w-4" />
Translate
</>
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</>
);
}