diff --git a/apps/web/app/(app)/recipes/[id]/page.tsx b/apps/web/app/(app)/recipes/[id]/page.tsx
index 076ac3f..a7bff94 100644
--- a/apps/web/app/(app)/recipes/[id]/page.tsx
+++ b/apps/web/app/(app)/recipes/[id]/page.tsx
@@ -28,7 +28,7 @@ import { CommentsSection } from "@/components/social/comments-section";
import { getPublicUrl } from "@/lib/storage";
import { cn } from "@/lib/utils";
import { RecipeChatPanel } from "@/components/recipe/recipe-chat-panel";
-import { getMessages } from "@/lib/i18n/server";
+import { getMessages, formatMessage } from "@/lib/i18n/server";
type Params = { params: Promise<{ id: string }> };
@@ -175,26 +175,26 @@ export default async function RecipePage({ params }: Params) {
{recipe.difficulty && (
-
{recipe.difficulty}
+
{m.recipe.difficulty[recipe.difficulty]}
)}
- {recipe.baseServings} servings
+ {formatMessage(m.recipe.servings, { count: recipe.baseServings })}
{recipe.prepMins && (
- {recipe.prepMins}m prep
+ {formatMessage(m.recipe.prep, { mins: recipe.prepMins })}
)}
{recipe.cookMins && (
- {recipe.cookMins}m cook
+ {formatMessage(m.recipe.cook, { mins: recipe.cookMins })}
)}
{totalMins > 0 && recipe.prepMins && recipe.cookMins && (
-
({totalMins}m total)
+
({formatMessage(m.recipe.total, { mins: totalMins })})
)}
diff --git a/apps/web/app/api/v1/ai/generate-from-idea/route.ts b/apps/web/app/api/v1/ai/generate-from-idea/route.ts
index e3793ee..6c13e30 100644
--- a/apps/web/app/api/v1/ai/generate-from-idea/route.ts
+++ b/apps/web/app/api/v1/ai/generate-from-idea/route.ts
@@ -14,6 +14,8 @@ const Schema = z.object({
model: z.string().optional(),
});
+const LANG: Record = { en: "English", fr: "French" };
+
export async function POST(req: NextRequest) {
const { session, response } = await requireSession();
if (response) return response;
@@ -30,11 +32,13 @@ export async function POST(req: NextRequest) {
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const privateBio = await getUserPrivateBio(session!.user.id);
+ const locale = (session!.user as { locale?: string }).locale ?? "en";
const recipe = await generateRecipe(parsed.data.title, {
provider: parsed.data.provider,
model: parsed.data.model,
userContext: privateBio ?? undefined,
+ language: LANG[locale] ?? "English",
});
const recipeId = crypto.randomUUID();
@@ -50,7 +54,7 @@ export async function POST(req: NextRequest) {
difficulty: recipe.difficulty ?? null,
visibility: "private",
aiGenerated: true,
- language: "en",
+ language: locale,
dietaryTags: recipe.dietaryTags ?? {},
tags: [],
});
diff --git a/apps/web/app/api/v1/ai/recipe-chat/route.ts b/apps/web/app/api/v1/ai/recipe-chat/route.ts
index b60216a..7efd05c 100644
--- a/apps/web/app/api/v1/ai/recipe-chat/route.ts
+++ b/apps/web/app/api/v1/ai/recipe-chat/route.ts
@@ -14,6 +14,8 @@ const Schema = z.object({
question: z.string().min(1).max(500),
});
+const LANG: Record = { en: "English", fr: "French" };
+
export async function POST(req: NextRequest) {
const { session, response } = await requireSession();
if (response) return response;
@@ -66,10 +68,12 @@ ${stepList || "None listed"}
]);
const model = resolveModel(config);
const bioContext = buildUserBioContext(privateBio);
+ const locale = (session!.user as { locale?: string }).locale ?? "en";
+ const lang = LANG[locale] ?? "English";
const { text } = await generateText({
model,
- system: `You are a helpful culinary assistant. Answer questions about the following recipe concisely and accurately. If a question is not related to the recipe or cooking, politely redirect. Keep answers under 200 words.
+ system: `You are a helpful culinary assistant. Answer questions about the following recipe concisely and accurately. If a question is not related to the recipe or cooking, politely redirect. Keep answers under 200 words. Respond in ${lang}.
${recipeContext}${bioContext}`,
prompt: parsed.data.question,
diff --git a/apps/web/app/api/v1/ai/recipe-ideas/route.ts b/apps/web/app/api/v1/ai/recipe-ideas/route.ts
index ac688d5..6840063 100644
--- a/apps/web/app/api/v1/ai/recipe-ideas/route.ts
+++ b/apps/web/app/api/v1/ai/recipe-ideas/route.ts
@@ -11,6 +11,8 @@ const InputSchema = z.object({
prompt: z.string().max(300).optional(),
});
+const LANG: Record = { en: "English", fr: "French" };
+
const IdeasSchema = z.object({
ideas: z.array(z.object({
title: z.string(),
@@ -45,6 +47,9 @@ export async function POST(req: NextRequest) {
? `Consider the following user preferences when suggesting ideas:${bioContext}\n\n`
: "";
+ const locale = (session!.user as { locale?: string }).locale ?? "en";
+ const lang = LANG[locale] ?? "English";
+
const prompt = parsed.data.prompt?.trim()
? `${userContext}Generate 6 diverse recipe ideas based on: "${parsed.data.prompt}". Include a mix of difficulty levels.`
: `${userContext}Generate 6 diverse, creative recipe ideas. Include different cuisines, difficulty levels, and meal types.`;
@@ -52,6 +57,7 @@ export async function POST(req: NextRequest) {
const { object } = await generateObject({
model,
schema: IdeasSchema,
+ system: `Respond in ${lang}.`,
prompt,
});
diff --git a/apps/web/app/api/v1/ai/substitute/route.ts b/apps/web/app/api/v1/ai/substitute/route.ts
index 470fec0..b5f399c 100644
--- a/apps/web/app/api/v1/ai/substitute/route.ts
+++ b/apps/web/app/api/v1/ai/substitute/route.ts
@@ -41,10 +41,13 @@ export async function POST(req: NextRequest) {
? { provider: parsed.data.provider, model: parsed.data.model }
: await getDefaultProviderWithKey(session!.user.id);
+ const locale = (session!.user as { locale?: string }).locale ?? "en";
+
const substitutions = await substituteIngredient(
parsed.data.ingredient,
context,
- aiConfig
+ aiConfig,
+ locale
);
return NextResponse.json({ substitutions });
diff --git a/apps/web/components/meal-plan/meal-planner.tsx b/apps/web/components/meal-plan/meal-planner.tsx
index 847df2b..dfb7cdf 100644
--- a/apps/web/components/meal-plan/meal-planner.tsx
+++ b/apps/web/components/meal-plan/meal-planner.tsx
@@ -273,7 +273,7 @@ export function MealPlanner({
{entry?.recipe ? (
{entry.recipe.title}
-
{entry.servings} srv
+
{t("servingsAbbrev", { count: entry.servings })}
diff --git a/apps/web/components/recipe/recipes-grid.tsx b/apps/web/components/recipe/recipes-grid.tsx
index 0253500..176d452 100644
--- a/apps/web/components/recipe/recipes-grid.tsx
+++ b/apps/web/components/recipe/recipes-grid.tsx
@@ -45,6 +45,7 @@ function SelectableRecipeCard({
selectMode: boolean;
onToggle: (id: string) => void;
}) {
+ const t = useTranslations("recipe");
const cover = recipe.photos?.find((p) => p.isCover) ?? recipe.photos?.[0];
const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0);
const VisibilityIcon = VISIBILITY_ICON[recipe.visibility];
@@ -139,12 +140,12 @@ function SelectableRecipeCard({
{totalMins > 0 && (
- {totalMins}m
+ {t("total", { mins: totalMins })}
)}
{recipe.difficulty && (
- {recipe.difficulty}
+ {t(`difficulty.${recipe.difficulty}`)}
)}
diff --git a/apps/web/components/search/explore-page-content.tsx b/apps/web/components/search/explore-page-content.tsx
index d565b9e..a20bdf0 100644
--- a/apps/web/components/search/explore-page-content.tsx
+++ b/apps/web/components/search/explore-page-content.tsx
@@ -43,8 +43,12 @@ type RecipeIdea = {
};
function RecipeCard({ recipe }: { recipe: ExploreRecipeResult | SearchRecipeResult }) {
+ const t = useTranslations("recipe");
const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0);
const description = "description" in recipe ? recipe.description : null;
+ const difficultyLabel = recipe.difficulty === "easy" || recipe.difficulty === "medium" || recipe.difficulty === "hard"
+ ? t(`difficulty.${recipe.difficulty}`)
+ : recipe.difficulty;
return (
- {recipe.difficulty}
+ {difficultyLabel}
)}
@@ -70,7 +74,7 @@ function RecipeCard({ recipe }: { recipe: ExploreRecipeResult | SearchRecipeResu
{totalMins > 0 && (
- {totalMins}m
+ {t("total", { mins: totalMins })}
)}
{recipe.authorName && (
@@ -103,6 +107,7 @@ export function ExplorePageContent({ trending, recent, initialQuery }: Props) {
const searchParams = useSearchParams();
const t = useTranslations("explore");
const tCommon = useTranslations("common");
+ const tRecipe = useTranslations("recipe");
const [inputValue, setInputValue] = useState(initialQuery);
const [query, setQuery] = useState(initialQuery);
@@ -388,7 +393,7 @@ export function ExplorePageContent({ trending, recent, initialQuery }: Props) {
variant="secondary"
className={`shrink-0 capitalize text-xs ${DIFFICULTY_COLORS[idea.difficulty] ?? ""}`}
>
- {idea.difficulty}
+ {tRecipe(`difficulty.${idea.difficulty}`)}
{idea.description}
@@ -398,7 +403,7 @@ export function ExplorePageContent({ trending, recent, initialQuery }: Props) {
))}
{idea.totalMins && (
- {idea.totalMins}m
+ {tRecipe("total", { mins: idea.totalMins })}
)}
diff --git a/apps/web/components/search/search-page-content.tsx b/apps/web/components/search/search-page-content.tsx
index b35b492..1cacf4c 100644
--- a/apps/web/components/search/search-page-content.tsx
+++ b/apps/web/components/search/search-page-content.tsx
@@ -2,6 +2,7 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { useRouter, useSearchParams } from "next/navigation";
+import { useTranslations } from "next-intl";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -39,7 +40,11 @@ const DIFFICULTY_COLORS: Record = {
};
function RecipeCard({ recipe }: { recipe: RecipeResult }) {
+ const t = useTranslations("recipe");
const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0);
+ const difficultyLabel = recipe.difficulty === "easy" || recipe.difficulty === "medium" || recipe.difficulty === "hard"
+ ? t(`difficulty.${recipe.difficulty}`)
+ : recipe.difficulty;
return (
- {recipe.difficulty}
+ {difficultyLabel}
)}
@@ -65,7 +70,7 @@ function RecipeCard({ recipe }: { recipe: RecipeResult }) {
{totalMins > 0 && (
- {totalMins}m
+ {t("total", { mins: totalMins })}
)}
{recipe.authorName && (
diff --git a/apps/web/lib/ai/features/substitute-ingredient.ts b/apps/web/lib/ai/features/substitute-ingredient.ts
index 8eccc3f..49a4cf4 100644
--- a/apps/web/lib/ai/features/substitute-ingredient.ts
+++ b/apps/web/lib/ai/features/substitute-ingredient.ts
@@ -13,18 +13,22 @@ const SubstitutionsSchema = z.object({
export type Substitution = z.infer["substitutions"][number];
+const LANG: Record = { en: "English", fr: "French" };
+
export async function substituteIngredient(
ingredient: string,
recipeContext: string,
- config?: AiConfig
+ config?: AiConfig,
+ locale?: string
): Promise {
const model = resolveModel(config);
+ const lang = LANG[locale ?? "en"] ?? "English";
const { object } = await generateObject({
model,
schema: SubstitutionsSchema,
system:
- "You are a professional chef specializing in ingredient substitutions. Provide practical, commonly available substitutes. For each substitute: name what to use, the ratio (e.g. '1:1', '3/4 the amount'), and a brief note on technique adjustments. Rate the flavor impact honestly.",
+ `You are a professional chef specializing in ingredient substitutions. Provide practical, commonly available substitutes. For each substitute: name what to use, the ratio (e.g. '1:1', '3/4 the amount'), and a brief note on technique adjustments. Rate the flavor impact honestly. Respond in ${lang}.`,
prompt: `Suggest 3-4 substitutes for "${ingredient}" in this recipe context: ${recipeContext}`,
});
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 502cd21..6c91f8c 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -335,6 +335,7 @@
"addTo": "Add to {day} – {meal}",
"searchRecipes": "Search recipes…",
"servings": "Servings",
+ "servingsAbbrev": "{count} srv",
"difficulty": "Difficulty",
"noRecipes": "No recipes found",
"addFailed": "Failed to add",
diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json
index cc1053a..f34855d 100644
--- a/apps/web/messages/fr.json
+++ b/apps/web/messages/fr.json
@@ -323,6 +323,7 @@
"addTo": "Ajouter à {day} – {meal}",
"searchRecipes": "Rechercher des recettes…",
"servings": "Portions",
+ "servingsAbbrev": "{count} pers.",
"difficulty": "Difficulté",
"noRecipes": "Aucune recette trouvée",
"addFailed": "Échec de l'ajout",