Files
Arnaud 5afc7cd182 feat: shopping list rename/delete, item reorder+categories+search, ingredient-quantity parsing fix
- Fixed a real i18n bug: the checked-count line called the wrong
  translation namespace and rendered the literal key on screen
- Shopping lists can now be renamed and deleted from both the list index
  and detail pages (API already supported delete; rename was net new)
- Root-caused "long list UI is off": meal-plan-generated lists never set
  an aisle, so every item fell into one undifferentiated "Other" bucket
  despite the grouping UI existing. Added a keyword-based aisle guesser
  wired into list generation (fallback only, never overrides an explicit
  aisle) plus a one-click "auto-categorize" for existing lists
- Items can now be reordered by drag-and-drop within a category (dnd-kit),
  recategorized via a dropdown, deleted, searched, and sorted (category /
  alphabetical / unchecked-first); searching flattens the grouped view
- Fixed a separate bug: AI-generated ingredients sometimes embedded the
  quantity/unit in the name itself (e.g. "2 cups flour" as one string).
  Added extractIngredientQuantity() as a Zod transform at both recipe
  create/update routes (the choke point every creation path funnels
  through) to split it back out, plus schema descriptions on the AI
  ingredient schemas as a prevention layer

New migration 0028 (shopping_list_items.sort_order), left unapplied like
the others. Verified with typecheck, lint, and a clean --no-cache docker
build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 10:29:28 +02:00

41 lines
1.7 KiB
TypeScript

import { generateObject } from "ai";
import { z } from "zod";
import { resolveModel, type AiConfig } from "../factory";
const TranslationOutputSchema = z.object({
title: z.string(),
description: z.string(),
ingredients: z.array(z.object({
rawName: z.string().describe("Translated ingredient name only — never add the quantity or unit, those are handled separately and unaffected by translation."),
note: z.string().optional(),
})),
steps: z.array(z.object({
instruction: z.string(),
})),
});
export type TranslatedRecipe = z.infer<typeof TranslationOutputSchema>;
export async function translateRecipe(
recipe: {
title: string;
description?: string | null;
ingredients: Array<{ rawName: string; note?: string | null }>;
steps: Array<{ instruction: string }>;
},
targetLanguage: string,
config?: AiConfig
): Promise<TranslatedRecipe> {
const model = resolveModel(config);
const { object } = await generateObject({
model,
schema: TranslationOutputSchema,
system:
"You are a professional culinary translator. Translate recipes accurately, preserving culinary meaning, cooking terms, and cultural context. Keep ingredient quantities and units as-is (numbers and unit abbreviations are universal). Only translate text content.",
prompt: `Translate the following recipe into ${targetLanguage}. Return the same structure with translated text.\n\nTitle: ${recipe.title}\nDescription: ${recipe.description ?? ""}\nIngredients: ${recipe.ingredients.map((i) => `- ${i.rawName}${i.note ? ` (${i.note})` : ""}`).join("\n")}\nSteps: ${recipe.steps.map((s, i) => `${i + 1}. ${s.instruction}`).join("\n")}`,
});
return object;
}