diff --git a/CHANGELOG.md b/CHANGELOG.md index f560b3d..ce68413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. +## 0.86.0 — 2026-07-24 21:30 + +### Added +- Ingredient-alias matching now covers ~137 common bilingual EN/FR ingredients across all grocery categories (was ~10) — produce, dairy/eggs, meat/seafood, bakery, frozen, pantry staples, spices/condiments, and beverages. + +### Fixed +- Ingredient name matching (pantry, can-cook, auto-deduct, shopping-list generation) now ignores accents as well as case — "café"/"Café"/"cafe" and "Épinard"/"epinard" all recognized as the same ingredient. + ## 0.85.0 — 2026-07-24 21:00 ### Added diff --git a/FEATURE_AUDIT.md b/FEATURE_AUDIT.md index 49b375f..5a075f4 100644 --- a/FEATURE_AUDIT.md +++ b/FEATURE_AUDIT.md @@ -71,7 +71,7 @@ Status legend: **Exists** (fully working) · **Partial** (works but with a real | Grocery delivery integration | **Partial / stub** | Generic export payload works (integrator-shaped — no visible in-app UI consumer besides the Instacart adapter); Instacart adapter is an explicit stub (requires a partnership agreement Epicure doesn't have — returns 501 if unconfigured, throws if "configured") | `apps/web/lib/grocery-providers/instacart.ts` | | Other delivery/price integrations (DoorDash, Kroger, Walmart, live pricing) | **Missing** | Confirmed absent by repo-wide search | — | | Pantry manual CRUD | Exists (extended 2026-07-24, twice) | Full edit dialog added (previously add+delete only, despite the API already supporting `PUT`) — name/quantity/unit/expiry plus **notes** (free text) and **category** (same `GROCERY_CATEGORIES` slugs shopping lists use; edit dialog's category dropdown initially displayed the raw slug/`__other__` instead of the translated label — fixed by passing a value→label render function to `SelectValue`, same pattern already used in `shopping-list-view.tsx`'s sort dropdown). List always renders all 9 category sections (8 `GROCERY_CATEGORIES` + Other) as collapsible groups, even empty ones — not just categories that currently have items — and items can be dragged between sections (dnd-kit `useDraggable`/`useDroppable`, cross-category drop only; no within-category manual reorder since pantry items have no `sortOrder` column to persist one). An "Auto-categorize" action (same `guessAisle` heuristic as the shopping list) fills in categories for uncategorized items in one click. Includes a bulk case-insensitive name+unit merge endpoint (used by the scan-confirm flow). | `apps/web/app/api/v1/pantry/**`, `apps/web/components/meal-plan/pantry-manager.tsx`, `apps/web/components/pantry/pantry-item-dialog.tsx` | -| Ingredient alias matching (new 2026-07-24) | Exists | The `ingredients` table (canonical name + `aliases[]`) existed but was never populated or queried anywhere. Seeded with ~10 bilingual EN/FR staples (salt, sugar, pepper, flour, butter, milk, egg, onion, garlic, olive oil — `packages/db/src/seed.ts`) and wired into every place that compares ingredient names by raw text: pantry add/edit (sets `ingredientId` when a name/alias matches), the can-cook / "use it up soon" scorer, auto-deduct-on-cook, and shopping-list pantry-awareness on generation. Resolution happens at compare-time from free text (`resolveIngredientKey`), not from a stored FK on both sides — recipe ingredients still don't carry `ingredientId`. | `apps/web/lib/ingredient-match.ts`, `packages/db/src/seed.ts` | +| Ingredient alias matching (new 2026-07-24, expanded same day) | Exists | The `ingredients` table (canonical name + `aliases[]`) existed but was never populated or queried anywhere. Seeded with ~137 bilingual EN/FR entries across all 8 grocery categories (produce, dairy/eggs, meat/seafood, bakery, frozen, pantry, spices/condiments, beverages — `packages/db/src/seed.ts`) and wired into every place that compares ingredient names by raw text: pantry add/edit (sets `ingredientId` when a name/alias matches), the can-cook / "use it up soon" scorer, auto-deduct-on-cook, and shopping-list pantry-awareness on generation. Comparison is case- **and accent-insensitive** (`café`/`Café`/`cafe` all equal — NFD-normalize + strip combining marks) in both the alias resolver and the older plain-name fallback matcher (`pantry-shopping-match.ts`'s `normalizeName`, used for the reliable-ingredientId-or-name-fallback path). Resolution happens at compare-time from free text (`resolveIngredientKey`), not from a stored FK on both sides — recipe ingredients still don't carry `ingredientId`. Growing the list: Admin → Ingredients (UI) or editing `packages/db/src/seed.ts` + `pnpm db:seed` (idempotent). | `apps/web/lib/ingredient-match.ts`, `packages/db/src/seed.ts` | | Admin: Ingredients CRUD (new 2026-07-24) | Exists | Admin-only page to manage canonical ingredients + aliases without a code change/redeploy — previously the seed file was the only place to add them. Create/edit/delete; name is unique (409 on collision); deleting an ingredient just unlinks any pantry item/recipe ingredient that pointed at it (`ingredientId` is `onDelete: "set null"` on both), nothing else is deleted. | `apps/web/app/admin/ingredients/page.tsx`, `apps/web/app/api/v1/admin/ingredients/**`, `apps/web/components/admin/ingredients-manager.tsx` | | Merge duplicate pantry items (new 2026-07-24, relaxed same day) | Exists | One-shot cleanup for pre-existing pantry rows that are the same ingredient under different names (added before alias matching existed) — groups by resolved ingredient key **alone** (unit is deliberately not part of the grouping key, so two rows merge even with mismatched, missing, or differently-unit'd quantities); sums quantities only when every row in a group has a parseable quantity **and** the same unit, otherwise keeps the first known (quantity, unit) pair rather than guessing. Concatenates notes, keeps the soonest expiry. Manual trigger (a button in the pantry toolbar), not automatic — manual single-item add still always inserts a new row rather than silently merging, since two batches of the same ingredient can have different expiry dates worth tracking separately. | `apps/web/app/api/v1/pantry/merge-duplicates/route.ts` | | Auto-deduct pantry on cook | Exists (closed 2026-07-24) | Both UI callers that previously hardcoded `deductFromPantry: false` (`batch-cook-dishes.tsx`, `meal-planner.tsx`) now pass `true`. The new general "Mark cooked" feature (see below) additionally exposes it as a per-cook checkbox, default checked, rather than a silent always-on. Matching now goes through the ingredient-alias resolver, not a raw name string compare. | `apps/web/app/api/v1/recipes/[id]/cooked/route.ts`, `apps/web/components/recipe/batch-cook-dishes.tsx`, `apps/web/components/meal-plan/meal-planner.tsx` | diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts index 7076998..f4d7e8a 100644 --- a/apps/web/lib/changelog.ts +++ b/apps/web/lib/changelog.ts @@ -1,5 +1,5 @@ // Mirrors CHANGELOG.md at the repo root — update both together. -export const APP_VERSION = "0.85.0"; +export const APP_VERSION = "0.86.0"; export type ChangelogEntry = { version: string; @@ -11,6 +11,16 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.86.0", + date: "2026-07-24 21:30", + added: [ + "Ingredient-alias matching now covers ~137 common bilingual EN/FR ingredients across all grocery categories (was ~10) — produce, dairy/eggs, meat/seafood, bakery, frozen, pantry staples, spices/condiments, and beverages.", + ], + fixed: [ + "Ingredient name matching (pantry, can-cook, auto-deduct, shopping-list generation) now ignores accents as well as case — \"café\"/\"Café\"/\"cafe\" and \"Épinard\"/\"epinard\" all recognized as the same ingredient.", + ], + }, { version: "0.85.0", date: "2026-07-24 21:00", diff --git a/apps/web/lib/ingredient-match.ts b/apps/web/lib/ingredient-match.ts index 202f9c0..b6d5bb4 100644 --- a/apps/web/lib/ingredient-match.ts +++ b/apps/web/lib/ingredient-match.ts @@ -1,14 +1,17 @@ -import { db, ingredients, sql } from "@epicure/db"; +import { db, ingredients } from "@epicure/db"; export type IngredientAliasIndex = Map; +// Case- and accent-insensitive so "café"/"cafe" and "Épinard"/"epinard" +// compare equal — NFD splits an accented letter into base letter + +// combining mark, then the combining marks (U+0300-U+036F) are stripped. function normalize(name: string): string { - return name.trim().toLowerCase(); + return name.trim().toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, ""); } /** * Loads every canonical ingredient's name + aliases into a flat - * lowercased-string -> canonical-ingredient-id map, once per request. Used + * normalized-string -> canonical-ingredient-id map, once per request. Used * to recognize that "sel", "sel fin", and "table salt" are all the same * ingredient, without requiring every recipe/pantry row to carry a stored * ingredientId (they don't — this resolves purely from the free-text name @@ -27,25 +30,24 @@ export async function loadIngredientAliasIndex(): Promise } /** Canonical ingredient id if `rawName` matches a known name/alias exactly - * (case/whitespace-insensitive); otherwise the normalized rawName itself, - * so unmatched items still compare equal to other unmatched items with the - * exact same text (today's behavior, unchanged for anything not seeded). */ + * (case/accent/whitespace-insensitive); otherwise the normalized rawName + * itself, so unmatched items still compare equal to other unmatched items + * with the same text (today's behavior, unchanged for anything not seeded). */ export function resolveIngredientKey(rawName: string, index: IngredientAliasIndex): string { const normalized = normalize(rawName); return index.get(normalized) ?? normalized; } -/** Single-name lookup (pantry add/edit) — a direct query rather than - * loading the whole table, since this runs once per add/rename rather than - * in a loop. Returns null when there's no canonical match, meaning the item - * stays a plain freeform pantry entry. */ +/** Single-name lookup (pantry add/edit). Loads the same small table as + * loadIngredientAliasIndex and compares in JS rather than in SQL — accent + * stripping via NFD has no simple SQL equivalent without the `unaccent` + * extension, which isn't guaranteed to be installed. The ingredients table + * is small (tens to low hundreds of rows), so this is cheap. Returns null + * when there's no canonical match, meaning the item stays a plain freeform + * pantry entry. */ export async function findIngredientIdByName(rawName: string): Promise { const normalized = normalize(rawName); if (!normalized) return null; - const [match] = await db - .select({ id: ingredients.id }) - .from(ingredients) - .where(sql`lower(${ingredients.name}) = ${normalized} or exists (select 1 from unnest(${ingredients.aliases}) a where lower(a) = ${normalized})`) - .limit(1); - return match?.id ?? null; + const index = await loadIngredientAliasIndex(); + return index.get(normalized) ?? null; } diff --git a/apps/web/lib/pantry-match.ts b/apps/web/lib/pantry-match.ts index b909e21..a3f7909 100644 --- a/apps/web/lib/pantry-match.ts +++ b/apps/web/lib/pantry-match.ts @@ -15,9 +15,10 @@ export function scoreRecipesAgainstPantry( pantry: { rawName: string; expiresAt: Date | null }[], aliasIndex?: IngredientAliasIndex ) { - // With no alias index, this resolves to a plain lowercase compare — - // same behavior as before aliases existed. - const keyOf = (name: string) => (aliasIndex ? resolveIngredientKey(name, aliasIndex) : name.trim().toLowerCase()); + // With no alias index, this resolves to a plain case/accent-insensitive + // compare — same behavior as before aliases existed, just accent-aware. + const keyOf = (name: string) => + aliasIndex ? resolveIngredientKey(name, aliasIndex) : name.trim().toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, ""); const pantryKeys = new Set(pantry.map((p) => keyOf(p.rawName))); const expiringSoonKeys = new Set( diff --git a/apps/web/lib/pantry-shopping-match.ts b/apps/web/lib/pantry-shopping-match.ts index 8e86823..97423e3 100644 --- a/apps/web/lib/pantry-shopping-match.ts +++ b/apps/web/lib/pantry-shopping-match.ts @@ -37,7 +37,10 @@ export type PantryAdjustedItem = { }; export function normalizeName(name: string): string { - const trimmed = name.toLowerCase().trim().replace(/\s+/g, " "); + // Accent-insensitive too ("café"/"cafe") — NFD splits an accented letter + // into base letter + combining mark, then the marks (U+0300-U+036F) are + // stripped. + const trimmed = name.toLowerCase().trim().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/\s+/g, " "); // Simple plural trim — strip a single trailing "s" for words longer than 3 chars. return trimmed.length > 3 && trimmed.endsWith("s") ? trimmed.slice(0, -1) : trimmed; } diff --git a/apps/web/package.json b/apps/web/package.json index 3b5c176..576e6b7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@epicure/web", - "version": "0.85.0", + "version": "0.86.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package.json b/package.json index f04dac9..dcaff11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epicure", - "version": "0.85.0", + "version": "0.86.0", "private": true, "scripts": { "dev": "pnpm --filter web dev", diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index dd4b070..b5ae418 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -1,22 +1,165 @@ import { db } from "./client"; import { tierDefinitions, ingredients } from "./schema"; -// Starter set of canonical ingredients + common EN/FR synonyms, so pantry -// items and recipe ingredients written differently ("sel", "sel fin", "sel -// de table", "table salt") can still be recognized as the same thing (see -// lib/ingredient-match.ts). Deliberately small — grows over time as gaps -// are found, not meant to be exhaustive on day one. +// Canonical ingredients + common EN/FR synonyms, so pantry items and recipe +// ingredients written differently ("sel", "sel fin", "sel de table", "table +// salt") can still be recognized as the same thing (see +// lib/ingredient-match.ts). Broad everyday-cooking coverage, not exhaustive — +// grows over time via Admin -> Ingredients or by editing this list (safe to +// add more and re-run; onConflictDoNothing on name). const STAPLE_INGREDIENTS: { name: string; aliases: string[]; category: string }[] = [ - { name: "salt", aliases: ["sel", "sel fin", "sel de table", "table salt", "fine salt", "sea salt", "sel de mer"], category: "spicesCondiments" }, - { name: "sugar", aliases: ["sucre", "sucre blanc", "white sugar", "granulated sugar", "sucre en poudre"], category: "pantry" }, - { name: "black pepper", aliases: ["pepper", "poivre", "poivre noir", "ground pepper", "poivre moulu"], category: "spicesCondiments" }, - { name: "flour", aliases: ["farine", "all-purpose flour", "farine de blé", "plain flour", "wheat flour"], category: "pantry" }, - { name: "butter", aliases: ["beurre", "unsalted butter", "beurre doux", "salted butter", "beurre demi-sel"], category: "dairyEggs" }, - { name: "milk", aliases: ["lait", "whole milk", "lait entier", "lait demi-écrémé"], category: "dairyEggs" }, - { name: "egg", aliases: ["eggs", "œuf", "oeuf", "œufs", "oeufs"], category: "dairyEggs" }, - { name: "onion", aliases: ["oignon", "oignons", "yellow onion", "onions"], category: "produce" }, - { name: "garlic", aliases: ["ail", "garlic clove", "gousse d'ail", "garlic cloves"], category: "produce" }, - { name: "olive oil", aliases: ["huile d'olive", "extra virgin olive oil", "huile d'olive vierge extra"], category: "pantry" }, + // Produce + { name: "onion", aliases: ["onions", "yellow onion", "oignon", "oignons"], category: "produce" }, + { name: "red onion", aliases: ["red onions", "oignon rouge", "oignons rouges"], category: "produce" }, + { name: "shallot", aliases: ["shallots", "échalote", "échalotes", "echalote", "echalotes"], category: "produce" }, + { name: "scallion", aliases: ["scallions", "green onion", "green onions", "spring onion", "oignon vert", "oignons verts", "ciboule"], category: "produce" }, + { name: "garlic", aliases: ["garlic clove", "garlic cloves", "ail", "gousse d'ail", "gousses d'ail"], category: "produce" }, + { name: "ginger", aliases: ["fresh ginger", "gingembre"], category: "produce" }, + { name: "carrot", aliases: ["carrots", "carotte", "carottes"], category: "produce" }, + { name: "celery", aliases: ["celery stalk", "celery stalks", "céleri", "celeri", "branche de céleri"], category: "produce" }, + { name: "potato", aliases: ["potatoes", "pomme de terre", "pommes de terre"], category: "produce" }, + { name: "sweet potato", aliases: ["sweet potatoes", "patate douce", "patates douces"], category: "produce" }, + { name: "tomato", aliases: ["tomatoes", "tomate", "tomates"], category: "produce" }, + { name: "cherry tomato", aliases: ["cherry tomatoes", "tomate cerise", "tomates cerises"], category: "produce" }, + { name: "cucumber", aliases: ["cucumbers", "concombre", "concombres"], category: "produce" }, + { name: "zucchini", aliases: ["zucchinis", "courgette", "courgettes"], category: "produce" }, + { name: "bell pepper", aliases: ["bell peppers", "poivron", "poivrons", "poivron rouge", "poivron vert"], category: "produce" }, + { name: "chili pepper", aliases: ["chili", "chile", "chilli", "piment", "piments"], category: "produce" }, + { name: "broccoli", aliases: ["brocoli"], category: "produce" }, + { name: "cauliflower", aliases: ["chou-fleur", "chou fleur"], category: "produce" }, + { name: "spinach", aliases: ["épinard", "épinards", "epinard", "epinards"], category: "produce" }, + { name: "lettuce", aliases: ["laitue"], category: "produce" }, + { name: "kale", aliases: ["chou kale"], category: "produce" }, + { name: "cabbage", aliases: ["chou"], category: "produce" }, + { name: "mushroom", aliases: ["mushrooms", "champignon", "champignons"], category: "produce" }, + { name: "avocado", aliases: ["avocados", "avocat", "avocats"], category: "produce" }, + { name: "lemon", aliases: ["lemons", "citron", "citrons"], category: "produce" }, + { name: "lime", aliases: ["limes", "citron vert", "citrons verts"], category: "produce" }, + { name: "orange", aliases: ["oranges", "orange fruit"], category: "produce" }, + { name: "apple", aliases: ["apples", "pomme", "pommes"], category: "produce" }, + { name: "banana", aliases: ["bananas", "banane", "bananes"], category: "produce" }, + { name: "leek", aliases: ["leeks", "poireau", "poireaux"], category: "produce" }, + { name: "fresh basil", aliases: ["basil", "basilic"], category: "produce" }, + { name: "fresh parsley", aliases: ["parsley", "persil"], category: "produce" }, + { name: "fresh cilantro", aliases: ["cilantro", "coriander leaves", "coriandre fraîche", "coriandre"], category: "produce" }, + { name: "fresh mint", aliases: ["mint", "menthe"], category: "produce" }, + { name: "fresh thyme", aliases: ["thyme", "thym"], category: "produce" }, + { name: "fresh rosemary", aliases: ["rosemary", "romarin"], category: "produce" }, + + // Dairy & eggs + { name: "butter", aliases: ["unsalted butter", "salted butter", "beurre", "beurre doux", "beurre demi-sel"], category: "dairyEggs" }, + { name: "milk", aliases: ["whole milk", "lait", "lait entier", "lait demi-écrémé"], category: "dairyEggs" }, + { name: "egg", aliases: ["eggs", "œuf", "œufs", "oeuf", "oeufs"], category: "dairyEggs" }, + { name: "heavy cream", aliases: ["whipping cream", "crème fraîche épaisse", "crème entière", "creme fraiche"], category: "dairyEggs" }, + { name: "sour cream", aliases: ["crème aigre"], category: "dairyEggs" }, + { name: "plain yogurt", aliases: ["yogurt", "yoghurt", "yaourt", "yaourt nature"], category: "dairyEggs" }, + { name: "cheddar cheese", aliases: ["cheddar", "cheddar cheese grated"], category: "dairyEggs" }, + { name: "parmesan cheese", aliases: ["parmesan", "parmigiano"], category: "dairyEggs" }, + { name: "mozzarella cheese", aliases: ["mozzarella"], category: "dairyEggs" }, + { name: "feta cheese", aliases: ["feta"], category: "dairyEggs" }, + { name: "goat cheese", aliases: ["fromage de chèvre", "chevre"], category: "dairyEggs" }, + { name: "cream cheese", aliases: ["fromage à tartiner", "philadelphia"], category: "dairyEggs" }, + { name: "ricotta cheese", aliases: ["ricotta"], category: "dairyEggs" }, + + // Meat & seafood + { name: "chicken breast", aliases: ["chicken breasts", "blanc de poulet", "escalope de poulet"], category: "meatSeafood" }, + { name: "chicken thigh", aliases: ["chicken thighs", "cuisse de poulet", "cuisses de poulet"], category: "meatSeafood" }, + { name: "whole chicken", aliases: ["poulet entier", "poulet"], category: "meatSeafood" }, + { name: "ground beef", aliases: ["minced beef", "bœuf haché", "boeuf hache", "viande hachée"], category: "meatSeafood" }, + { name: "beef steak", aliases: ["steak", "steak de bœuf", "bifteck"], category: "meatSeafood" }, + { name: "ground pork", aliases: ["minced pork", "porc haché"], category: "meatSeafood" }, + { name: "pork chop", aliases: ["pork chops", "côte de porc", "côtes de porc"], category: "meatSeafood" }, + { name: "bacon", aliases: ["lardons", "lardon"], category: "meatSeafood" }, + { name: "sausage", aliases: ["sausages", "saucisse", "saucisses"], category: "meatSeafood" }, + { name: "ham", aliases: ["jambon"], category: "meatSeafood" }, + { name: "salmon", aliases: ["saumon"], category: "meatSeafood" }, + { name: "tuna", aliases: ["thon"], category: "meatSeafood" }, + { name: "shrimp", aliases: ["prawns", "crevette", "crevettes"], category: "meatSeafood" }, + { name: "cod", aliases: ["morue", "cabillaud"], category: "meatSeafood" }, + { name: "mussels", aliases: ["moule", "moules"], category: "meatSeafood" }, + { name: "tofu", aliases: ["tofu firme"], category: "meatSeafood" }, + + // Bakery + { name: "bread", aliases: ["pain"], category: "bakery" }, + { name: "baguette", aliases: ["french baguette"], category: "bakery" }, + { name: "tortilla", aliases: ["tortillas", "wrap", "wraps"], category: "bakery" }, + { name: "pita bread", aliases: ["pita", "pain pita"], category: "bakery" }, + { name: "breadcrumbs", aliases: ["bread crumbs", "chapelure", "panko"], category: "bakery" }, + { name: "puff pastry", aliases: ["pâte feuilletée", "pate feuilletee"], category: "bakery" }, + { name: "pie crust", aliases: ["pâte brisée", "pate brisee"], category: "bakery" }, + + // Frozen + { name: "frozen peas", aliases: ["peas", "petits pois", "petits pois surgelés"], category: "frozen" }, + { name: "frozen corn", aliases: ["corn", "maïs", "mais", "maïs surgelé"], category: "frozen" }, + { name: "ice cream", aliases: ["glace"], category: "frozen" }, + + // Pantry + { name: "sugar", aliases: ["white sugar", "granulated sugar", "sucre", "sucre blanc", "sucre en poudre"], category: "pantry" }, + { name: "brown sugar", aliases: ["sucre roux", "sucre brun", "cassonade"], category: "pantry" }, + { name: "flour", aliases: ["all-purpose flour", "plain flour", "wheat flour", "farine", "farine de blé"], category: "pantry" }, + { name: "olive oil", aliases: ["extra virgin olive oil", "huile d'olive", "huile d'olive vierge extra"], category: "pantry" }, + { name: "vegetable oil", aliases: ["cooking oil", "sunflower oil", "huile végétale", "huile de tournesol"], category: "pantry" }, + { name: "rice", aliases: ["white rice", "riz", "riz blanc"], category: "pantry" }, + { name: "pasta", aliases: ["spaghetti", "penne", "pâtes", "pates"], category: "pantry" }, + { name: "couscous", aliases: ["couscous grains"], category: "pantry" }, + { name: "quinoa", aliases: ["quinoa grains"], category: "pantry" }, + { name: "lentils", aliases: ["lentil", "lentilles", "lentille"], category: "pantry" }, + { name: "chickpeas", aliases: ["chickpea", "garbanzo beans", "pois chiches", "pois chiche"], category: "pantry" }, + { name: "black beans", aliases: ["haricots noirs"], category: "pantry" }, + { name: "kidney beans", aliases: ["red beans", "haricots rouges"], category: "pantry" }, + { name: "canned tomatoes", aliases: ["diced tomatoes", "tomates en conserve", "tomates concassées"], category: "pantry" }, + { name: "tomato paste", aliases: ["concentré de tomate", "concentre de tomate"], category: "pantry" }, + { name: "tomato sauce", aliases: ["sauce tomate", "coulis de tomate"], category: "pantry" }, + { name: "vegetable stock", aliases: ["vegetable broth", "bouillon de légumes", "bouillon legumes"], category: "pantry" }, + { name: "chicken stock", aliases: ["chicken broth", "bouillon de poulet"], category: "pantry" }, + { name: "beef stock", aliases: ["beef broth", "bouillon de bœuf", "bouillon de boeuf"], category: "pantry" }, + { name: "baking powder", aliases: ["levure chimique"], category: "pantry" }, + { name: "baking soda", aliases: ["bicarbonate of soda", "bicarbonate de soude", "bicarbonate"], category: "pantry" }, + { name: "yeast", aliases: ["active dry yeast", "levure de boulanger", "levure boulangère"], category: "pantry" }, + { name: "vanilla extract", aliases: ["vanilla", "extrait de vanille", "vanille"], category: "pantry" }, + { name: "honey", aliases: ["miel"], category: "pantry" }, + { name: "maple syrup", aliases: ["sirop d'érable", "sirop derable"], category: "pantry" }, + { name: "peanut butter", aliases: ["beurre de cacahuète", "beurre de cacahuete"], category: "pantry" }, + { name: "coconut milk", aliases: ["lait de coco"], category: "pantry" }, + { name: "chocolate chips", aliases: ["pépites de chocolat", "pepites de chocolat"], category: "pantry" }, + { name: "dark chocolate", aliases: ["chocolat noir"], category: "pantry" }, + { name: "cornstarch", aliases: ["corn starch", "cornflour", "maïzena", "maizena", "fécule de maïs"], category: "pantry" }, + { name: "soy sauce", aliases: ["sauce soja", "sauce soya"], category: "pantry" }, + { name: "vinegar", aliases: ["white vinegar", "vinaigre", "vinaigre blanc"], category: "pantry" }, + { name: "balsamic vinegar", aliases: ["vinaigre balsamique"], category: "pantry" }, + { name: "almonds", aliases: ["almond", "amande", "amandes"], category: "pantry" }, + { name: "walnuts", aliases: ["walnut", "noix"], category: "pantry" }, + { name: "raisins", aliases: ["raisin", "raisins secs"], category: "pantry" }, + + // Spices & condiments + { name: "salt", aliases: ["table salt", "fine salt", "sea salt", "sel", "sel fin", "sel de table", "sel de mer"], category: "spicesCondiments" }, + { name: "black pepper", aliases: ["pepper", "ground pepper", "poivre", "poivre noir", "poivre moulu"], category: "spicesCondiments" }, + { name: "paprika", aliases: ["paprika doux"], category: "spicesCondiments" }, + { name: "cumin", aliases: ["ground cumin", "cumin moulu", "cumin poudre"], category: "spicesCondiments" }, + { name: "cinnamon", aliases: ["ground cinnamon", "cannelle", "cannelle moulue"], category: "spicesCondiments" }, + { name: "nutmeg", aliases: ["muscade", "noix de muscade"], category: "spicesCondiments" }, + { name: "chili powder", aliases: ["piment en poudre", "poudre de piment"], category: "spicesCondiments" }, + { name: "cayenne pepper", aliases: ["cayenne", "poivre de cayenne"], category: "spicesCondiments" }, + { name: "turmeric", aliases: ["curcuma"], category: "spicesCondiments" }, + { name: "curry powder", aliases: ["curry", "poudre de curry"], category: "spicesCondiments" }, + { name: "bay leaf", aliases: ["bay leaves", "laurier", "feuille de laurier"], category: "spicesCondiments" }, + { name: "oregano", aliases: ["origan"], category: "spicesCondiments" }, + { name: "dried thyme", aliases: ["thym séché", "thym sec"], category: "spicesCondiments" }, + { name: "dried basil", aliases: ["basilic séché"], category: "spicesCondiments" }, + { name: "mustard", aliases: ["dijon mustard", "moutarde", "moutarde de dijon"], category: "spicesCondiments" }, + { name: "ketchup", aliases: ["tomato ketchup", "ketchup tomate"], category: "spicesCondiments" }, + { name: "mayonnaise", aliases: ["mayo", "mayonnaise sauce"], category: "spicesCondiments" }, + { name: "hot sauce", aliases: ["sauce piquante"], category: "spicesCondiments" }, + { name: "garlic powder", aliases: ["ail en poudre", "poudre d'ail"], category: "spicesCondiments" }, + { name: "onion powder", aliases: ["oignon en poudre", "poudre d'oignon"], category: "spicesCondiments" }, + + // Beverages + { name: "coffee", aliases: ["ground coffee", "café", "cafe"], category: "beverages" }, + { name: "tea", aliases: ["thé", "the"], category: "beverages" }, + { name: "orange juice", aliases: ["jus d'orange", "jus dorange"], category: "beverages" }, + { name: "red wine", aliases: ["vin rouge"], category: "beverages" }, + { name: "white wine", aliases: ["vin blanc"], category: "beverages" }, + { name: "sparkling water", aliases: ["eau gazeuse", "eau pétillante"], category: "beverages" }, ]; async function seed() {