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:
Arnaud
2026-07-02 07:58:18 +02:00
parent afff6cf9eb
commit 01fdbb880b
32 changed files with 515 additions and 187 deletions
@@ -1,6 +1,7 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
@@ -19,6 +20,7 @@ type Props = {
};
export function RecipeChatPanel({ recipeId, recipeTitle }: Props) {
const t = useTranslations("recipeChat");
const [open, setOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
@@ -49,12 +51,12 @@ export function RecipeChatPanel({ recipeId, recipeTitle }: Props) {
const data = await res.json() as { answer?: string; error?: string };
setMessages((prev) => [
...prev,
{ role: "assistant", content: data.answer ?? "Sorry, I couldn't answer that." },
{ role: "assistant", content: data.answer ?? t("sorry") },
]);
} catch {
setMessages((prev) => [
...prev,
{ role: "assistant", content: "Something went wrong. Please try again." },
{ role: "assistant", content: t("error") },
]);
} finally {
setLoading(false);
@@ -62,10 +64,10 @@ export function RecipeChatPanel({ recipeId, recipeTitle }: Props) {
};
const suggestions = [
"Can I substitute any ingredients?",
"How do I know when it's done?",
"Can I make this ahead of time?",
"What can I serve with this?",
t("suggestion1"),
t("suggestion2"),
t("suggestion3"),
t("suggestion4"),
];
return (
@@ -75,7 +77,7 @@ export function RecipeChatPanel({ recipeId, recipeTitle }: Props) {
size="icon"
className="fixed bottom-6 right-6 h-12 w-12 rounded-full shadow-lg z-40"
onClick={() => setOpen(true)}
aria-label="Ask AI about this recipe"
aria-label={t("ariaLabel")}
>
<MessageCircle className="h-5 w-5" />
</Button>
@@ -85,7 +87,7 @@ export function RecipeChatPanel({ recipeId, recipeTitle }: Props) {
<SheetHeader className="px-4 py-3 pr-10 border-b shrink-0">
<SheetTitle className="text-base flex items-center gap-2">
<Bot className="h-4 w-4 text-primary" />
Ask about this recipe
{t("title")}
</SheetTitle>
<p className="text-xs text-muted-foreground text-left">{recipeTitle}</p>
</SheetHeader>
@@ -94,7 +96,7 @@ export function RecipeChatPanel({ recipeId, recipeTitle }: Props) {
{messages.length === 0 && (
<div className="space-y-3">
<p className="text-sm text-muted-foreground text-center py-4">
Ask anything about this recipe ingredients, techniques, substitutions, timing
{t("intro")}
</p>
<div className="space-y-2">
{suggestions.map((s) => (
@@ -163,7 +165,7 @@ export function RecipeChatPanel({ recipeId, recipeTitle }: Props) {
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask a question…"
placeholder={t("inputPlaceholder")}
disabled={loading}
className="flex-1"
autoComplete="off"