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:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ShoppingCart, Loader2, Plus, Check } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -45,6 +46,10 @@ export function AddToShoppingListButton({
|
||||
baseServings: number;
|
||||
ingredients: Ingredient[];
|
||||
}) {
|
||||
const t = useTranslations("recipe");
|
||||
const tShopping = useTranslations("shoppingLists");
|
||||
const tCommon = useTranslations("common");
|
||||
const tForm = useTranslations("recipeForm");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [lists, setLists] = useState<ShoppingList[]>([]);
|
||||
const [loadingLists, setLoadingLists] = useState(false);
|
||||
@@ -91,7 +96,7 @@ export function AddToShoppingListButton({
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: newListName.trim() || recipeTitle }),
|
||||
});
|
||||
if (!res.ok) { toast.error("Failed to create list"); return; }
|
||||
if (!res.ok) { toast.error(t("shoppingListCreateFailed")); return; }
|
||||
const created = await res.json() as { id: string };
|
||||
listId = created.id;
|
||||
}
|
||||
@@ -102,9 +107,9 @@ export function AddToShoppingListButton({
|
||||
body: JSON.stringify({ items }),
|
||||
});
|
||||
|
||||
if (!res.ok) { toast.error("Failed to add ingredients"); return; }
|
||||
if (!res.ok) { toast.error(t("shoppingListAddFailed")); return; }
|
||||
|
||||
toast.success(`${items.length} ingredients added to list`);
|
||||
toast.success(tShopping("addedCount", { count: items.length }));
|
||||
setOpen(false);
|
||||
} finally {
|
||||
setAdding(false);
|
||||
@@ -120,7 +125,7 @@ export function AddToShoppingListButton({
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
</Button>
|
||||
} />
|
||||
<TooltipContent>Add to list</TooltipContent>
|
||||
<TooltipContent>{tShopping("addToList")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -129,17 +134,20 @@ export function AddToShoppingListButton({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ShoppingCart className="h-5 w-5 text-primary" />
|
||||
Add to shopping list
|
||||
{tShopping("dialogTitle")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add the ingredients of <strong>{recipeTitle}</strong> to a shopping list.
|
||||
{tShopping.rich("dialogDescription", {
|
||||
title: recipeTitle,
|
||||
strong: (chunks) => <strong>{chunks}</strong>,
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Servings scaler */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="shrink-0">Servings</Label>
|
||||
<Label className="shrink-0">{tForm("servings")}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button" variant="ghost" size="icon" className="h-7 w-7"
|
||||
@@ -153,7 +161,7 @@ export function AddToShoppingListButton({
|
||||
>+</Button>
|
||||
</div>
|
||||
{scale !== 1 && (
|
||||
<span className="text-xs text-muted-foreground">({scale > 1 ? "×" : "÷"}{Math.abs(scale) !== 1 ? (scale > 1 ? scale.toFixed(2).replace(/\.?0+$/, "") : (1 / scale).toFixed(2).replace(/\.?0+$/, "")) : ""} scaled)</span>
|
||||
<span className="text-xs text-muted-foreground">({scale > 1 ? "×" : "÷"}{Math.abs(scale) !== 1 ? (scale > 1 ? scale.toFixed(2).replace(/\.?0+$/, "") : (1 / scale).toFixed(2).replace(/\.?0+$/, "")) : ""} {tShopping("scaledSuffix")}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -162,7 +170,7 @@ export function AddToShoppingListButton({
|
||||
{/* List picker */}
|
||||
{loadingLists ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground py-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading your lists…
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> {tShopping("loadingLists")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -170,7 +178,7 @@ export function AddToShoppingListButton({
|
||||
<RadioGroup value={mode} onValueChange={(v: string) => setMode(v as "existing" | "new")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="existing" id="mode-existing" />
|
||||
<Label htmlFor="mode-existing">Add to existing list</Label>
|
||||
<Label htmlFor="mode-existing">{tShopping("modeExisting")}</Label>
|
||||
</div>
|
||||
{mode === "existing" && (
|
||||
<div className="ml-6 space-y-1.5">
|
||||
@@ -192,7 +200,7 @@ export function AddToShoppingListButton({
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="new" id="mode-new" />
|
||||
<Label htmlFor="mode-new">Create new list</Label>
|
||||
<Label htmlFor="mode-new">{tShopping("modeNew")}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
@@ -202,7 +210,7 @@ export function AddToShoppingListButton({
|
||||
<Input
|
||||
value={newListName}
|
||||
onChange={(e) => setNewListName(e.target.value)}
|
||||
placeholder="List name"
|
||||
placeholder={tShopping("listNamePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -211,13 +219,13 @@ export function AddToShoppingListButton({
|
||||
|
||||
<Separator />
|
||||
|
||||
<p className="text-xs text-muted-foreground">{items.length} ingredient{items.length !== 1 ? "s" : ""} will be added.</p>
|
||||
<p className="text-xs text-muted-foreground">{tShopping("ingredientsWillBeAdded", { count: items.length })}</p>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="ghost" onClick={() => setOpen(false)} disabled={adding}>Cancel</Button>
|
||||
<Button variant="ghost" onClick={() => setOpen(false)} disabled={adding}>{tCommon("cancel")}</Button>
|
||||
<Button onClick={handleAdd} disabled={adding || loadingLists || (mode === "existing" && !selectedListId)}>
|
||||
{adding ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
|
||||
{adding ? "Adding…" : "Add ingredients"}
|
||||
{adding ? tShopping("adding") : tShopping("addIngredients")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user