diff --git a/apps/web/app/(app)/messages/page.tsx b/apps/web/app/(app)/messages/page.tsx index e9f2635..76fbb43 100644 --- a/apps/web/app/(app)/messages/page.tsx +++ b/apps/web/app/(app)/messages/page.tsx @@ -1,10 +1,16 @@ import type { Metadata } from "next"; +import { headers } from "next/headers"; import { MessageCircle } from "lucide-react"; +import { auth } from "@/lib/auth/server"; import { ConversationsList } from "@/components/social/conversations-list"; +import { getMessages } from "@/lib/i18n/server"; export const metadata: Metadata = { title: "Messages — Epicure" }; -export default function MessagesPage() { +export default async function MessagesPage() { + const session = await auth.api.getSession({ headers: await headers() }); + const m = getMessages((session?.user as { locale?: string } | undefined)?.locale); + return (
@@ -13,7 +19,7 @@ export default function MessagesPage() {
-

Select a conversation

+

{m.messages.selectConversation}

diff --git a/apps/web/app/(app)/people/page.tsx b/apps/web/app/(app)/people/page.tsx index 21900b2..034d768 100644 --- a/apps/web/app/(app)/people/page.tsx +++ b/apps/web/app/(app)/people/page.tsx @@ -1,14 +1,20 @@ import type { Metadata } from "next"; +import { headers } from "next/headers"; +import { auth } from "@/lib/auth/server"; import { PeopleSearch } from "@/components/social/people-search"; +import { getMessages } from "@/lib/i18n/server"; export const metadata: Metadata = { title: "Find People — Epicure" }; -export default function PeoplePage() { +export default async function PeoplePage() { + const session = await auth.api.getSession({ headers: await headers() }); + const m = getMessages((session?.user as { locale?: string } | undefined)?.locale); + return (
-

Find People

-

Search for cooks to follow by name or username.

+

{m.people.title}

+

{m.people.subtitle}

diff --git a/apps/web/components/recipe/drink-pairing-button.tsx b/apps/web/components/recipe/drink-pairing-button.tsx index 59e3564..96cadea 100644 --- a/apps/web/components/recipe/drink-pairing-button.tsx +++ b/apps/web/components/recipe/drink-pairing-button.tsx @@ -36,13 +36,13 @@ const TYPE_ICON: Record = { hot: Coffee, }; -const TYPE_LABEL: Record = { - wine: "Wine", - beer: "Beer", - cocktail: "Cocktail", - spirit: "Spirit", - "non-alcoholic": "Sans alcool", - hot: "Chaud", +const TYPE_LABEL_KEY: Record = { + wine: "drinksTypeWine", + beer: "drinksTypeBeer", + cocktail: "drinksTypeCocktail", + spirit: "drinksTypeSpirit", + "non-alcoholic": "drinksTypeNonAlcoholic", + hot: "drinksTypeHot", }; export function DrinkPairingButton({ recipeId }: { recipeId: string }) { @@ -61,7 +61,7 @@ export function DrinkPairingButton({ recipeId }: { recipeId: string }) { body: JSON.stringify({ count: 4 }), }); if (!res.ok) { - toast.error("Failed to suggest drinks"); + toast.error(t("pairingDrinkFailed")); return; } const data = await res.json() as { drinks: Drink[] }; @@ -94,22 +94,22 @@ export function DrinkPairingButton({ recipeId }: { recipeId: string }) { - Drink pairings + {t("drinksDialogTitle")} - AI-suggested drinks that complement this recipe. + {t("drinksDialogDescription")} {loading ? (
- +
) : drinks.length === 0 ? (
) : ( @@ -125,18 +125,18 @@ export function DrinkPairingButton({ recipeId }: { recipeId: string }) {
{drink.name} - {TYPE_LABEL[drink.type]} + {t(TYPE_LABEL_KEY[drink.type])} {!drink.alcoholic && ( - Sans alcool + {t("drinksTypeNonAlcoholic")} )}

{drink.description}

{drink.examples.length > 0 && (

- e.g. + {t("drinksExamplesLabel")} {drink.examples.join(" · ")}

)} @@ -153,7 +153,7 @@ export function DrinkPairingButton({ recipeId }: { recipeId: string }) {
)} diff --git a/apps/web/components/recipe/meal-pairing-button.tsx b/apps/web/components/recipe/meal-pairing-button.tsx index 1ce2d38..7c2d462 100644 --- a/apps/web/components/recipe/meal-pairing-button.tsx +++ b/apps/web/components/recipe/meal-pairing-button.tsx @@ -38,14 +38,14 @@ const ROLE_ICON: Record = { sauce: ChefHat, }; -const ROLE_LABEL: Record = { - starter: "Starter", - side: "Side", - salad: "Salad", - bread: "Bread", - drink: "Drink", - dessert: "Dessert", - sauce: "Sauce", +const ROLE_LABEL_KEY: Record = { + starter: "pairingRoleStarter", + side: "pairingRoleSide", + salad: "pairingRoleSalad", + bread: "pairingRoleBread", + drink: "pairingRoleDrink", + dessert: "pairingRoleDessert", + sauce: "pairingRoleSauce", }; const DIFFICULTY_VARIANT = { @@ -154,22 +154,22 @@ export function MealPairingButton({ recipeId }: { recipeId: string }) { - Complete the meal + {t("pairingDialogTitle")} - AI-suggested dishes that pair well with this recipe. Generate any of them as a new recipe. + {t("pairingDialogDescription")} {loading ? (
- +
) : pairings.length === 0 ? (
) : ( @@ -199,7 +199,7 @@ export function MealPairingButton({ recipeId }: { recipeId: string }) {
{pairing.name} - {ROLE_LABEL[pairing.role]} + {t(ROLE_LABEL_KEY[pairing.role])} {pairing.difficulty} {pairing.prepTimeMins && ( {pairing.prepTimeMins}m @@ -218,7 +218,7 @@ export function MealPairingButton({ recipeId }: { recipeId: string }) { )}
@@ -229,7 +229,7 @@ export function MealPairingButton({ recipeId }: { recipeId: string }) { disabled={!!generatingProgress} > - Regenerate + {t("pairingRegenerate")} diff --git a/apps/web/components/recipe/nutrition-panel.tsx b/apps/web/components/recipe/nutrition-panel.tsx index 4f942f9..e471e98 100644 --- a/apps/web/components/recipe/nutrition-panel.tsx +++ b/apps/web/components/recipe/nutrition-panel.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { useTranslations } from "next-intl"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -21,6 +22,7 @@ interface NutritionPanelProps { } export function NutritionPanel({ recipeId, initialData }: NutritionPanelProps) { + const t = useTranslations("nutritionPanel"); const [nutrition, setNutrition] = useState(initialData ?? null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -33,13 +35,12 @@ export function NutritionPanel({ recipeId, initialData }: NutritionPanelProps) { method: "POST", }); if (!res.ok) { - const body = await res.json() as { error?: string }; - throw new Error(body.error ?? "Failed to estimate nutrition"); + throw new Error(t("estimateFailed")); } const data = await res.json() as { nutrition: NutritionData }; setNutrition(data.nutrition); } catch (err) { - setError(err instanceof Error ? err.message : "Something went wrong"); + setError(err instanceof Error ? err.message : t("error")); } finally { setLoading(false); } @@ -50,7 +51,7 @@ export function NutritionPanel({ recipeId, initialData }: NutritionPanelProps) {
{error &&

{error}

}
); @@ -60,7 +61,7 @@ export function NutritionPanel({ recipeId, initialData }: NutritionPanelProps) {
- Nutrition per serving + {t("title")}
{error &&

{error}

} @@ -84,27 +85,27 @@ export function NutritionPanel({ recipeId, initialData }: NutritionPanelProps) {
@@ -114,7 +115,7 @@ export function NutritionPanel({ recipeId, initialData }: NutritionPanelProps) { {loading && !nutrition && (

- Estimating nutrition… + {t("estimatingNutrition")}

)} diff --git a/apps/web/components/recipe/serving-scaler.tsx b/apps/web/components/recipe/serving-scaler.tsx index d0c3c58..eccef4a 100644 --- a/apps/web/components/recipe/serving-scaler.tsx +++ b/apps/web/components/recipe/serving-scaler.tsx @@ -120,7 +120,7 @@ export function ServingScaler({ diff --git a/apps/web/components/recipe/url-import-dialog.tsx b/apps/web/components/recipe/url-import-dialog.tsx index 43b273c..fae9738 100644 --- a/apps/web/components/recipe/url-import-dialog.tsx +++ b/apps/web/components/recipe/url-import-dialog.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { Link2, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -28,6 +29,8 @@ export function UrlImportDialog({ open: boolean; onOpenChange: (open: boolean) => void; }) { + const t = useTranslations("recipe"); + const tCommon = useTranslations("common"); const router = useRouter(); const [url, setUrl] = useState(""); const [importing, setImporting] = useState(false); @@ -44,7 +47,7 @@ export function UrlImportDialog({ if (!res.ok) { const err = await res.json() as { error?: string }; - toast.error(err.error ?? "Failed to import recipe"); + toast.error(err.error ?? t("urlImportFetchFailed")); return; } @@ -61,12 +64,12 @@ export function UrlImportDialog({ }); if (!saveRes.ok) { - toast.error("Failed to save imported recipe"); + toast.error(t("urlImportSaveFailed")); return; } const saved = await saveRes.json() as { id: string }; - toast.success("Recipe imported! Review before publishing."); + toast.success(t("urlImportSuccess")); onOpenChange(false); router.push(`/recipes/${saved.id}/edit`); } finally { @@ -80,15 +83,15 @@ export function UrlImportDialog({ - Import recipe from URL + {t("urlImportTitle")} - Paste a recipe URL and AI will extract the ingredients and instructions. + {t("urlImportDescription")}
- +
diff --git a/apps/web/components/social/comment-reactions.tsx b/apps/web/components/social/comment-reactions.tsx index 79fe94f..229116f 100644 --- a/apps/web/components/social/comment-reactions.tsx +++ b/apps/web/components/social/comment-reactions.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; const REACTIONS: Record = { like: "👍", @@ -20,6 +21,7 @@ type Props = { }; export function CommentReactions({ recipeId, commentId, initialCounts = {}, initialUserReactions = [] }: Props) { + const t = useTranslations("social"); const [counts, setCounts] = useState>(initialCounts); const [userReactions, setUserReactions] = useState(initialUserReactions); const [pending, setPending] = useState(null); @@ -69,9 +71,9 @@ export function CommentReactions({ recipeId, commentId, initialCounts = {}, init [type]: Math.max(0, (prev[type] ?? 0) + (hasReacted ? 1 : -1)), })); if (res.status === 401) { - toast.error("Sign in to react to comments"); + toast.error(t("signInToReact")); } else { - toast.error("Failed to update reaction"); + toast.error(t("reactionFailed")); } return; } @@ -92,7 +94,7 @@ export function CommentReactions({ recipeId, commentId, initialCounts = {}, init ...prev, [type]: Math.max(0, (prev[type] ?? 0) + (hasReacted ? 1 : -1)), })); - toast.error("Failed to update reaction"); + toast.error(t("reactionFailed")); } finally { setPending(null); } @@ -115,7 +117,7 @@ export function CommentReactions({ recipeId, commentId, initialCounts = {}, init : "border-border bg-transparent text-muted-foreground hover:border-primary/50 hover:text-foreground", pending === type ? "opacity-60 cursor-not-allowed" : "cursor-pointer", ].join(" ")} - aria-label={`${reacted ? "Remove" : "Add"} ${type} reaction`} + aria-label={reacted ? t("removeReaction", { type }) : t("addReaction", { type })} aria-pressed={reacted} > {emoji} diff --git a/apps/web/components/social/comments-section.tsx b/apps/web/components/social/comments-section.tsx index f2d69d1..973f15a 100644 --- a/apps/web/components/social/comments-section.tsx +++ b/apps/web/components/social/comments-section.tsx @@ -45,21 +45,21 @@ function renderContentWithMentions(content: string) { ); } -function timeAgo(dateStr: string) { +function timeAgo(dateStr: string, t: ReturnType) { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); - if (mins < 1) return "just now"; - if (mins < 60) return `${mins}m ago`; + if (mins < 1) return t("justNow"); + if (mins < 60) return t("minutesAgo", { mins }); const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours}h ago`; - return `${Math.floor(hours / 24)}d ago`; + if (hours < 24) return t("hoursAgo", { hours }); + return t("daysAgo", { days: Math.floor(hours / 24) }); } function CommentForm({ recipeId, parentId, onSubmit, - placeholder = "Add a comment…", + placeholder, onCancel, }: { recipeId: string; @@ -68,6 +68,8 @@ function CommentForm({ placeholder?: string; onCancel?: () => void; }) { + const t = useTranslations("social"); + const tCommon = useTranslations("common"); const [content, setContent] = useState(""); const [submitting, setSubmitting] = useState(false); @@ -80,7 +82,7 @@ function CommentForm({ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: content.trim(), parentId }), }); - if (!res.ok) { toast.error("Failed to post comment"); return; } + if (!res.ok) { toast.error(t("commentFailed")); return; } setContent(""); onSubmit(); } finally { @@ -93,15 +95,15 @@ function CommentForm({