fix(i18n): translate remaining recipe card/detail strings, localize AI responses
Recipe cards, the recipe detail page, and meal planner still rendered raw
"{n} servings"/"{n}m cook"/"{n} srv" text and untranslated difficulty labels
outside the earlier i18n pass. Also wires the user's locale into AI features
that previously always answered in English regardless of app language:
recipe chat, ingredient substitution, recipe ideas, and generate-from-idea.
The recipe-language picker in the AI generate dialog now defaults to the
user's locale instead of always defaulting to English.
This commit is contained in:
@@ -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) {
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||
{recipe.difficulty && (
|
||||
<Badge variant={DIFFICULTY_COLOR[recipe.difficulty]}>{recipe.difficulty}</Badge>
|
||||
<Badge variant={DIFFICULTY_COLOR[recipe.difficulty]}>{m.recipe.difficulty[recipe.difficulty]}</Badge>
|
||||
)}
|
||||
<span className="flex items-center gap-1 text-muted-foreground">
|
||||
<Users className="h-4 w-4" />
|
||||
{recipe.baseServings} servings
|
||||
{formatMessage(m.recipe.servings, { count: recipe.baseServings })}
|
||||
</span>
|
||||
{recipe.prepMins && (
|
||||
<span className="flex items-center gap-1 text-muted-foreground">
|
||||
<Clock className="h-4 w-4" />
|
||||
{recipe.prepMins}m prep
|
||||
{formatMessage(m.recipe.prep, { mins: recipe.prepMins })}
|
||||
</span>
|
||||
)}
|
||||
{recipe.cookMins && (
|
||||
<span className="flex items-center gap-1 text-muted-foreground">
|
||||
<ChefHat className="h-4 w-4" />
|
||||
{recipe.cookMins}m cook
|
||||
{formatMessage(m.recipe.cook, { mins: recipe.cookMins })}
|
||||
</span>
|
||||
)}
|
||||
{totalMins > 0 && recipe.prepMins && recipe.cookMins && (
|
||||
<span className="text-muted-foreground">({totalMins}m total)</span>
|
||||
<span className="text-muted-foreground">({formatMessage(m.recipe.total, { mins: totalMins })})</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1 text-muted-foreground ml-auto">
|
||||
<VisibilityIcon className="h-4 w-4" />
|
||||
|
||||
@@ -14,6 +14,8 @@ const Schema = z.object({
|
||||
model: z.string().optional(),
|
||||
});
|
||||
|
||||
const LANG: Record<string, string> = { 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: [],
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@ const Schema = z.object({
|
||||
question: z.string().min(1).max(500),
|
||||
});
|
||||
|
||||
const LANG: Record<string, string> = { 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,
|
||||
|
||||
@@ -11,6 +11,8 @@ const InputSchema = z.object({
|
||||
prompt: z.string().max(300).optional(),
|
||||
});
|
||||
|
||||
const LANG: Record<string, string> = { 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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user