diff --git a/apps/api/prisma/migrations/20260819165510_ingredient_reproducible_flag/migration.sql b/apps/api/prisma/migrations/20260819165510_ingredient_reproducible_flag/migration.sql new file mode 100644 index 0000000..1e85df8 --- /dev/null +++ b/apps/api/prisma/migrations/20260819165510_ingredient_reproducible_flag/migration.sql @@ -0,0 +1,9 @@ +-- Replaces the never-wired-up `Ingredient.alternateRecipeId` FK (zero +-- usage anywhere outside schema.prisma — confirmed by repo-wide grep) +-- with a plain boolean flag: whether this ingredient is reasonably +-- makeable at home. Product decision: no ingredient↔recipe linking in +-- the database — the recipe form only nudges the author toward the +-- recipe catalog's own search, pre-filled with the ingredient's name. +ALTER TABLE "ingredients" DROP CONSTRAINT "ingredients_alternate_recipe_fkey"; +ALTER TABLE "ingredients" DROP COLUMN "alternate_recipe"; +ALTER TABLE "ingredients" ADD COLUMN "reproducible" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/api/prisma/migrations/20260819180000_catalog_camel_case_uids/migration.sql b/apps/api/prisma/migrations/20260819180000_catalog_camel_case_uids/migration.sql new file mode 100644 index 0000000..3c81d3d --- /dev/null +++ b/apps/api/prisma/migrations/20260819180000_catalog_camel_case_uids/migration.sql @@ -0,0 +1,294 @@ +-- Replaces every Diet/Category(allergen)/Ingredient `key` with a +-- directly-authored English camelCase uid (no more French label + +-- separate catalog-en-keys.ts lookup table — see reference-seed-data.ts's +-- module doc comment). Auto-generated once by +-- scripts/gen-camel-uid-migration.ts — do not re-run, do not hand-edit. +-- Same shape as 20260818193000_catalog_keys_to_english/migration.sql. + +UPDATE "diet" SET "key" = 'glutenFree' WHERE "key" = 'gluten_free'; +UPDATE "category" SET "key" = 'treeNuts' WHERE "key" = 'tree_nuts'; +UPDATE "category" SET "key" = 'sesameSeeds' WHERE "key" = 'sesame_seeds'; +-- "sesame_seeds" is also an Ingredient key (the ingredient "Graines de +-- sésame" carries an allergen of the same name on itself) — hand-added, +-- the generator's first pass missed this dual-table case (see its +-- updated `if`/`if`/`if` — not `if`/`else if` — comment for why). +UPDATE "ingredients" SET "key" = 'sesameSeeds' WHERE "key" = 'sesame_seeds'; +UPDATE "ingredients" SET "key" = 'bellPepper' WHERE "key" = 'bell_pepper'; +UPDATE "ingredients" SET "key" = 'whiteCabbage' WHERE "key" = 'white_cabbage'; +UPDATE "ingredients" SET "key" = 'redCabbage' WHERE "key" = 'red_cabbage'; +UPDATE "ingredients" SET "key" = 'brusselsSprouts' WHERE "key" = 'brussels_sprouts'; +UPDATE "ingredients" SET "key" = 'swissChard' WHERE "key" = 'swiss_chard'; +UPDATE "ingredients" SET "key" = 'greenBean' WHERE "key" = 'green_bean'; +UPDATE "ingredients" SET "key" = 'butternutSquash' WHERE "key" = 'butternut_squash'; +UPDATE "ingredients" SET "key" = 'sweetPotato' WHERE "key" = 'sweet_potato'; +UPDATE "ingredients" SET "key" = 'cherryTomato' WHERE "key" = 'cherry_tomato'; +UPDATE "ingredients" SET "key" = 'bokChoy' WHERE "key" = 'bok_choy'; +UPDATE "ingredients" SET "key" = 'soybeanSprouts' WHERE "key" = 'soybean_sprouts'; +UPDATE "ingredients" SET "key" = 'freshGreenChili' WHERE "key" = 'fresh_green_chili'; +UPDATE "ingredients" SET "key" = 'napaCabbage' WHERE "key" = 'napa_cabbage'; +UPDATE "ingredients" SET "key" = 'springOnion' WHERE "key" = 'spring_onion'; +UPDATE "ingredients" SET "key" = 'redKuriSquash' WHERE "key" = 'red_kuri_squash'; +UPDATE "ingredients" SET "key" = 'lambsLettuce' WHERE "key" = 'lambs_lettuce'; +UPDATE "ingredients" SET "key" = 'bayLeaf' WHERE "key" = 'bay_leaf'; +UPDATE "ingredients" SET "key" = 'freshCilantro' WHERE "key" = 'fresh_cilantro'; +UPDATE "ingredients" SET "key" = 'kaffirLime' WHERE "key" = 'kaffir_lime'; +UPDATE "ingredients" SET "key" = 'groundBeef' WHERE "key" = 'ground_beef'; +UPDATE "ingredients" SET "key" = 'beefSteak' WHERE "key" = 'beef_steak'; +UPDATE "ingredients" SET "key" = 'beefRoast' WHERE "key" = 'beef_roast'; +UPDATE "ingredients" SET "key" = 'vealCutlet' WHERE "key" = 'veal_cutlet'; +UPDATE "ingredients" SET "key" = 'porkTenderloin' WHERE "key" = 'pork_tenderloin'; +UPDATE "ingredients" SET "key" = 'porkChop' WHERE "key" = 'pork_chop'; +UPDATE "ingredients" SET "key" = 'legOfLamb' WHERE "key" = 'leg_of_lamb'; +UPDATE "ingredients" SET "key" = 'baconLardons' WHERE "key" = 'bacon_lardons'; +UPDATE "ingredients" SET "key" = 'curedHam' WHERE "key" = 'cured_ham'; +UPDATE "ingredients" SET "key" = 'whitePudding' WHERE "key" = 'white_pudding'; +UPDATE "ingredients" SET "key" = 'blackPudding' WHERE "key" = 'black_pudding'; +UPDATE "ingredients" SET "key" = 'dryCuredSausage' WHERE "key" = 'dry_cured_sausage'; +UPDATE "ingredients" SET "key" = 'bayonneHam' WHERE "key" = 'bayonne_ham'; +UPDATE "ingredients" SET "key" = 'rosetteSausage' WHERE "key" = 'rosette_sausage'; +UPDATE "ingredients" SET "key" = 'vealLiver' WHERE "key" = 'veal_liver'; +UPDATE "ingredients" SET "key" = 'vealKidneys' WHERE "key" = 'veal_kidneys'; +UPDATE "ingredients" SET "key" = 'vealBrain' WHERE "key" = 'veal_brain'; +UPDATE "ingredients" SET "key" = 'vealSweetbread' WHERE "key" = 'veal_sweetbread'; +UPDATE "ingredients" SET "key" = 'beefTongue' WHERE "key" = 'beef_tongue'; +UPDATE "ingredients" SET "key" = 'roeDeer' WHERE "key" = 'roe_deer'; +UPDATE "ingredients" SET "key" = 'wildBoar' WHERE "key" = 'wild_boar'; +UPDATE "ingredients" SET "key" = 'horseMeat' WHERE "key" = 'horse_meat'; +UPDATE "ingredients" SET "key" = 'beefHeart' WHERE "key" = 'beef_heart'; +UPDATE "ingredients" SET "key" = 'foieGras' WHERE "key" = 'foie_gras'; +UPDATE "ingredients" SET "key" = 'beefMuzzle' WHERE "key" = 'beef_muzzle'; +UPDATE "ingredients" SET "key" = 'grisonsDriedBeef' WHERE "key" = 'grisons_dried_beef'; +UPDATE "ingredients" SET "key" = 'duckBreast' WHERE "key" = 'duck_breast'; +UPDATE "ingredients" SET "key" = 'guineaFowl' WHERE "key" = 'guinea_fowl'; +UPDATE "ingredients" SET "key" = 'poultryLiver' WHERE "key" = 'poultry_liver'; +UPDATE "ingredients" SET "key" = 'seaBass' WHERE "key" = 'sea_bass'; +UPDATE "ingredients" SET "key" = 'seaBream' WHERE "key" = 'sea_bream'; +UPDATE "ingredients" SET "key" = 'redMullet' WHERE "key" = 'red_mullet'; +UPDATE "ingredients" SET "key" = 'smokedSalmon' WHERE "key" = 'smoked_salmon'; +UPDATE "ingredients" SET "key" = 'driedFish' WHERE "key" = 'dried_fish'; +UPDATE "ingredients" SET "key" = 'saltCod' WHERE "key" = 'salt_cod'; +UPDATE "ingredients" SET "key" = 'lemonSole' WHERE "key" = 'lemon_sole'; +UPDATE "ingredients" SET "key" = 'spinyLobster' WHERE "key" = 'spiny_lobster'; +UPDATE "ingredients" SET "key" = 'spiderCrab' WHERE "key" = 'spider_crab'; +UPDATE "ingredients" SET "key" = 'greyShrimp' WHERE "key" = 'grey_shrimp'; +UPDATE "ingredients" SET "key" = 'wholeWheatPasta' WHERE "key" = 'whole_wheat_pasta'; +UPDATE "ingredients" SET "key" = 'basmatiRice' WHERE "key" = 'basmati_rice'; +UPDATE "ingredients" SET "key" = 'brownRice' WHERE "key" = 'brown_rice'; +UPDATE "ingredients" SET "key" = 'lasagnaSheets' WHERE "key" = 'lasagna_sheets'; +UPDATE "ingredients" SET "key" = 'arborioRice' WHERE "key" = 'arborio_rice'; +UPDATE "ingredients" SET "key" = 'riceNoodles' WHERE "key" = 'rice_noodles'; +UPDATE "ingredients" SET "key" = 'udonNoodles' WHERE "key" = 'udon_noodles'; +UPDATE "ingredients" SET "key" = 'sobaNoodles' WHERE "key" = 'soba_noodles'; +UPDATE "ingredients" SET "key" = 'chineseNoodles' WHERE "key" = 'chinese_noodles'; +UPDATE "ingredients" SET "key" = 'riceVermicelli' WHERE "key" = 'rice_vermicelli'; +UPDATE "ingredients" SET "key" = 'soyVermicelli' WHERE "key" = 'soy_vermicelli'; +UPDATE "ingredients" SET "key" = 'stickyRice' WHERE "key" = 'sticky_rice'; +UPDATE "ingredients" SET "key" = 'sushiRice' WHERE "key" = 'sushi_rice'; +UPDATE "ingredients" SET "key" = 'jasmineRice' WHERE "key" = 'jasmine_rice'; +UPDATE "ingredients" SET "key" = 'greenLentils' WHERE "key" = 'green_lentils'; +UPDATE "ingredients" SET "key" = 'redLentils' WHERE "key" = 'red_lentils'; +UPDATE "ingredients" SET "key" = 'whiteBeans' WHERE "key" = 'white_beans'; +UPDATE "ingredients" SET "key" = 'kidneyBeans' WHERE "key" = 'kidney_beans'; +UPDATE "ingredients" SET "key" = 'blackBeans' WHERE "key" = 'black_beans'; +UPDATE "ingredients" SET "key" = 'splitPeas' WHERE "key" = 'split_peas'; +UPDATE "ingredients" SET "key" = 'favaBeans' WHERE "key" = 'fava_beans'; +UPDATE "ingredients" SET "key" = 'pintoBeans' WHERE "key" = 'pinto_beans'; +UPDATE "ingredients" SET "key" = 'flageoletBeans' WHERE "key" = 'flageolet_beans'; +UPDATE "ingredients" SET "key" = 'goldenLentils' WHERE "key" = 'golden_lentils'; +UPDATE "ingredients" SET "key" = 'peanutsShelled' WHERE "key" = 'peanuts_shelled'; +UPDATE "ingredients" SET "key" = 'almondPowder' WHERE "key" = 'almond_powder'; +UPDATE "ingredients" SET "key" = 'pineNuts' WHERE "key" = 'pine_nuts'; +UPDATE "ingredients" SET "key" = 'sunflowerSeeds' WHERE "key" = 'sunflower_seeds'; +UPDATE "ingredients" SET "key" = 'pumpkinSeeds' WHERE "key" = 'pumpkin_seeds'; +UPDATE "ingredients" SET "key" = 'shreddedCoconut' WHERE "key" = 'shredded_coconut'; +UPDATE "ingredients" SET "key" = 'driedApricots' WHERE "key" = 'dried_apricots'; +UPDATE "ingredients" SET "key" = 'blackMushrooms' WHERE "key" = 'black_mushrooms'; +UPDATE "ingredients" SET "key" = 'noriSeaweed' WHERE "key" = 'nori_seaweed'; +UPDATE "ingredients" SET "key" = 'wakameSeaweed' WHERE "key" = 'wakame_seaweed'; +UPDATE "ingredients" SET "key" = 'kombuSeaweed' WHERE "key" = 'kombu_seaweed'; +UPDATE "ingredients" SET "key" = 'bambooShoots' WHERE "key" = 'bamboo_shoots'; +UPDATE "ingredients" SET "key" = 'waterChestnuts' WHERE "key" = 'water_chestnuts'; +UPDATE "ingredients" SET "key" = 'sandwichBread' WHERE "key" = 'sandwich_bread'; +UPDATE "ingredients" SET "key" = 'wholeWheatBread' WHERE "key" = 'whole_wheat_bread'; +UPDATE "ingredients" SET "key" = 'ryeBread' WHERE "key" = 'rye_bread'; +UPDATE "ingredients" SET "key" = 'burgerBun' WHERE "key" = 'burger_bun'; +UPDATE "ingredients" SET "key" = 'briocheBun' WHERE "key" = 'brioche_bun'; +UPDATE "ingredients" SET "key" = 'hotDogBun' WHERE "key" = 'hot_dog_bun'; +UPDATE "ingredients" SET "key" = 'pitaBread' WHERE "key" = 'pita_bread'; +UPDATE "ingredients" SET "key" = 'wrapBread' WHERE "key" = 'wrap_bread'; +UPDATE "ingredients" SET "key" = 'vienneseBread' WHERE "key" = 'viennese_bread'; +UPDATE "ingredients" SET "key" = 'countryBread' WHERE "key" = 'country_bread'; +UPDATE "ingredients" SET "key" = 'multigrainBread' WHERE "key" = 'multigrain_bread'; +UPDATE "ingredients" SET "key" = 'breadRoll' WHERE "key" = 'bread_roll'; +UPDATE "ingredients" SET "key" = 'swedishBread' WHERE "key" = 'swedish_bread'; +UPDATE "ingredients" SET "key" = 'glutenFreeBread' WHERE "key" = 'gluten_free_bread'; +UPDATE "ingredients" SET "key" = 'cornTortilla' WHERE "key" = 'corn_tortilla'; +UPDATE "ingredients" SET "key" = 'wheatTortilla' WHERE "key" = 'wheat_tortilla'; +UPDATE "ingredients" SET "key" = 'puffPastry' WHERE "key" = 'puff_pastry'; +UPDATE "ingredients" SET "key" = 'shortcrustPastry' WHERE "key" = 'shortcrust_pastry'; +UPDATE "ingredients" SET "key" = 'pizzaDough' WHERE "key" = 'pizza_dough'; +UPDATE "ingredients" SET "key" = 'sweetShortcrustPastry' WHERE "key" = 'sweet_shortcrust_pastry'; +UPDATE "ingredients" SET "key" = 'cremeFraiche' WHERE "key" = 'creme_fraiche'; +UPDATE "ingredients" SET "key" = 'liquidCream' WHERE "key" = 'liquid_cream'; +UPDATE "ingredients" SET "key" = 'goatCheese' WHERE "key" = 'goat_cheese'; +UPDATE "ingredients" SET "key" = 'fromageBlanc' WHERE "key" = 'fromage_blanc'; +UPDATE "ingredients" SET "key" = 'saintNectaire' WHERE "key" = 'saint_nectaire'; +UPDATE "ingredients" SET "key" = 'blueCheese' WHERE "key" = 'blue_cheese'; +UPDATE "ingredients" SET "key" = 'pontLeveque' WHERE "key" = 'pont_leveque'; +UPDATE "ingredients" SET "key" = 'racletteCheese' WHERE "key" = 'raclette_cheese'; +UPDATE "ingredients" SET "key" = 'fourmeDAmbert' WHERE "key" = 'fourme_d_ambert'; +UPDATE "ingredients" SET "key" = 'ossauIraty' WHERE "key" = 'ossau_iraty'; +UPDATE "ingredients" SET "key" = 'saintMarcellin' WHERE "key" = 'saint_marcellin'; +UPDATE "ingredients" SET "key" = 'crottinDeChavignol' WHERE "key" = 'crottin_de_chavignol'; +UPDATE "ingredients" SET "key" = 'abondanceCheese' WHERE "key" = 'abondance_cheese'; +UPDATE "ingredients" SET "key" = 'carreDeLEst' WHERE "key" = 'carre_de_l_est'; +UPDATE "ingredients" SET "key" = 'montDor' WHERE "key" = 'mont_dor'; +UPDATE "ingredients" SET "key" = 'greekYogurt' WHERE "key" = 'greek_yogurt'; +UPDATE "ingredients" SET "key" = 'coconutMilk' WHERE "key" = 'coconut_milk'; +UPDATE "ingredients" SET "key" = 'coconutCream' WHERE "key" = 'coconut_cream'; +UPDATE "ingredients" SET "key" = 'almondMilk' WHERE "key" = 'almond_milk'; +UPDATE "ingredients" SET "key" = 'oatMilk' WHERE "key" = 'oat_milk'; +UPDATE "ingredients" SET "key" = 'silkenTofu' WHERE "key" = 'silken_tofu'; +UPDATE "ingredients" SET "key" = 'herbesDeProvence' WHERE "key" = 'herbes_de_provence'; +UPDATE "ingredients" SET "key" = 'blackPepper' WHERE "key" = 'black_pepper'; +UPDATE "ingredients" SET "key" = 'espelettePepper' WHERE "key" = 'espelette_pepper'; +UPDATE "ingredients" SET "key" = 'cayennePepper' WHERE "key" = 'cayenne_pepper'; +UPDATE "ingredients" SET "key" = 'curryPowder' WHERE "key" = 'curry_powder'; +UPDATE "ingredients" SET "key" = 'vanillaBean' WHERE "key" = 'vanilla_bean'; +UPDATE "ingredients" SET "key" = 'whitePepper' WHERE "key" = 'white_pepper'; +UPDATE "ingredients" SET "key" = 'pinkPepper' WHERE "key" = 'pink_pepper'; +UPDATE "ingredients" SET "key" = 'sichuanPepper' WHERE "key" = 'sichuan_pepper'; +UPDATE "ingredients" SET "key" = 'smokedPaprika' WHERE "key" = 'smoked_paprika'; +UPDATE "ingredients" SET "key" = 'birdEyeChili' WHERE "key" = 'bird_eye_chili'; +UPDATE "ingredients" SET "key" = 'juniperBerries' WHERE "key" = 'juniper_berries'; +UPDATE "ingredients" SET "key" = 'starAnise' WHERE "key" = 'star_anise'; +UPDATE "ingredients" SET "key" = 'greenAnise' WHERE "key" = 'green_anise'; +UPDATE "ingredients" SET "key" = 'fennelSeeds' WHERE "key" = 'fennel_seeds'; +UPDATE "ingredients" SET "key" = 'colomboPowder' WHERE "key" = 'colombo_powder'; +UPDATE "ingredients" SET "key" = 'herbSalt' WHERE "key" = 'herb_salt'; +UPDATE "ingredients" SET "key" = 'celerySalt' WHERE "key" = 'celery_salt'; +UPDATE "ingredients" SET "key" = 'fleurDeSel' WHERE "key" = 'fleur_de_sel'; +UPDATE "ingredients" SET "key" = 'fiveSpice' WHERE "key" = 'five_spice'; +UPDATE "ingredients" SET "key" = 'garamMasala' WHERE "key" = 'garam_masala'; +UPDATE "ingredients" SET "key" = 'corianderSeeds' WHERE "key" = 'coriander_seeds'; +UPDATE "ingredients" SET "key" = 'poblanoPepper' WHERE "key" = 'poblano_pepper'; +UPDATE "ingredients" SET "key" = 'rasElHanout' WHERE "key" = 'ras_el_hanout'; +UPDATE "ingredients" SET "key" = 'soySauce' WHERE "key" = 'soy_sauce'; +UPDATE "ingredients" SET "key" = 'worcestershireSauce' WHERE "key" = 'worcestershire_sauce'; +UPDATE "ingredients" SET "key" = 'fishSauce' WHERE "key" = 'fish_sauce'; +UPDATE "ingredients" SET "key" = 'curryPaste' WHERE "key" = 'curry_paste'; +UPDATE "ingredients" SET "key" = 'peanutButter' WHERE "key" = 'peanut_butter'; +UPDATE "ingredients" SET "key" = 'dijonMustard' WHERE "key" = 'dijon_mustard'; +UPDATE "ingredients" SET "key" = 'wholegrainMustard' WHERE "key" = 'wholegrain_mustard'; +UPDATE "ingredients" SET "key" = 'barbecueSauce' WHERE "key" = 'barbecue_sauce'; +UPDATE "ingredients" SET "key" = 'tartarSauce' WHERE "key" = 'tartar_sauce'; +UPDATE "ingredients" SET "key" = 'cocktailSauce' WHERE "key" = 'cocktail_sauce'; +UPDATE "ingredients" SET "key" = 'bearnaiseSauce' WHERE "key" = 'bearnaise_sauce'; +UPDATE "ingredients" SET "key" = 'hollandaiseSauce' WHERE "key" = 'hollandaise_sauce'; +UPDATE "ingredients" SET "key" = 'bechamelSauce' WHERE "key" = 'bechamel_sauce'; +UPDATE "ingredients" SET "key" = 'teriyakiSauce' WHERE "key" = 'teriyaki_sauce'; +UPDATE "ingredients" SET "key" = 'ponzuSauce' WHERE "key" = 'ponzu_sauce'; +UPDATE "ingredients" SET "key" = 'redPesto' WHERE "key" = 'red_pesto'; +UPDATE "ingredients" SET "key" = 'oysterSauce' WHERE "key" = 'oyster_sauce'; +UPDATE "ingredients" SET "key" = 'hoisinSauce' WHERE "key" = 'hoisin_sauce'; +UPDATE "ingredients" SET "key" = 'sweetChiliSauce' WHERE "key" = 'sweet_chili_sauce'; +UPDATE "ingredients" SET "key" = 'shrimpPaste' WHERE "key" = 'shrimp_paste'; +UPDATE "ingredients" SET "key" = 'redCurryPaste' WHERE "key" = 'red_curry_paste'; +UPDATE "ingredients" SET "key" = 'greenCurryPaste' WHERE "key" = 'green_curry_paste'; +UPDATE "ingredients" SET "key" = 'oliveOil' WHERE "key" = 'olive_oil'; +UPDATE "ingredients" SET "key" = 'sunflowerOil' WHERE "key" = 'sunflower_oil'; +UPDATE "ingredients" SET "key" = 'rapeseedOil' WHERE "key" = 'rapeseed_oil'; +UPDATE "ingredients" SET "key" = 'coconutOil' WHERE "key" = 'coconut_oil'; +UPDATE "ingredients" SET "key" = 'sesameOil' WHERE "key" = 'sesame_oil'; +UPDATE "ingredients" SET "key" = 'ciderVinegar' WHERE "key" = 'cider_vinegar'; +UPDATE "ingredients" SET "key" = 'whiteVinegar' WHERE "key" = 'white_vinegar'; +UPDATE "ingredients" SET "key" = 'balsamicVinegar' WHERE "key" = 'balsamic_vinegar'; +UPDATE "ingredients" SET "key" = 'blackOlives' WHERE "key" = 'black_olives'; +UPDATE "ingredients" SET "key" = 'greenOlives' WHERE "key" = 'green_olives'; +UPDATE "ingredients" SET "key" = 'whiteWine' WHERE "key" = 'white_wine'; +UPDATE "ingredients" SET "key" = 'redWine' WHERE "key" = 'red_wine'; +UPDATE "ingredients" SET "key" = 'roseWine' WHERE "key" = 'rose_wine'; +UPDATE "ingredients" SET "key" = 'redWineVinegar' WHERE "key" = 'red_wine_vinegar'; +UPDATE "ingredients" SET "key" = 'whiteWineVinegar' WHERE "key" = 'white_wine_vinegar'; +UPDATE "ingredients" SET "key" = 'sherryVinegar' WHERE "key" = 'sherry_vinegar'; +UPDATE "ingredients" SET "key" = 'walnutOil' WHERE "key" = 'walnut_oil'; +UPDATE "ingredients" SET "key" = 'hazelnutOil' WHERE "key" = 'hazelnut_oil'; +UPDATE "ingredients" SET "key" = 'peanutOil' WHERE "key" = 'peanut_oil'; +UPDATE "ingredients" SET "key" = 'chiliOil' WHERE "key" = 'chili_oil'; +UPDATE "ingredients" SET "key" = 'riceVinegar' WHERE "key" = 'rice_vinegar'; +UPDATE "ingredients" SET "key" = 'cornOil' WHERE "key" = 'corn_oil'; +UPDATE "ingredients" SET "key" = 'grapeseedOil' WHERE "key" = 'grapeseed_oil'; +UPDATE "ingredients" SET "key" = 'soybeanOil' WHERE "key" = 'soybean_oil'; +UPDATE "ingredients" SET "key" = 'palmOil' WHERE "key" = 'palm_oil'; +UPDATE "ingredients" SET "key" = 'lemonJuice' WHERE "key" = 'lemon_juice'; +UPDATE "ingredients" SET "key" = 'limeJuice' WHERE "key" = 'lime_juice'; +UPDATE "ingredients" SET "key" = 'orangeJuice' WHERE "key" = 'orange_juice'; +UPDATE "ingredients" SET "key" = 'appleJuice' WHERE "key" = 'apple_juice'; +UPDATE "ingredients" SET "key" = 'grapeJuice' WHERE "key" = 'grape_juice'; +UPDATE "ingredients" SET "key" = 'tomatoJuice' WHERE "key" = 'tomato_juice'; +UPDATE "ingredients" SET "key" = 'cranberryJuice' WHERE "key" = 'cranberry_juice'; +UPDATE "ingredients" SET "key" = 'portWine' WHERE "key" = 'port_wine'; +UPDATE "ingredients" SET "key" = 'vinJaune' WHERE "key" = 'vin_jaune'; +UPDATE "ingredients" SET "key" = 'wheatFlour' WHERE "key" = 'wheat_flour'; +UPDATE "ingredients" SET "key" = 'wholeWheatFlour' WHERE "key" = 'whole_wheat_flour'; +UPDATE "ingredients" SET "key" = 'cornFlour' WHERE "key" = 'corn_flour'; +UPDATE "ingredients" SET "key" = 'buckwheatFlour' WHERE "key" = 'buckwheat_flour'; +UPDATE "ingredients" SET "key" = 'riceFlour' WHERE "key" = 'rice_flour'; +UPDATE "ingredients" SET "key" = 'vegetableStockCube' WHERE "key" = 'vegetable_stock_cube'; +UPDATE "ingredients" SET "key" = 'chickenStockCube' WHERE "key" = 'chicken_stock_cube'; +UPDATE "ingredients" SET "key" = 'tomatoPaste' WHERE "key" = 'tomato_paste'; +UPDATE "ingredients" SET "key" = 'tomatoCoulis' WHERE "key" = 'tomato_coulis'; +UPDATE "ingredients" SET "key" = 'cannedPeeledTomatoes' WHERE "key" = 'canned_peeled_tomatoes'; +UPDATE "ingredients" SET "key" = 'sunDriedTomatoes' WHERE "key" = 'sun_dried_tomatoes'; +UPDATE "ingredients" SET "key" = 'vealStock' WHERE "key" = 'veal_stock'; +UPDATE "ingredients" SET "key" = 'chickenStock' WHERE "key" = 'chicken_stock'; +UPDATE "ingredients" SET "key" = 'beefStockCube' WHERE "key" = 'beef_stock_cube'; +UPDATE "ingredients" SET "key" = 'fishStockCube' WHERE "key" = 'fish_stock_cube'; +UPDATE "ingredients" SET "key" = 'vegetableBroth' WHERE "key" = 'vegetable_broth'; +UPDATE "ingredients" SET "key" = 'chickenBroth' WHERE "key" = 'chicken_broth'; +UPDATE "ingredients" SET "key" = 'beefBroth' WHERE "key" = 'beef_broth'; +UPDATE "ingredients" SET "key" = 'courtBouillon' WHERE "key" = 'court_bouillon'; +UPDATE "ingredients" SET "key" = 'shellfishBisque' WHERE "key" = 'shellfish_bisque'; +UPDATE "ingredients" SET "key" = 'tapiocaFlour' WHERE "key" = 'tapioca_flour'; +UPDATE "ingredients" SET "key" = 'masaHarina' WHERE "key" = 'masa_harina'; +UPDATE "ingredients" SET "key" = 'sparklingWater' WHERE "key" = 'sparkling_water'; +UPDATE "ingredients" SET "key" = 'orangeBlossomWater' WHERE "key" = 'orange_blossom_water'; +UPDATE "ingredients" SET "key" = 'roseWater' WHERE "key" = 'rose_water'; +UPDATE "ingredients" SET "key" = 'fishFumet' WHERE "key" = 'fish_fumet'; +UPDATE "ingredients" SET "key" = 'bakersYeast' WHERE "key" = 'bakers_yeast'; +UPDATE "ingredients" SET "key" = 'bakingPowder' WHERE "key" = 'baking_powder'; +UPDATE "ingredients" SET "key" = 'lupinFlour' WHERE "key" = 'lupin_flour'; +UPDATE "ingredients" SET "key" = 'bakingSoda' WHERE "key" = 'baking_soda'; +UPDATE "ingredients" SET "key" = 'potatoStarch' WHERE "key" = 'potato_starch'; +UPDATE "ingredients" SET "key" = 'mapleSyrup' WHERE "key" = 'maple_syrup'; +UPDATE "ingredients" SET "key" = 'brownSugar' WHERE "key" = 'brown_sugar'; +UPDATE "ingredients" SET "key" = 'powderedSugar' WHERE "key" = 'powdered_sugar'; +UPDATE "ingredients" SET "key" = 'demeraraSugar' WHERE "key" = 'demerara_sugar'; +UPDATE "ingredients" SET "key" = 'darkChocolate' WHERE "key" = 'dark_chocolate'; +UPDATE "ingredients" SET "key" = 'milkChocolate' WHERE "key" = 'milk_chocolate'; +UPDATE "ingredients" SET "key" = 'whiteChocolate' WHERE "key" = 'white_chocolate'; +UPDATE "ingredients" SET "key" = 'chocolateChips' WHERE "key" = 'chocolate_chips'; +UPDATE "ingredients" SET "key" = 'cocoaPowder' WHERE "key" = 'cocoa_powder'; +UPDATE "ingredients" SET "key" = 'vanillaExtract' WHERE "key" = 'vanilla_extract'; +UPDATE "ingredients" SET "key" = 'palmSugar' WHERE "key" = 'palm_sugar'; +UPDATE "ingredients" SET "key" = 'caneSyrup' WHERE "key" = 'cane_syrup'; + +-- IngredientCategory/IngredientSubcategory enum rename — same +-- add-with-default-then-swap approach as +-- 20260818113250_ingredient_taxonomy_rework/migration.sql: the old and +-- new enums share no values, so a direct cast isn't possible. Existing +-- rows land on the placeholder default; seedReferenceData() (runs on +-- every container start, see apps/api/Dockerfile) corrects every row's +-- real category/subcategory immediately after. +CREATE TYPE "IngredientCategory_new" AS ENUM ('freshProduce', 'meatAndSeafood', 'dryGoods', 'bakery', 'dairyAndCheese', 'condimentsAndSpices', 'cookingEssentials'); +CREATE TYPE "IngredientSubcategory_new" AS ENUM ('vegetables', 'fruits', 'freshHerbs', 'meats', 'poultry', 'fish', 'shellfish', 'starches', 'legumes', 'nutsAndSeeds', 'other', 'breads', 'rawDough', 'dairy', 'eggs', 'plantBasedAlternatives', 'spices', 'sauces', 'seasonings', 'bases', 'thickeners', 'sugars'); +ALTER TABLE "ingredients" ADD COLUMN "category_new" "IngredientCategory_new" NOT NULL DEFAULT 'dryGoods'; +ALTER TABLE "ingredients" ADD COLUMN "subcategory_new" "IngredientSubcategory_new" NOT NULL DEFAULT 'other'; +ALTER TABLE "ingredients" DROP COLUMN "category"; +ALTER TABLE "ingredients" DROP COLUMN "subcategory"; +ALTER TABLE "ingredients" RENAME COLUMN "category_new" TO "category"; +ALTER TABLE "ingredients" RENAME COLUMN "subcategory_new" TO "subcategory"; +DROP TYPE "IngredientCategory"; +DROP TYPE "IngredientSubcategory"; +ALTER TYPE "IngredientCategory_new" RENAME TO "IngredientCategory"; +ALTER TYPE "IngredientSubcategory_new" RENAME TO "IngredientSubcategory"; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index b7af9b9..85da367 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -37,11 +37,11 @@ model House { /// `key` is `@unique` — not in the original spec doc, added so the seed /// script (prisma/seed.ts) can `upsert` by key and stay idempotent/safe to /// re-run, and so two reference rows can never silently duplicate the same -/// regime. A stable slug (e.g. `"vegetarien"`), not the display label — -/// the label itself lives in `apps/web`'s `locales/fr/translation.json` -/// under `catalog.diets.` (see `reference-seed-data.ts`'s `DIETS` and -/// `utils/slugify.ts`), so it can be edited/translated without ever -/// touching this column or the rows that reference it by id. +/// regime. A stable English camelCase uid (e.g. `"vegetarian"`), not the +/// display label — the label itself lives in `apps/web`'s +/// `locales/fr/translation.json` under `catalog.diets.` (see +/// `reference-seed-data.ts`'s `DIETS`), so it can be edited/translated +/// without ever touching this column or the rows that reference it by id. model Diet { id Int @id @default(autoincrement()) key String @unique @@ -259,8 +259,6 @@ model Recipe { planningItems PlanningItem[] favoritedBy RecipeFavorite[] diets RecipeDiet[] - /// Ingredients for which this recipe is offered as a make-it-yourself alternative. - alternateFor Ingredient[] @relation("IngredientAlternateRecipe") @@map("recipe") } @@ -315,30 +313,28 @@ model RecipeDiet { /// `reference-seed-data.ts`'s `INGREDIENT_GROUPS` keys exactly — that file /// is the single source of truth for which ingredient belongs to which /// (category, subcategory) pair, these enums just give it type-safe -/// columns to live in. `@default(EPICERIE_SECHE)` exists only so this +/// columns to live in. `@default(dryGoods)` exists only so this /// column can be added `NOT NULL` to a table that may already have rows — /// the seed script corrects every row's real category on the very next /// run, this default is never the intended value for a real ingredient. enum IngredientCategory { - /// 🥦 Légumes, fruits, herbes fraîches. - PRODUITS_FRAIS - /// 🥩 Viandes, volailles, poissons, crustacés & fruits de mer. - BOUCHERIE_POISSONNERIE - /// 🥫 Féculents, légumineuses, graines & fruits secs, et le reste des - /// produits secs/en conserve qui ne rentre dans aucune autre case - /// (algues séchées, champignons séchés…). - EPICERIE_SECHE - /// 🍞 Pains et pâtes à cuire (crues, à enfourner). - BOULANGERIE - /// 🧈 Produits laitiers, œufs, alternatives végétales (laits végétaux, - /// tofu…). - CREMERIE_FROMAGE - /// 🧂 Épices, sauces, assaisonnements (huiles, vinaigres, alcools de - /// cuisine…). - CONDIMENTS_EPICES - /// 🍳 Bases de préparation (farines, bouillons, eau), épaississants - /// (levures, fécules, gélatine), sucres. - AIDES_CULINAIRES + /// 🥦 Vegetables, fruits, fresh herbs. + freshProduce + /// 🥩 Meats, poultry, fish, shellfish & seafood. + meatAndSeafood + /// 🥫 Starches, legumes, nuts & seeds, and the rest of the dry/tinned + /// goods that don't fit any other bucket (dried seaweed, dried + /// mushrooms…). + dryGoods + /// 🍞 Breads and raw dough (uncooked, ready to bake). + bakery + /// 🧈 Dairy, eggs, plant-based alternatives (plant milks, tofu…). + dairyAndCheese + /// 🧂 Spices, sauces, seasonings (oils, vinegars, cooking alcohols…). + condimentsAndSpices + /// 🍳 Prep bases (flours, stocks, water), thickeners (yeasts, starches, + /// gelatin), sugars. + cookingEssentials } /// Finer-grained rack within one {@link IngredientCategory} aisle — see @@ -347,50 +343,50 @@ enum IngredientCategory { /// `reference-seed-data.ts`'s `INGREDIENT_GROUPS`, not enforced at the /// database level — Postgres enums can't express that relationship, same /// tradeoff already accepted for `IngredientCategory` itself). -/// `@default(AUTRES)` — same NOT-NULL-migration-safety-net reasoning as +/// `@default(other)` — same NOT-NULL-migration-safety-net reasoning as /// `IngredientCategory`'s default, never the intended value for a real row. enum IngredientSubcategory { - // --- Produits frais ------------------------------------------------------ - LEGUMES - FRUITS - HERBES_FRAICHES - // --- Boucherie & poissonnerie --------------------------------------------- - VIANDES - VOLAILLES - POISSONS - CRUSTACES_FRUITS_DE_MER - // --- Épicerie sèche -------------------------------------------------------- - FECULENTS - LEGUMINEUSES - GRAINES_FRUITS_SECS + // --- freshProduce ---------------------------------------------------------- + vegetables + fruits + freshHerbs + // --- meatAndSeafood ---------------------------------------------------------- + meats + poultry + fish + shellfish + // --- dryGoods ---------------------------------------------------------------- + starches + legumes + nutsAndSeeds /// Catch-all for dried/tinned pantry items that don't fit the three /// subcategories above — dried seaweed, dried mushrooms, tinned bamboo /// shoots/water chestnuts… - AUTRES - // --- Boulangerie ------------------------------------------------------- - PAINS + other + // --- bakery -------------------------------------------------------------- + breads /// Raw, uncooked doughs meant to be baked (puff pastry, shortcrust…) — - /// distinct from `PAINS` (already-baked bread). - PATES_A_CUIRE - // --- Crémerie & fromage -------------------------------------------------- - PRODUITS_LAITIERS - OEUFS + /// distinct from `breads` (already-baked bread). + rawDough + // --- dairyAndCheese ------------------------------------------------------ + dairy + eggs /// Plant-based dairy/meat substitutes — coconut/almond/oat "milk", tofu. - ALTERNATIVES - // --- Condiments & épices ------------------------------------------------- - EPICES - SAUCES + plantBasedAlternatives + // --- condimentsAndSpices --------------------------------------------------- + spices + sauces /// Oils, vinegars, citrus juices, cooking alcohols/wines — liquids that /// season rather than form the base of a dish. - ASSAISONNEMENTS - // --- Aides culinaires ---------------------------------------------------- + seasonings + // --- cookingEssentials ----------------------------------------------------- /// Flours, stocks/broths, canned tomato bases, water — the literal base /// a recipe is built on. - BASES + bases /// Leavening/gelling/thickening agents — yeast, baking soda, cornstarch, /// gelatin. - EPAISSISSANTS - SUCRES + thickeners + sugars } /// Generic pictogram *type* for an ingredient — not in the original spec @@ -432,20 +428,29 @@ enum IngredientIcon { } model Ingredient { - id Int @id @default(autoincrement()) - key String @unique - icon IngredientIcon @default(JAR) - category IngredientCategory @default(EPICERIE_SECHE) - subcategory IngredientSubcategory @default(AUTRES) - alternateRecipeId Int? @map("alternate_recipe") + id Int @id @default(autoincrement()) + key String @unique + icon IngredientIcon @default(JAR) + category IngredientCategory @default(dryGoods) + subcategory IngredientSubcategory @default(other) + /// Whether this ingredient is reasonably makeable at home (a burger bun, + /// a béchamel) rather than something you'd only ever buy (a raw + /// vegetable, a specific cut of meat) — surfaced in the recipe form as a + /// badge/link nudging the author to go check the recipe catalog for a + /// "make it yourself" recipe (see `apps/web`'s `IngredientRow`/ + /// `IngredientPicker`). Deliberately just a flag, not a link to a + /// specific recipe — replaces an earlier, never-wired-up + /// `alternateRecipeId` FK (product decision discussed in chat: no + /// ingredient↔recipe linking in the database, the UI only pre-fills the + /// catalog's own search with this ingredient's name). + reproducible Boolean @default(false) - alternateRecipe Recipe? @relation("IngredientAlternateRecipe", fields: [alternateRecipeId], references: [id], onDelete: SetNull) - recipes RecipeIngredient[] - allergies IngredientAllergy[] + recipes RecipeIngredient[] + allergies IngredientAllergy[] /// Profiles that personally dislike this ingredient — see {@link UserProfileDislikedIngredient}. - dislikedBy UserProfileDislikedIngredient[] + dislikedBy UserProfileDislikedIngredient[] /// Diet regimes this ingredient is compatible with — see {@link IngredientDiet}. - diets IngredientDiet[] + diets IngredientDiet[] @@map("ingredients") } diff --git a/apps/api/scripts/generate-catalog-i18n.ts b/apps/api/scripts/generate-catalog-i18n.ts deleted file mode 100644 index cc1a80c..0000000 --- a/apps/api/scripts/generate-catalog-i18n.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * One-off generator, run by hand whenever the catalog's reference data - * changes (a new ingredient/diet/allergen added to - * `db/reference-seed-data.ts`, or an English key corrected in - * `catalog-en-keys.ts`): regenerates - * `apps/web/src/locales/fr/translation.json`'s - * `catalog.{diets,allergens,ingredients}` sections (English key -> French - * label), merged in without touching the rest of the file. - * - * Doesn't touch the database — a brand new diet/allergen/ingredient is - * created fresh by `seedReferenceData`'s normal create path (see - * `reference-seed-data.ts`), no backfill needed. Renaming an *existing* - * item's English key in `catalog-en-keys.ts` does need a one-off migration - * (`UPDATE ... SET key = ...`, keyed by the *old* key value) written by - * hand for that occasion — see - * `prisma/migrations/20260818193000_catalog_keys_to_english/` for the shape - * one looks like. - * - * Never imported by the app itself — a dev-time tool, run via - * `tsx scripts/generate-catalog-i18n.ts`. - */ -import { readFileSync, writeFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import { getEnglishKey } from "../src/db/catalog-en-keys.js"; -import { ALLERGENS, DIETS, INGREDIENT_GROUPS } from "../src/db/reference-seed-data.js"; - -const here = fileURLToPath(new URL(".", import.meta.url)); - -function toKeyLabelMap(labels: string[]): Record { - const map: Record = {}; - for (const label of labels) { - map[getEnglishKey(label)] = label; - } - return map; -} - -const diets = toKeyLabelMap(DIETS); -const allergens = toKeyLabelMap(ALLERGENS.map((a) => a.name)); -const ingredients = toKeyLabelMap(INGREDIENT_GROUPS.flatMap((g) => g.items.map((i) => i.name))); - -console.log( - `diets: ${Object.keys(diets).length}, allergens: ${Object.keys(allergens).length}, ingredients: ${Object.keys(ingredients).length}`, -); - -const localePath = `${here}../../web/src/locales/fr/translation.json`; -const locale = JSON.parse(readFileSync(localePath, "utf8")); -locale.catalog = { diets, allergens, ingredients }; -writeFileSync(localePath, `${JSON.stringify(locale, null, 2)}\n`); -console.log(`wrote ${localePath}`); diff --git a/apps/api/scripts/validate-catalog-en-keys.ts b/apps/api/scripts/validate-catalog-en-keys.ts deleted file mode 100644 index d7199b2..0000000 --- a/apps/api/scripts/validate-catalog-en-keys.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { getEnglishKey } from "../src/db/catalog-en-keys.js"; -/** - * One-off validation, run by hand: checks that `catalog-en-keys.ts` has an - * entry for every diet/allergen/ingredient currently in - * `reference-seed-data.ts`, and that the resulting English keys are unique - * within each table. Not part of the app or the seed itself — just a - * pre-flight check while building/editing the dictionary by hand. - */ -import { ALLERGENS, DIETS, INGREDIENT_GROUPS } from "../src/db/reference-seed-data.js"; - -function check(label: string, names: string[]) { - const keys = new Map(); - const missing: string[] = []; - const duplicates: string[] = []; - for (const name of names) { - let key: string; - try { - key = getEnglishKey(name); - } catch { - missing.push(name); - continue; - } - const existing = keys.get(key); - if (existing !== undefined && existing !== name) { - duplicates.push(`"${existing}" and "${name}" both map to "${key}"`); - } - keys.set(key, name); - } - console.log(`${label}: ${names.length} names, ${keys.size} unique keys`); - if (missing.length > 0) { - console.log(` MISSING (${missing.length}):`, missing); - } - if (duplicates.length > 0) { - console.log(` DUPLICATES (${duplicates.length}):`, duplicates); - } -} - -check("Diets", DIETS); -check( - "Allergens", - ALLERGENS.map((a) => a.name), -); -check( - "Ingredients", - INGREDIENT_GROUPS.flatMap((g) => g.items.map((i) => i.name)), -); diff --git a/apps/api/src/db/catalog-en-keys.ts b/apps/api/src/db/catalog-en-keys.ts deleted file mode 100644 index 48979a6..0000000 --- a/apps/api/src/db/catalog-en-keys.ts +++ /dev/null @@ -1,666 +0,0 @@ -/** - * English key for every catalog reference label — `Diet`/`Category` - * (allergens)/`Ingredient` rows must carry a stable, storage-safe `key` - * that is itself English, independent of whatever language the *seed's* - * authoring label (`DIETS`/`ALLERGENS`/`INGREDIENT_GROUPS` in - * `reference-seed-data.ts`, currently French) happens to be in — a French - * `key` would tie the identifier to the one language it's meant to be - * decoupled from (see `utils/slugify.ts`'s doc comment and `apps/web`'s - * `locales/fr/translation.json` `catalog.*` namespace, which resolves the - * *display* label from this same key). - * - * Hand-assigned (not machine-translated) — an English label is chosen once - * and never changes, exactly like the key it produces (via {@link - * slugify}). Keyed by the exact French authoring label so - * `reference-seed-data.ts` and `scripts/generate-catalog-i18n.ts` can look - * a row's key up by the same string they already have in hand. - * - * `getEnglishKey` throws on a missing entry rather than falling back to - * slugifying the French label — a silently-French key defeats the point, - * so a newly-added diet/allergen/ingredient must get an entry here before - * it can seed. - */ -import { slugify } from "../utils/slugify.js"; - -const DIET_KEYS: Record = { - Omnivore: "omnivore", - Végétarien: "vegetarian", - Végan: "vegan", - Pescétarien: "pescatarian", - "Sans gluten": "gluten_free", -}; - -const ALLERGEN_KEYS: Record = { - Gluten: "gluten", - Crustacés: "crustaceans", - Œufs: "eggs", - Poissons: "fish", - Arachides: "peanuts", - Soja: "soy", - Lait: "milk", - "Fruits à coque": "tree_nuts", - Céleri: "celery", - Moutarde: "mustard", - "Graines de sésame": "sesame_seeds", - Sulfites: "sulfites", - Lupin: "lupin", - Mollusques: "molluscs", -}; - -// Grouped by (category, subcategory), mirroring `INGREDIENT_GROUPS` in -// reference-seed-data.ts, purely so a translator can find/check an entry -// against its source group — this is one flat lookup table at runtime. -const INGREDIENT_KEYS: Record = { - // --- Produits frais / Légumes --------------------------------------- - Tomate: "tomato", - Oignon: "onion", - Échalote: "shallot", - Ail: "garlic", - Carotte: "carrot", - Courgette: "zucchini", - Concombre: "cucumber", - Cornichons: "gherkins", - Poivron: "bell_pepper", - Champignon: "mushroom", - Cèpes: "porcini", - Aubergine: "eggplant", - Brocoli: "broccoli", - "Chou-fleur": "cauliflower", - "Chou blanc": "white_cabbage", - "Chou rouge": "red_cabbage", - "Chou de Bruxelles": "brussels_sprouts", - Épinard: "spinach", - Blette: "swiss_chard", - Salade: "lettuce", - Roquette: "arugula", - Cresson: "watercress", - Poireau: "leek", - Radis: "radish", - Betterave: "beetroot", - Navet: "turnip", - Panais: "parsnip", - "Haricot vert": "green_bean", - "Petit pois": "pea", - Maïs: "corn", - Artichaut: "artichoke", - Fenouil: "fennel", - Endive: "endive", - Potiron: "pumpkin", - Butternut: "butternut_squash", - Asperge: "asparagus", - Avocat: "avocado", - "Pomme de terre": "potato", - "Patate douce": "sweet_potato", - "Tomates cerises": "cherry_tomato", - "Pak-choï": "bok_choy", - "Germes de soja": "soybean_sprouts", - Shiitake: "shiitake", - Daikon: "daikon", - "Piment vert frais": "fresh_green_chili", - Cardon: "cardoon", - "Chicorée rouge": "radicchio", - "Chou romanesco": "romanesco", - "Chou-rave": "kohlrabi", - "Chou chinois": "napa_cabbage", - "Céleri-rave": "celeriac", - Gombo: "okra", - "Oignon nouveau": "spring_onion", - Potimarron: "red_kuri_squash", - Rutabaga: "rutabaga", - Salicorne: "samphire", - Salsifis: "salsify", - Mâche: "lambs_lettuce", - Scarole: "escarole", - - // --- Produits frais / Fruits ----------------------------------------- - Citron: "lemon", - "Citron vert": "lime", - Pomme: "apple", - Poire: "pear", - Banane: "banana", - Orange: "orange", - Clémentine: "clementine", - Pamplemousse: "grapefruit", - Fraise: "strawberry", - Framboise: "raspberry", - Myrtille: "blueberry", - Mûre: "blackberry", - Cerise: "cherry", - Abricot: "apricot", - Pêche: "peach", - Prune: "plum", - Raisin: "grape", - Melon: "melon", - Pastèque: "watermelon", - Ananas: "pineapple", - Mangue: "mango", - Kiwi: "kiwi", - Figue: "fig", - Datte: "date", - Litchi: "lychee", - Grenade: "pomegranate", - Rhubarbe: "rhubarb", - Coing: "quince", - Cassis: "blackcurrant", - Canneberge: "cranberry", - Groseille: "redcurrant", - Kaki: "persimmon", - Nectarine: "nectarine", - Tamarin: "tamarind", - - // --- Produits frais / Herbes fraîches --------------------------------- - Basilic: "basil", - Persil: "parsley", - Thym: "thyme", - Romarin: "rosemary", - Laurier: "bay_leaf", - Ciboulette: "chives", - "Coriandre fraîche": "fresh_cilantro", - Menthe: "mint", - Origan: "oregano", - Aneth: "dill", - Estragon: "tarragon", - Sarriette: "savory", - Marjolaine: "marjoram", - Sauge: "sage", - Cerfeuil: "chervil", - Gingembre: "ginger", - Citronnelle: "lemongrass", - Combava: "kaffir_lime", - - // --- Boucherie & poissonnerie / Viandes ------------------------------- - Lapin: "rabbit", - "Bœuf haché": "ground_beef", - "Steak de bœuf": "beef_steak", - "Rôti de bœuf": "beef_roast", - "Escalope de veau": "veal_cutlet", - "Filet mignon de porc": "pork_tenderloin", - "Côte de porc": "pork_chop", - Agneau: "lamb", - "Gigot d'agneau": "leg_of_lamb", - Lardons: "bacon_lardons", - Bacon: "bacon", - "Jambon blanc": "ham", - "Jambon cru": "cured_ham", - Saucisse: "sausage", - Chorizo: "chorizo", - Merguez: "merguez", - Prosciutto: "prosciutto", - Pancetta: "pancetta", - Mortadelle: "mortadella", - Salami: "salami", - Andouille: "andouille", - Andouillette: "andouillette", - "Boudin blanc": "white_pudding", - "Boudin noir": "black_pudding", - Cervelas: "cervelat", - Rillettes: "rillettes", - "Saucisson sec": "dry_cured_sausage", - "Jambon de Bayonne": "bayonne_ham", - Coppa: "coppa", - "Rosette (saucisson)": "rosette_sausage", - "Foie de veau": "veal_liver", - "Rognons de veau": "veal_kidneys", - "Cervelle de veau": "veal_brain", - "Ris de veau": "veal_sweetbread", - "Langue de bœuf": "beef_tongue", - Tripes: "tripe", - Cerf: "venison", - Chevreuil: "roe_deer", - Sanglier: "wild_boar", - Cheval: "horse_meat", - "Cœur de bœuf": "beef_heart", - "Foie gras": "foie_gras", - "Museau de bœuf": "beef_muzzle", - "Viande des Grisons": "grisons_dried_beef", - - // --- Boucherie & poissonnerie / Volailles ----------------------------- - Poulet: "chicken", - Dinde: "turkey", - Canard: "duck", - "Magret de canard": "duck_breast", - Caille: "quail", - Pintade: "guinea_fowl", - Oie: "goose", - "Foie de volaille": "poultry_liver", - Chapon: "capon", - Pigeon: "pigeon", - Faisan: "pheasant", - - // --- Boucherie & poissonnerie / Poissons ------------------------------- - Saumon: "salmon", - Thon: "tuna", - Cabillaud: "cod", - Truite: "trout", - Sardine: "sardine", - Anchois: "anchovy", - Merlan: "whiting", - Surimi: "surimi", - "Bar (loup de mer)": "sea_bass", - Dorade: "sea_bream", - Sole: "sole", - Turbot: "turbot", - Merlu: "hake", - Colin: "pollock", - "Lieu noir": "saithe", - Églefin: "haddock", - Maquereau: "mackerel", - Hareng: "herring", - Rouget: "red_mullet", - Raie: "skate", - Lotte: "monkfish", - Flétan: "halibut", - Espadon: "swordfish", - Carpe: "carp", - Brochet: "pike", - Perche: "perch", - Tilapia: "tilapia", - Panga: "pangasius", - "Saumon fumé": "smoked_salmon", - "Poisson séché": "dried_fish", - Anguille: "eel", - "Carrelet (ou plie)": "plaice", - Morue: "salt_cod", - Limande: "lemon_sole", - Rascasse: "scorpionfish", - - // --- Boucherie & poissonnerie / Crustacés & fruits de mer ------------- - Crevettes: "shrimp", - Langoustines: "langoustine", - Homard: "lobster", - Crabe: "crab", - Langouste: "spiny_lobster", - Moules: "mussels", - Huîtres: "oysters", - "Saint-Jacques": "scallops", - Calamar: "squid", - Poulpe: "octopus", - Palourdes: "clams", - Bulots: "whelks", - "Araignée de mer": "spider_crab", - Bigorneau: "periwinkle", - Écrevisse: "crayfish", - "Crevette grise": "grey_shrimp", - Coque: "cockle", - Escargot: "snail", - Seiche: "cuttlefish", - - // --- Épicerie sèche / Féculents ---------------------------------------- - Semoule: "semolina", - Couscous: "couscous", - Boulgour: "bulgur", - Polenta: "polenta", - Quinoa: "quinoa", - Pâtes: "pasta", - "Pâtes complètes": "whole_wheat_pasta", - Riz: "rice", - "Riz basmati": "basmati_rice", - "Riz complet": "brown_rice", - "Flocons d'avoine": "oats", - Spaghetti: "spaghetti", - Penne: "penne", - Tagliatelles: "tagliatelle", - "Lasagnes (feuilles)": "lasagna_sheets", - Gnocchi: "gnocchi", - "Riz arborio": "arborio_rice", - "Nouilles de riz": "rice_noodles", - "Nouilles udon": "udon_noodles", - "Nouilles soba": "soba_noodles", - "Nouilles chinoises": "chinese_noodles", - "Vermicelles de riz": "rice_vermicelli", - "Vermicelles de soja": "soy_vermicelli", - "Riz gluant": "sticky_rice", - "Riz à sushi": "sushi_rice", - "Riz jasmin": "jasmine_rice", - - // --- Épicerie sèche / Légumineuses -------------------------------------- - "Lentilles vertes": "green_lentils", - "Lentilles corail": "red_lentils", - "Pois chiches": "chickpeas", - "Haricots blancs": "white_beans", - "Haricots rouges": "kidney_beans", - "Haricots noirs": "black_beans", - "Pois cassés": "split_peas", - Fèves: "fava_beans", - Edamame: "edamame", - "Haricots pinto": "pinto_beans", - "Haricots flageolets": "flageolet_beans", - "Lentilles blondes": "golden_lentils", - - // --- Épicerie sèche / Graines & fruits secs ---------------------------- - Cacahuètes: "peanuts_shelled", - Amandes: "almonds", - Noix: "walnuts", - Noisettes: "hazelnuts", - "Noix de cajou": "cashews", - Pistaches: "pistachios", - "Noix de pécan": "pecans", - "Poudre d'amande": "almond_powder", - "Pignons de pin": "pine_nuts", - "Graines de tournesol": "sunflower_seeds", - "Graines de courge": "pumpkin_seeds", - "Noix de coco râpée": "shredded_coconut", - "Raisins secs": "raisins", - Pruneaux: "prunes", - "Abricots secs": "dried_apricots", - - // --- Épicerie sèche / Autres -------------------------------------------- - "Champignons noirs": "black_mushrooms", - "Algue nori": "nori_seaweed", - "Algue wakamé": "wakame_seaweed", - "Algue kombu": "kombu_seaweed", - "Pousses de bambou": "bamboo_shoots", - "Châtaignes d'eau": "water_chestnuts", - - // --- Boulangerie / Pains ------------------------------------------------- - Pain: "bread", - "Pain de mie": "sandwich_bread", - "Pain complet": "whole_wheat_bread", - Baguette: "baguette", - "Pain de seigle": "rye_bread", - Chapelure: "breadcrumbs", - "Pain à burger": "burger_bun", - "Pain brioché": "brioche_bun", - "Pain à hot-dog": "hot_dog_bun", - "Pain pita": "pita_bread", - "Pain bagel": "bagel", - Naan: "naan", - "Pain wrap": "wrap_bread", - "Pain viennois": "viennese_bread", - "Pain de campagne": "country_bread", - "Pain aux céréales": "multigrain_bread", - "Petit pain": "bread_roll", - "Pain suédois": "swedish_bread", - "Pain sans gluten": "gluten_free_bread", - Biscotte: "rusk", - Croûtons: "croutons", - Focaccia: "focaccia", - Ciabatta: "ciabatta", - "Tortilla de maïs": "corn_tortilla", - "Tortilla de blé": "wheat_tortilla", - Gressin: "breadstick", - - // --- Boulangerie / Pâtes à cuire ----------------------------------------- - "Pâte feuilletée": "puff_pastry", - "Pâte brisée": "shortcrust_pastry", - "Pâte à pizza": "pizza_dough", - "Pâte à tarte sablée": "sweet_shortcrust_pastry", - - // --- Crémerie & fromage / Produits laitiers ------------------------------ - Lait: "milk", - Beurre: "butter", - "Crème fraîche": "creme_fraiche", - "Crème liquide": "liquid_cream", - Fromage: "cheese", - Emmental: "emmental", - Gruyère: "gruyere", - Parmesan: "parmesan", - Mozzarella: "mozzarella", - "Chèvre (fromage)": "goat_cheese", - Feta: "feta", - Comté: "comte", - "Fromage blanc": "fromage_blanc", - Mascarpone: "mascarpone", - Yaourt: "yogurt", - Burrata: "burrata", - Ricotta: "ricotta", - Pecorino: "pecorino", - Gorgonzola: "gorgonzola", - Cheddar: "cheddar", - Brie: "brie", - Camembert: "camembert", - Roquefort: "roquefort", - Munster: "munster", - Reblochon: "reblochon", - Cantal: "cantal", - Beaufort: "beaufort", - "Saint-Nectaire": "saint_nectaire", - "Bleu (fromage)": "blue_cheese", - Cancoillotte: "cancoillotte", - Tomme: "tomme", - Époisses: "epoisses", - Chaource: "chaource", - Livarot: "livarot", - "Pont-l'Évêque": "pont_leveque", - Morbier: "morbier", - "Raclette (fromage)": "raclette_cheese", - "Fourme d'Ambert": "fourme_d_ambert", - Salers: "salers", - "Ossau-Iraty": "ossau_iraty", - Vacherin: "vacherin", - "Saint-Marcellin": "saint_marcellin", - Neufchâtel: "neufchatel", - "Crottin de Chavignol": "crottin_de_chavignol", - Abondance: "abondance_cheese", - "Carré de l'Est": "carre_de_l_est", - Edam: "edam", - Gouda: "gouda", - Mimolette: "mimolette", - Maroilles: "maroilles", - "Mont d'or": "mont_dor", - Kéfir: "kefir", - "Yaourt à la grecque": "greek_yogurt", - - // --- Crémerie & fromage / Œufs ------------------------------------------- - Œuf: "egg", - - // --- Crémerie & fromage / Alternatives ------------------------------------ - "Lait de coco": "coconut_milk", - "Crème de coco": "coconut_cream", - "Lait d'amande": "almond_milk", - "Lait d'avoine": "oat_milk", - Tofu: "tofu", - "Tofu soyeux": "silken_tofu", - - // --- Condiments & épices / Épices ----------------------------------------- - "Herbes de Provence": "herbes_de_provence", - "Poivre noir": "black_pepper", - Paprika: "paprika", - "Piment d'Espelette": "espelette_pepper", - "Piment de Cayenne": "cayenne_pepper", - Cumin: "cumin", - "Curry (poudre)": "curry_powder", - Curcuma: "turmeric", - Cannelle: "cinnamon", - Muscade: "nutmeg", - Safran: "saffron", - "Clou de girofle": "clove", - "Vanille (gousse)": "vanilla_bean", - "Poivre blanc": "white_pepper", - "Poivre rose": "pink_pepper", - "Poivre du Sichuan": "sichuan_pepper", - "Paprika fumé": "smoked_paprika", - "Piment oiseau": "bird_eye_chili", - "Baies de genièvre": "juniper_berries", - "Anis étoilé (badiane)": "star_anise", - "Anis vert": "green_anise", - "Graines de fenouil": "fennel_seeds", - Sumac: "sumac", - Nigelle: "nigella", - "Quatre épices": "allspice", - "Colombo (poudre)": "colombo_powder", - Baharat: "baharat", - Raifort: "horseradish", - "Sel aux herbes": "herb_salt", - "Sel de céleri": "celery_salt", - "Fleur de sel": "fleur_de_sel", - Sel: "salt", - "Cinq épices": "five_spice", - "Garam masala": "garam_masala", - "Graines de coriandre": "coriander_seeds", - Cardamome: "cardamom", - Fenugrec: "fenugreek", - "Piment jalapeño": "jalapeno", - "Piment chipotle": "chipotle", - "Piment poblano": "poblano_pepper", - "Piment habanero": "habanero", - "Ras el hanout": "ras_el_hanout", - "Za'atar": "zaatar", - - // --- Condiments & épices / Sauces ------------------------------------------- - "Sauce soja": "soy_sauce", - Moutarde: "mustard", - Mayonnaise: "mayonnaise", - Ketchup: "ketchup", - Tabasco: "tabasco", - "Sauce Worcestershire": "worcestershire_sauce", - "Sauce nuoc-mâm": "fish_sauce", - Wasabi: "wasabi", - Harissa: "harissa", - "Pâte de curry": "curry_paste", - "Beurre de cacahuète": "peanut_butter", - "Moutarde de Dijon": "dijon_mustard", - "Moutarde à l'ancienne": "wholegrain_mustard", - "Sauce barbecue": "barbecue_sauce", - "Sauce tartare": "tartar_sauce", - "Sauce cocktail": "cocktail_sauce", - "Sauce béarnaise": "bearnaise_sauce", - "Sauce hollandaise": "hollandaise_sauce", - "Sauce béchamel": "bechamel_sauce", - "Sauce teriyaki": "teriyaki_sauce", - "Sauce ponzu": "ponzu_sauce", - Chimichurri: "chimichurri", - "Pesto rouge (tomates séchées)": "red_pesto", - Pesto: "pesto", - "Sauce huître": "oyster_sauce", - "Sauce hoisin": "hoisin_sauce", - "Sauce sriracha": "sriracha", - "Sauce sweet chili": "sweet_chili_sauce", - Miso: "miso", - "Pâte de crevettes": "shrimp_paste", - "Pâte de curry rouge (thaï)": "red_curry_paste", - "Pâte de curry vert (thaï)": "green_curry_paste", - Tahini: "tahini", - Aïoli: "aioli", - "Sauce vinaigrette": "vinaigrette", - Houmous: "hummus", - - // --- Condiments & épices / Assaisonnements ----------------------------------- - "Huile d'olive": "olive_oil", - "Huile de tournesol": "sunflower_oil", - "Huile de colza": "rapeseed_oil", - "Huile de coco": "coconut_oil", - "Huile de sésame": "sesame_oil", - "Vinaigre de cidre": "cider_vinegar", - "Vinaigre blanc": "white_vinegar", - "Vinaigre balsamique": "balsamic_vinegar", - Câpres: "capers", - Olives: "olives", - "Olives noires": "black_olives", - "Olives vertes": "green_olives", - "Vin blanc (cuisine)": "white_wine", - "Vin rouge (cuisine)": "red_wine", - "Vin rosé (cuisine)": "rose_wine", - "Vinaigre de vin rouge": "red_wine_vinegar", - "Vinaigre de vin blanc": "white_wine_vinegar", - "Vinaigre de xérès": "sherry_vinegar", - "Huile de noix": "walnut_oil", - "Huile de noisette": "hazelnut_oil", - "Huile d'arachide": "peanut_oil", - "Huile pimentée": "chili_oil", - "Huile de maïs": "corn_oil", - "Huile de pépins de raisin": "grapeseed_oil", - "Huile de soja": "soybean_oil", - "Huile de palme": "palm_oil", - "Vinaigre de riz": "rice_vinegar", - Mirin: "mirin", - "Saké (cuisine)": "sake", - "Jus de citron": "lemon_juice", - "Jus de citron vert": "lime_juice", - "Jus d'orange": "orange_juice", - "Jus de pomme": "apple_juice", - "Jus de raisin": "grape_juice", - "Jus de tomate": "tomato_juice", - "Jus de cranberry": "cranberry_juice", - Café: "coffee", - Thé: "tea", - "Bière (cuisine)": "beer", - "Cidre (cuisine)": "cider", - "Champagne / vin pétillant (cuisine)": "champagne", - "Porto (cuisine)": "port_wine", - "Vin jaune (cuisine)": "vin_jaune", - Cognac: "cognac", - Rhum: "rum", - Whisky: "whisky", - Vodka: "vodka", - - // --- Aides culinaires / Bases ------------------------------------------- - "Farine de blé": "wheat_flour", - "Farine complète": "whole_wheat_flour", - "Farine de maïs": "corn_flour", - "Farine de sarrasin": "buckwheat_flour", - "Farine de riz": "rice_flour", - "Bouillon cube légumes": "vegetable_stock_cube", - "Bouillon cube volaille": "chicken_stock_cube", - "Concentré de tomate": "tomato_paste", - "Coulis de tomate": "tomato_coulis", - "Tomates pelées (conserve)": "canned_peeled_tomatoes", - "Tomates séchées": "sun_dried_tomatoes", - "Fond de veau": "veal_stock", - "Fond de volaille": "chicken_stock", - "Bouillon cube bœuf": "beef_stock_cube", - "Bouillon cube poisson": "fish_stock_cube", - "Bouillon de légumes": "vegetable_broth", - "Bouillon de volaille": "chicken_broth", - "Bouillon de bœuf": "beef_broth", - "Court-bouillon": "court_bouillon", - "Dashi (bouillon japonais)": "dashi", - "Bisque de crustacés": "shellfish_bisque", - "Farine de tapioca": "tapioca_flour", - "Masa harina": "masa_harina", - Eau: "water", - "Eau gazeuse": "sparkling_water", - "Eau de fleur d'oranger": "orange_blossom_water", - "Eau de rose": "rose_water", - "Fumet de poisson": "fish_fumet", - - // --- Aides culinaires / Épaississants ------------------------------------- - "Levure boulangère": "bakers_yeast", - "Levure chimique": "baking_powder", - Maïzena: "cornstarch", - "Farine de lupin": "lupin_flour", - Gélatine: "gelatin", - "Bicarbonate de soude": "baking_soda", - "Fécule de pomme de terre": "potato_starch", - - // --- Aides culinaires / Sucres --------------------------------------------- - Sucre: "sugar", - Miel: "honey", - "Sirop d'érable": "maple_syrup", - "Sucre roux": "brown_sugar", - "Sucre glace": "powdered_sugar", - Cassonade: "demerara_sugar", - "Chocolat noir": "dark_chocolate", - "Chocolat au lait": "milk_chocolate", - "Chocolat blanc": "white_chocolate", - "Pépites de chocolat": "chocolate_chips", - "Cacao en poudre": "cocoa_powder", - "Extrait de vanille": "vanilla_extract", - "Sucre de palme": "palm_sugar", - "Sirop de sucre de canne": "cane_syrup", -}; - -const ALL_KEYS: Record = { - ...DIET_KEYS, - ...ALLERGEN_KEYS, - ...INGREDIENT_KEYS, -}; - -/** - * Resolves a seed-time French authoring label to its English `key` — - * throws if it's missing an entry above rather than silently falling back - * to a French slug, since a new diet/allergen/ingredient needs a - * deliberately-chosen English key before it can seed at all. - * `slugify` still runs over the result so a stray character/casing slip in - * the table above can't produce a key that doesn't match the - * `[a-z0-9_]`-only shape every other key has. - */ -export function getEnglishKey(frenchLabel: string): string { - const english = ALL_KEYS[frenchLabel]; - if (english === undefined) { - throw new Error( - `No English key registered for "${frenchLabel}" — add one to catalog-en-keys.ts`, - ); - } - return slugify(english); -} diff --git a/apps/api/src/db/reference-seed-data.ts b/apps/api/src/db/reference-seed-data.ts index e82ca20..da8d74f 100644 --- a/apps/api/src/db/reference-seed-data.ts +++ b/apps/api/src/db/reference-seed-data.ts @@ -5,14 +5,15 @@ import type { IngredientSubcategory, PrismaClient, } from "@prisma/client"; -import { getEnglishKey } from "./catalog-en-keys.js"; // Short, optional-to-pick regime list — `UserProfile.dietId` stays -// nullable, this is not meant to be exhaustive. Exported for -// `apps/api/scripts/generate-catalog-i18n.ts` (one-off, regenerates -// `apps/web`'s `locales/fr/translation.json` `catalog.diets` section from -// this same list). -export const DIETS = ["Omnivore", "Végétarien", "Végan", "Pescétarien", "Sans gluten"]; +// nullable, this is not meant to be exhaustive. Authored directly as +// English camelCase `uid`s (the exact value stored as `Diet.key`) — no +// French label, no separate lookup table (see the module doc comment on +// `seedReferenceData` below). The French display text for each lives +// solely in `apps/web`'s `locales/fr/translation.json` (`catalog.diets`), +// maintained independently, tied together only by this same uid string. +export const DIETS = ["omnivore", "vegetarian", "vegan", "pescatarian", "glutenFree"]; // The 14 allergens EU Regulation 1169/2011 (Annex II) requires food // businesses to declare — a standard, defensible reference list rather than @@ -21,25 +22,26 @@ export const DIETS = ["Omnivore", "Végétarien", "Végan", "Pescétarien", "San // sensitivity) per the product decision discussed in chat: only Gluten and // Sulfites are commonly-recognized intolerances among the 14; the rest are // true allergens. -export const ALLERGENS: Array<{ name: string; kind: AllergenKind }> = [ - { name: "Gluten", kind: "INTOLERANCE" }, - { name: "Crustacés", kind: "ALLERGY" }, - { name: "Œufs", kind: "ALLERGY" }, - { name: "Poissons", kind: "ALLERGY" }, - { name: "Arachides", kind: "ALLERGY" }, - { name: "Soja", kind: "ALLERGY" }, - { name: "Lait", kind: "ALLERGY" }, - { name: "Fruits à coque", kind: "ALLERGY" }, - { name: "Céleri", kind: "ALLERGY" }, - { name: "Moutarde", kind: "ALLERGY" }, - { name: "Graines de sésame", kind: "ALLERGY" }, - { name: "Sulfites", kind: "INTOLERANCE" }, - { name: "Lupin", kind: "ALLERGY" }, - { name: "Mollusques", kind: "ALLERGY" }, +export const ALLERGENS: Array<{ uid: string; kind: AllergenKind }> = [ + { uid: "gluten", kind: "INTOLERANCE" }, + { uid: "crustaceans", kind: "ALLERGY" }, + { uid: "eggs", kind: "ALLERGY" }, + { uid: "fish", kind: "ALLERGY" }, + { uid: "peanuts", kind: "ALLERGY" }, + { uid: "soy", kind: "ALLERGY" }, + { uid: "milk", kind: "ALLERGY" }, + { uid: "treeNuts", kind: "ALLERGY" }, + { uid: "celery", kind: "ALLERGY" }, + { uid: "mustard", kind: "ALLERGY" }, + { uid: "sesameSeeds", kind: "ALLERGY" }, + { uid: "sulfites", kind: "INTOLERANCE" }, + { uid: "lupin", kind: "ALLERGY" }, + { uid: "molluscs", kind: "ALLERGY" }, ]; interface IngredientSeed { - name: string; + /** English camelCase identifier, authored directly — also the DB `key` value (no French label, no separate lookup table). */ + uid: string; /** * Generic pictogram type, overriding its group's `defaultIcon` below — * only needed for the exceptions within a subcategory (a wedge of cheese @@ -51,29 +53,41 @@ interface IngredientSeed { * vocabulary. */ icon?: IngredientIcon; - allergenNames: string[]; + allergenUids: string[]; /** * Diet regimes this ingredient is compatible with, overriding its group's * `defaultDiets` below — only needed for the exceptions within a * subcategory (a fish-based stock inside an otherwise-vegan "bases" * group, a butter-based dough inside an otherwise-vegan "pâtes à - * cuire"…). References `DIETS` by name, same as `allergenNames` - * references `ALLERGENS`. Deliberately never includes `"Omnivore"` - * (trivial, every ingredient qualifies) or `"Sans gluten"` (derived from - * `allergenNames` instead — see `IngredientDiet` in schema.prisma for + * cuire"…). References `DIETS` by uid, same as `allergenUids` + * references `ALLERGENS`. Deliberately never includes `"omnivore"` + * (trivial, every ingredient qualifies) or `"glutenFree"` (derived from + * `allergenUids` instead — see `IngredientDiet` in schema.prisma for * why). */ - dietNames?: string[]; + dietUids?: string[]; + /** + * Whether this ingredient is reasonably makeable at home (a burger bun, a + * béchamel) rather than something you'd only ever buy (a raw vegetable, a + * specific cut of meat) — see `Ingredient.reproducible` in schema.prisma. + * Omitted (falsy) by default; only set `true` on the curated subset this + * is actually true for. Never group-level (unlike `defaultDiets`/ + * `defaultIcon`) — even a homogeneous-looking group like "Pains" mixes + * genuinely home-bakeable items (`Pain`, `Naan`) with ones nobody + * realistically bakes from scratch (`Pain de seigle`, `Biscotte`), so this + * needs a per-item judgment call, not a group default. + */ + reproducible?: boolean; } // A broad pantry list — the goal is to cover the large majority of what a // home cook reaches for (viandes, poissons, légumes, fruits, féculents, // condiments, épices...), not just enough to exercise the recipe catalog in // tests. Ingredients are reference data (see `Ingredient` in schema.prisma: -// `name` is `@unique`, there's no create/update/delete endpoint), so this is +// `key` is `@unique`, there's no create/update/delete endpoint), so this is // meant to already be comprehensive at first deploy rather than grown -// piecemeal as recipes need more of it. `allergenNames` reference -// `ALLERGENS` above by name — every one of the 14 EU-regulated allergens is +// piecemeal as recipes need more of it. `allergenUids` reference +// `ALLERGENS` above by uid — every one of the 14 EU-regulated allergens is // covered by at least one ingredient here. // // Grouped by (`category`, `subcategory`) — mirrors `IngredientCategory`/ @@ -90,7 +104,7 @@ interface IngredientSeed { // array for the seeding loop. // // `defaultDiets`/`defaultIcon` are what every item in the group shares -// unless it sets its own `dietNames`/`icon` — most groups are homogeneous +// unless it sets its own `dietUids`/`icon` — most groups are homogeneous // on both counts (a vegetable is always vegan and always looks like a // vegetable; a cut of meat never is and never does), so this avoids // repeating the same values on hundreds of items; only a group's @@ -107,464 +121,468 @@ export const INGREDIENT_GROUPS: Array<{ // Produits frais // ========================================================================= { - category: "PRODUITS_FRAIS", - subcategory: "LEGUMES", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "freshProduce", + subcategory: "vegetables", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "VEGETABLE", items: [ - { name: "Tomate", allergenNames: [] }, - { name: "Oignon", allergenNames: [] }, - { name: "Échalote", allergenNames: [] }, - { name: "Ail", allergenNames: [] }, - { name: "Carotte", allergenNames: [] }, - { name: "Courgette", allergenNames: [] }, - { name: "Concombre", allergenNames: [] }, - { name: "Cornichons", allergenNames: [] }, - { name: "Poivron", allergenNames: [] }, - { name: "Champignon", allergenNames: [] }, - { name: "Cèpes", allergenNames: [] }, - { name: "Aubergine", allergenNames: [] }, - { name: "Brocoli", allergenNames: [] }, - { name: "Chou-fleur", allergenNames: [] }, - { name: "Chou blanc", allergenNames: [] }, - { name: "Chou rouge", allergenNames: [] }, - { name: "Chou de Bruxelles", allergenNames: [] }, - { name: "Épinard", allergenNames: [] }, - { name: "Blette", allergenNames: [] }, - { name: "Salade", allergenNames: [] }, - { name: "Roquette", allergenNames: [] }, - { name: "Cresson", allergenNames: [] }, - { name: "Poireau", allergenNames: [] }, - { name: "Céleri", allergenNames: ["Céleri"] }, - { name: "Radis", allergenNames: [] }, - { name: "Betterave", allergenNames: [] }, - { name: "Navet", allergenNames: [] }, - { name: "Panais", allergenNames: [] }, - { name: "Haricot vert", allergenNames: [] }, - { name: "Petit pois", allergenNames: [] }, - { name: "Maïs", allergenNames: [] }, - { name: "Artichaut", allergenNames: [] }, - { name: "Fenouil", allergenNames: [] }, - { name: "Endive", allergenNames: [] }, - { name: "Potiron", allergenNames: [] }, - { name: "Butternut", allergenNames: [] }, - { name: "Asperge", allergenNames: [] }, - { name: "Avocat", allergenNames: [] }, - { name: "Pomme de terre", allergenNames: [] }, - { name: "Patate douce", allergenNames: [] }, - { name: "Tomates cerises", allergenNames: [] }, - { name: "Pak-choï", allergenNames: [] }, - { name: "Germes de soja", allergenNames: ["Soja"] }, - { name: "Shiitake", allergenNames: [] }, - { name: "Daikon", allergenNames: [] }, - { name: "Piment vert frais", allergenNames: [] }, - { name: "Cardon", allergenNames: [] }, + { uid: "tomato", allergenUids: [] }, + { uid: "onion", allergenUids: [] }, + { uid: "shallot", allergenUids: [] }, + { uid: "garlic", allergenUids: [] }, + { uid: "carrot", allergenUids: [] }, + { uid: "zucchini", allergenUids: [] }, + { uid: "cucumber", allergenUids: [] }, + { uid: "gherkins", allergenUids: [] }, + { uid: "bellPepper", allergenUids: [] }, + { uid: "mushroom", allergenUids: [] }, + { uid: "porcini", allergenUids: [] }, + { uid: "eggplant", allergenUids: [] }, + { uid: "broccoli", allergenUids: [] }, + { uid: "cauliflower", allergenUids: [] }, + { uid: "whiteCabbage", allergenUids: [] }, + { uid: "redCabbage", allergenUids: [] }, + { uid: "brusselsSprouts", allergenUids: [] }, + { uid: "spinach", allergenUids: [] }, + { uid: "swissChard", allergenUids: [] }, + { uid: "lettuce", allergenUids: [] }, + { uid: "arugula", allergenUids: [] }, + { uid: "watercress", allergenUids: [] }, + { uid: "leek", allergenUids: [] }, + { uid: "celery", allergenUids: ["celery"] }, + { uid: "radish", allergenUids: [] }, + { uid: "beetroot", allergenUids: [] }, + { uid: "turnip", allergenUids: [] }, + { uid: "parsnip", allergenUids: [] }, + { uid: "greenBean", allergenUids: [] }, + { uid: "pea", allergenUids: [] }, + { uid: "corn", allergenUids: [] }, + { uid: "artichoke", allergenUids: [] }, + { uid: "fennel", allergenUids: [] }, + { uid: "endive", allergenUids: [] }, + { uid: "pumpkin", allergenUids: [] }, + { uid: "butternutSquash", allergenUids: [] }, + { uid: "asparagus", allergenUids: [] }, + { uid: "avocado", allergenUids: [] }, + { uid: "potato", allergenUids: [] }, + { uid: "sweetPotato", allergenUids: [] }, + { uid: "cherryTomato", allergenUids: [] }, + { uid: "bokChoy", allergenUids: [] }, + { uid: "soybeanSprouts", allergenUids: ["soy"] }, + { uid: "shiitake", allergenUids: [] }, + { uid: "daikon", allergenUids: [] }, + { uid: "freshGreenChili", allergenUids: [] }, + { uid: "cardoon", allergenUids: [] }, // Second Ciqual pass — see the PR description. - { name: "Chicorée rouge", allergenNames: [] }, - { name: "Chou romanesco", allergenNames: [] }, - { name: "Chou-rave", allergenNames: [] }, - { name: "Chou chinois", allergenNames: [] }, - { name: "Céleri-rave", allergenNames: ["Céleri"] }, - { name: "Gombo", allergenNames: [] }, - { name: "Oignon nouveau", allergenNames: [] }, - { name: "Potimarron", allergenNames: [] }, - { name: "Rutabaga", allergenNames: [] }, - { name: "Salicorne", allergenNames: [] }, - { name: "Salsifis", allergenNames: [] }, - { name: "Mâche", allergenNames: [] }, - { name: "Scarole", allergenNames: [] }, + { uid: "radicchio", allergenUids: [] }, + { uid: "romanesco", allergenUids: [] }, + { uid: "kohlrabi", allergenUids: [] }, + { uid: "napaCabbage", allergenUids: [] }, + { uid: "celeriac", allergenUids: ["celery"] }, + { uid: "okra", allergenUids: [] }, + { uid: "springOnion", allergenUids: [] }, + { uid: "redKuriSquash", allergenUids: [] }, + { uid: "rutabaga", allergenUids: [] }, + { uid: "samphire", allergenUids: [] }, + { uid: "salsify", allergenUids: [] }, + { uid: "lambsLettuce", allergenUids: [] }, + { uid: "escarole", allergenUids: [] }, ], }, { - category: "PRODUITS_FRAIS", - subcategory: "FRUITS", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "freshProduce", + subcategory: "fruits", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "FRUIT", items: [ - { name: "Citron", allergenNames: [] }, - { name: "Citron vert", allergenNames: [] }, - { name: "Pomme", allergenNames: [] }, - { name: "Poire", allergenNames: [] }, - { name: "Banane", allergenNames: [] }, - { name: "Orange", allergenNames: [] }, - { name: "Clémentine", allergenNames: [] }, - { name: "Pamplemousse", allergenNames: [] }, - { name: "Fraise", allergenNames: [] }, - { name: "Framboise", allergenNames: [] }, - { name: "Myrtille", allergenNames: [] }, - { name: "Mûre", allergenNames: [] }, - { name: "Cerise", allergenNames: [] }, - { name: "Abricot", allergenNames: [] }, - { name: "Pêche", allergenNames: [] }, - { name: "Prune", allergenNames: [] }, - { name: "Raisin", allergenNames: [] }, - { name: "Melon", allergenNames: [] }, - { name: "Pastèque", allergenNames: [] }, - { name: "Ananas", allergenNames: [] }, - { name: "Mangue", allergenNames: [] }, - { name: "Kiwi", allergenNames: [] }, - { name: "Figue", allergenNames: [] }, - { name: "Datte", allergenNames: [] }, - { name: "Litchi", allergenNames: [] }, - { name: "Grenade", allergenNames: [] }, - { name: "Rhubarbe", allergenNames: [] }, - { name: "Coing", allergenNames: [] }, - { name: "Cassis", allergenNames: [] }, - { name: "Canneberge", allergenNames: [] }, + { uid: "lemon", allergenUids: [] }, + { uid: "lime", allergenUids: [] }, + { uid: "apple", allergenUids: [] }, + { uid: "pear", allergenUids: [] }, + { uid: "banana", allergenUids: [] }, + { uid: "orange", allergenUids: [] }, + { uid: "clementine", allergenUids: [] }, + { uid: "grapefruit", allergenUids: [] }, + { uid: "strawberry", allergenUids: [] }, + { uid: "raspberry", allergenUids: [] }, + { uid: "blueberry", allergenUids: [] }, + { uid: "blackberry", allergenUids: [] }, + { uid: "cherry", allergenUids: [] }, + { uid: "apricot", allergenUids: [] }, + { uid: "peach", allergenUids: [] }, + { uid: "plum", allergenUids: [] }, + { uid: "grape", allergenUids: [] }, + { uid: "melon", allergenUids: [] }, + { uid: "watermelon", allergenUids: [] }, + { uid: "pineapple", allergenUids: [] }, + { uid: "mango", allergenUids: [] }, + { uid: "kiwi", allergenUids: [] }, + { uid: "fig", allergenUids: [] }, + { uid: "date", allergenUids: [] }, + { uid: "lychee", allergenUids: [] }, + { uid: "pomegranate", allergenUids: [] }, + { uid: "rhubarb", allergenUids: [] }, + { uid: "quince", allergenUids: [] }, + { uid: "blackcurrant", allergenUids: [] }, + { uid: "cranberry", allergenUids: [] }, // Second Ciqual pass. - { name: "Groseille", allergenNames: [] }, - { name: "Kaki", allergenNames: [] }, - { name: "Nectarine", allergenNames: [] }, - { name: "Tamarin", allergenNames: [] }, + { uid: "redcurrant", allergenUids: [] }, + { uid: "persimmon", allergenUids: [] }, + { uid: "nectarine", allergenUids: [] }, + { uid: "tamarind", allergenUids: [] }, ], }, { - category: "PRODUITS_FRAIS", - subcategory: "HERBES_FRAICHES", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "freshProduce", + subcategory: "freshHerbs", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "HERB", items: [ - { name: "Basilic", allergenNames: [] }, - { name: "Persil", allergenNames: [] }, - { name: "Thym", allergenNames: [] }, - { name: "Romarin", allergenNames: [] }, - { name: "Laurier", allergenNames: [] }, - { name: "Ciboulette", allergenNames: [] }, - { name: "Coriandre fraîche", allergenNames: [] }, - { name: "Menthe", allergenNames: [] }, - { name: "Origan", allergenNames: [] }, - { name: "Aneth", allergenNames: [] }, - { name: "Estragon", allergenNames: [] }, - { name: "Sarriette", allergenNames: [] }, - { name: "Marjolaine", allergenNames: [] }, - { name: "Sauge", allergenNames: [] }, - { name: "Cerfeuil", allergenNames: [] }, - { name: "Gingembre", allergenNames: [] }, - { name: "Citronnelle", allergenNames: [] }, - { name: "Combava", allergenNames: [] }, + { uid: "basil", allergenUids: [] }, + { uid: "parsley", allergenUids: [] }, + { uid: "thyme", allergenUids: [] }, + { uid: "rosemary", allergenUids: [] }, + { uid: "bayLeaf", allergenUids: [] }, + { uid: "chives", allergenUids: [] }, + { uid: "freshCilantro", allergenUids: [] }, + { uid: "mint", allergenUids: [] }, + { uid: "oregano", allergenUids: [] }, + { uid: "dill", allergenUids: [] }, + { uid: "tarragon", allergenUids: [] }, + { uid: "savory", allergenUids: [] }, + { uid: "marjoram", allergenUids: [] }, + { uid: "sage", allergenUids: [] }, + { uid: "chervil", allergenUids: [] }, + { uid: "ginger", allergenUids: [] }, + { uid: "lemongrass", allergenUids: [] }, + { uid: "kaffirLime", allergenUids: [] }, ], }, // ========================================================================= // Boucherie & poissonnerie // ========================================================================= { - category: "BOUCHERIE_POISSONNERIE", - subcategory: "VIANDES", + category: "meatAndSeafood", + subcategory: "meats", defaultDiets: [], defaultIcon: "MEAT", items: [ - { name: "Lapin", allergenNames: [] }, - { name: "Bœuf haché", allergenNames: [] }, - { name: "Steak de bœuf", allergenNames: [] }, - { name: "Rôti de bœuf", allergenNames: [] }, - { name: "Escalope de veau", allergenNames: [] }, - { name: "Filet mignon de porc", allergenNames: [] }, - { name: "Côte de porc", allergenNames: [] }, - { name: "Agneau", allergenNames: [] }, - { name: "Gigot d'agneau", allergenNames: [] }, - { name: "Lardons", allergenNames: [] }, - { name: "Bacon", allergenNames: [] }, - { name: "Jambon blanc", allergenNames: [] }, - { name: "Jambon cru", allergenNames: [] }, - { name: "Saucisse", allergenNames: [] }, - { name: "Chorizo", allergenNames: [] }, - { name: "Merguez", allergenNames: [] }, - { name: "Prosciutto", allergenNames: [] }, - { name: "Pancetta", allergenNames: [] }, - { name: "Mortadelle", allergenNames: [] }, - { name: "Salami", allergenNames: [] }, + { uid: "rabbit", allergenUids: [] }, + { uid: "groundBeef", allergenUids: [] }, + { uid: "beefSteak", allergenUids: [] }, + { uid: "beefRoast", allergenUids: [] }, + { uid: "vealCutlet", allergenUids: [] }, + { uid: "porkTenderloin", allergenUids: [] }, + { uid: "porkChop", allergenUids: [] }, + { uid: "lamb", allergenUids: [] }, + { uid: "legOfLamb", allergenUids: [] }, + { uid: "baconLardons", allergenUids: [] }, + { uid: "bacon", allergenUids: [] }, + { uid: "ham", allergenUids: [] }, + { uid: "curedHam", allergenUids: [] }, + { uid: "sausage", allergenUids: [] }, + { uid: "chorizo", allergenUids: [] }, + { uid: "merguez", allergenUids: [] }, + { uid: "prosciutto", allergenUids: [] }, + { uid: "pancetta", allergenUids: [] }, + { uid: "mortadella", allergenUids: [] }, + { uid: "salami", allergenUids: [] }, // Added from the Ciqual 2025 "aliments moyens" table — French // charcuterie/offal/game staples the pantry list was missing (see // the PR description for how this batch was sourced/curated). - { name: "Andouille", allergenNames: [] }, - { name: "Andouillette", allergenNames: [] }, - { name: "Boudin blanc", allergenNames: [] }, - { name: "Boudin noir", allergenNames: [] }, - { name: "Cervelas", allergenNames: [] }, - { name: "Rillettes", allergenNames: [] }, - { name: "Saucisson sec", allergenNames: [] }, - { name: "Jambon de Bayonne", allergenNames: [] }, - { name: "Coppa", allergenNames: [] }, - { name: "Rosette (saucisson)", allergenNames: [] }, - { name: "Foie de veau", allergenNames: [] }, - { name: "Rognons de veau", allergenNames: [] }, - { name: "Cervelle de veau", allergenNames: [] }, - { name: "Ris de veau", allergenNames: [] }, - { name: "Langue de bœuf", allergenNames: [] }, - { name: "Tripes", allergenNames: [] }, - { name: "Cerf", allergenNames: [] }, - { name: "Chevreuil", allergenNames: [] }, - { name: "Sanglier", allergenNames: [] }, + { uid: "andouille", allergenUids: [] }, + { uid: "andouillette", allergenUids: [] }, + { uid: "whitePudding", allergenUids: [] }, + { uid: "blackPudding", allergenUids: [] }, + { uid: "cervelat", allergenUids: [] }, + { uid: "rillettes", allergenUids: [] }, + { uid: "dryCuredSausage", allergenUids: [] }, + { uid: "bayonneHam", allergenUids: [] }, + { uid: "coppa", allergenUids: [] }, + { uid: "rosetteSausage", allergenUids: [] }, + { uid: "vealLiver", allergenUids: [] }, + { uid: "vealKidneys", allergenUids: [] }, + { uid: "vealBrain", allergenUids: [] }, + { uid: "vealSweetbread", allergenUids: [] }, + { uid: "beefTongue", allergenUids: [] }, + { uid: "tripe", allergenUids: [] }, + { uid: "venison", allergenUids: [] }, + { uid: "roeDeer", allergenUids: [] }, + { uid: "wildBoar", allergenUids: [] }, // Second Ciqual pass. - { name: "Cheval", allergenNames: [] }, - { name: "Cœur de bœuf", allergenNames: [] }, - { name: "Foie gras", allergenNames: [] }, - { name: "Museau de bœuf", allergenNames: [] }, - { name: "Viande des Grisons", allergenNames: [] }, + { uid: "horseMeat", allergenUids: [] }, + { uid: "beefHeart", allergenUids: [] }, + { uid: "foieGras", allergenUids: [] }, + { uid: "beefMuzzle", allergenUids: [] }, + { uid: "grisonsDriedBeef", allergenUids: [] }, ], }, { - category: "BOUCHERIE_POISSONNERIE", - subcategory: "VOLAILLES", + category: "meatAndSeafood", + subcategory: "poultry", defaultDiets: [], defaultIcon: "POULTRY", items: [ - { name: "Poulet", allergenNames: [] }, - { name: "Dinde", allergenNames: [] }, - { name: "Canard", allergenNames: [] }, - { name: "Magret de canard", allergenNames: [] }, + { uid: "chicken", allergenUids: [] }, + { uid: "turkey", allergenUids: [] }, + { uid: "duck", allergenUids: [] }, + { uid: "duckBreast", allergenUids: [] }, // Ciqual 2025 additions — see the VIANDES group above. - { name: "Caille", allergenNames: [] }, - { name: "Pintade", allergenNames: [] }, - { name: "Oie", allergenNames: [] }, - { name: "Foie de volaille", allergenNames: [] }, + { uid: "quail", allergenUids: [] }, + { uid: "guineaFowl", allergenUids: [] }, + { uid: "goose", allergenUids: [] }, + { uid: "poultryLiver", allergenUids: [] }, // Second Ciqual pass. - { name: "Chapon", allergenNames: [] }, - { name: "Pigeon", allergenNames: [] }, - { name: "Faisan", allergenNames: [] }, + { uid: "capon", allergenUids: [] }, + { uid: "pigeon", allergenUids: [] }, + { uid: "pheasant", allergenUids: [] }, ], }, { - category: "BOUCHERIE_POISSONNERIE", - subcategory: "POISSONS", - defaultDiets: ["Pescétarien"], + category: "meatAndSeafood", + subcategory: "fish", + defaultDiets: ["pescatarian"], defaultIcon: "FISH", items: [ - { name: "Saumon", allergenNames: ["Poissons"] }, - { name: "Thon", allergenNames: ["Poissons"] }, - { name: "Cabillaud", allergenNames: ["Poissons"] }, - { name: "Truite", allergenNames: ["Poissons"] }, - { name: "Sardine", allergenNames: ["Poissons"] }, - { name: "Anchois", allergenNames: ["Poissons"] }, - { name: "Merlan", allergenNames: ["Poissons"] }, - { name: "Surimi", allergenNames: ["Poissons"] }, - { name: "Bar (loup de mer)", allergenNames: ["Poissons"] }, - { name: "Dorade", allergenNames: ["Poissons"] }, - { name: "Sole", allergenNames: ["Poissons"] }, - { name: "Turbot", allergenNames: ["Poissons"] }, - { name: "Merlu", allergenNames: ["Poissons"] }, - { name: "Colin", allergenNames: ["Poissons"] }, - { name: "Lieu noir", allergenNames: ["Poissons"] }, - { name: "Églefin", allergenNames: ["Poissons"] }, - { name: "Maquereau", allergenNames: ["Poissons"] }, - { name: "Hareng", allergenNames: ["Poissons"] }, - { name: "Rouget", allergenNames: ["Poissons"] }, - { name: "Raie", allergenNames: ["Poissons"] }, - { name: "Lotte", allergenNames: ["Poissons"] }, - { name: "Flétan", allergenNames: ["Poissons"] }, - { name: "Espadon", allergenNames: ["Poissons"] }, - { name: "Carpe", allergenNames: ["Poissons"] }, - { name: "Brochet", allergenNames: ["Poissons"] }, - { name: "Perche", allergenNames: ["Poissons"] }, - { name: "Tilapia", allergenNames: ["Poissons"] }, - { name: "Panga", allergenNames: ["Poissons"] }, - { name: "Saumon fumé", allergenNames: ["Poissons"] }, - { name: "Poisson séché", allergenNames: ["Poissons"] }, + { uid: "salmon", allergenUids: ["fish"] }, + { uid: "tuna", allergenUids: ["fish"] }, + { uid: "cod", allergenUids: ["fish"] }, + { uid: "trout", allergenUids: ["fish"] }, + { uid: "sardine", allergenUids: ["fish"] }, + { uid: "anchovy", allergenUids: ["fish"] }, + { uid: "whiting", allergenUids: ["fish"] }, + { uid: "surimi", allergenUids: ["fish"] }, + { uid: "seaBass", allergenUids: ["fish"] }, + { uid: "seaBream", allergenUids: ["fish"] }, + { uid: "sole", allergenUids: ["fish"] }, + { uid: "turbot", allergenUids: ["fish"] }, + { uid: "hake", allergenUids: ["fish"] }, + { uid: "pollock", allergenUids: ["fish"] }, + { uid: "saithe", allergenUids: ["fish"] }, + { uid: "haddock", allergenUids: ["fish"] }, + { uid: "mackerel", allergenUids: ["fish"] }, + { uid: "herring", allergenUids: ["fish"] }, + { uid: "redMullet", allergenUids: ["fish"] }, + { uid: "skate", allergenUids: ["fish"] }, + { uid: "monkfish", allergenUids: ["fish"] }, + { uid: "halibut", allergenUids: ["fish"] }, + { uid: "swordfish", allergenUids: ["fish"] }, + { uid: "carp", allergenUids: ["fish"] }, + { uid: "pike", allergenUids: ["fish"] }, + { uid: "perch", allergenUids: ["fish"] }, + { uid: "tilapia", allergenUids: ["fish"] }, + { uid: "pangasius", allergenUids: ["fish"] }, + { uid: "smokedSalmon", allergenUids: ["fish"] }, + { uid: "driedFish", allergenUids: ["fish"] }, // Ciqual 2025 additions. - { name: "Anguille", allergenNames: ["Poissons"] }, - { name: "Carrelet (ou plie)", allergenNames: ["Poissons"] }, + { uid: "eel", allergenUids: ["fish"] }, + { uid: "plaice", allergenUids: ["fish"] }, // Second Ciqual pass. - { name: "Morue", allergenNames: ["Poissons"] }, - { name: "Limande", allergenNames: ["Poissons"] }, - { name: "Rascasse", allergenNames: ["Poissons"] }, + { uid: "saltCod", allergenUids: ["fish"] }, + { uid: "lemonSole", allergenUids: ["fish"] }, + { uid: "scorpionfish", allergenUids: ["fish"] }, ], }, { - category: "BOUCHERIE_POISSONNERIE", - subcategory: "CRUSTACES_FRUITS_DE_MER", - defaultDiets: ["Pescétarien"], + category: "meatAndSeafood", + subcategory: "shellfish", + defaultDiets: ["pescatarian"], defaultIcon: "SHELLFISH", items: [ - { name: "Crevettes", allergenNames: ["Crustacés"] }, - { name: "Langoustines", allergenNames: ["Crustacés"] }, - { name: "Homard", allergenNames: ["Crustacés"] }, - { name: "Crabe", allergenNames: ["Crustacés"] }, - { name: "Langouste", allergenNames: ["Crustacés"] }, - { name: "Moules", allergenNames: ["Mollusques"] }, - { name: "Huîtres", allergenNames: ["Mollusques"] }, - { name: "Saint-Jacques", allergenNames: ["Mollusques"] }, - { name: "Calamar", allergenNames: ["Mollusques"] }, - { name: "Poulpe", allergenNames: ["Mollusques"] }, - { name: "Palourdes", allergenNames: ["Mollusques"] }, - { name: "Bulots", allergenNames: ["Mollusques"] }, + { uid: "shrimp", allergenUids: ["crustaceans"] }, + { uid: "langoustine", allergenUids: ["crustaceans"] }, + { uid: "lobster", allergenUids: ["crustaceans"] }, + { uid: "crab", allergenUids: ["crustaceans"] }, + { uid: "spinyLobster", allergenUids: ["crustaceans"] }, + { uid: "mussels", allergenUids: ["molluscs"] }, + { uid: "oysters", allergenUids: ["molluscs"] }, + { uid: "scallops", allergenUids: ["molluscs"] }, + { uid: "squid", allergenUids: ["molluscs"] }, + { uid: "octopus", allergenUids: ["molluscs"] }, + { uid: "clams", allergenUids: ["molluscs"] }, + { uid: "whelks", allergenUids: ["molluscs"] }, // Ciqual 2025 additions. - { name: "Araignée de mer", allergenNames: ["Crustacés"] }, - { name: "Bigorneau", allergenNames: ["Mollusques"] }, + { uid: "spiderCrab", allergenUids: ["crustaceans"] }, + { uid: "periwinkle", allergenUids: ["molluscs"] }, // Second Ciqual pass. - { name: "Écrevisse", allergenNames: ["Crustacés"] }, - { name: "Crevette grise", allergenNames: ["Crustacés"] }, - { name: "Coque", allergenNames: ["Mollusques"] }, - { name: "Escargot", allergenNames: ["Mollusques"] }, - { name: "Seiche", allergenNames: ["Mollusques"] }, + { uid: "crayfish", allergenUids: ["crustaceans"] }, + { uid: "greyShrimp", allergenUids: ["crustaceans"] }, + { uid: "cockle", allergenUids: ["molluscs"] }, + { uid: "snail", allergenUids: ["molluscs"] }, + { uid: "cuttlefish", allergenUids: ["molluscs"] }, ], }, // ========================================================================= // Épicerie sèche // ========================================================================= { - category: "EPICERIE_SECHE", - subcategory: "FECULENTS", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "dryGoods", + subcategory: "starches", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "GRAIN", items: [ - { name: "Semoule", allergenNames: ["Gluten"] }, - { name: "Couscous", allergenNames: ["Gluten"] }, - { name: "Boulgour", allergenNames: ["Gluten"] }, - { name: "Polenta", allergenNames: [] }, - { name: "Quinoa", allergenNames: [] }, - { name: "Pâtes", allergenNames: ["Gluten"] }, - { name: "Pâtes complètes", allergenNames: ["Gluten"] }, - { name: "Riz", allergenNames: [] }, - { name: "Riz basmati", allergenNames: [] }, - { name: "Riz complet", allergenNames: [] }, - { name: "Flocons d'avoine", allergenNames: ["Gluten"] }, - { name: "Spaghetti", allergenNames: ["Gluten"] }, - { name: "Penne", allergenNames: ["Gluten"] }, - { name: "Tagliatelles", allergenNames: ["Gluten"] }, - { name: "Lasagnes (feuilles)", allergenNames: ["Gluten"] }, - { name: "Gnocchi", allergenNames: ["Gluten"] }, - { name: "Riz arborio", allergenNames: [] }, - { name: "Nouilles de riz", allergenNames: [] }, - { name: "Nouilles udon", allergenNames: ["Gluten"] }, - { name: "Nouilles soba", allergenNames: ["Gluten"] }, - { name: "Nouilles chinoises", allergenNames: ["Gluten"] }, - { name: "Vermicelles de riz", allergenNames: [] }, - { name: "Vermicelles de soja", allergenNames: [] }, - { name: "Riz gluant", allergenNames: [] }, - { name: "Riz à sushi", allergenNames: [] }, - { name: "Riz jasmin", allergenNames: [] }, + { uid: "semolina", allergenUids: ["gluten"] }, + { uid: "couscous", allergenUids: ["gluten"] }, + { uid: "bulgur", allergenUids: ["gluten"] }, + { uid: "polenta", allergenUids: [] }, + { uid: "quinoa", allergenUids: [] }, + { uid: "pasta", allergenUids: ["gluten"] }, + { uid: "wholeWheatPasta", allergenUids: ["gluten"] }, + { uid: "rice", allergenUids: [] }, + { uid: "basmatiRice", allergenUids: [] }, + { uid: "brownRice", allergenUids: [] }, + { uid: "oats", allergenUids: ["gluten"] }, + { uid: "spaghetti", allergenUids: ["gluten"] }, + { uid: "penne", allergenUids: ["gluten"] }, + { uid: "tagliatelle", allergenUids: ["gluten"] }, + { uid: "lasagnaSheets", allergenUids: ["gluten"] }, + { uid: "gnocchi", allergenUids: ["gluten"] }, + { uid: "arborioRice", allergenUids: [] }, + { uid: "riceNoodles", allergenUids: [] }, + { uid: "udonNoodles", allergenUids: ["gluten"] }, + { uid: "sobaNoodles", allergenUids: ["gluten"] }, + { uid: "chineseNoodles", allergenUids: ["gluten"] }, + { uid: "riceVermicelli", allergenUids: [] }, + { uid: "soyVermicelli", allergenUids: [] }, + { uid: "stickyRice", allergenUids: [] }, + { uid: "sushiRice", allergenUids: [] }, + { uid: "jasmineRice", allergenUids: [] }, ], }, { - category: "EPICERIE_SECHE", - subcategory: "LEGUMINEUSES", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "dryGoods", + subcategory: "legumes", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "LEGUME", items: [ - { name: "Lentilles vertes", allergenNames: [] }, - { name: "Lentilles corail", allergenNames: [] }, - { name: "Pois chiches", allergenNames: [] }, - { name: "Haricots blancs", allergenNames: [] }, - { name: "Haricots rouges", allergenNames: [] }, - { name: "Haricots noirs", allergenNames: [] }, - { name: "Pois cassés", allergenNames: [] }, - { name: "Fèves", allergenNames: [] }, - { name: "Edamame", allergenNames: ["Soja"] }, - { name: "Haricots pinto", allergenNames: [] }, + { uid: "greenLentils", allergenUids: [] }, + { uid: "redLentils", allergenUids: [] }, + { uid: "chickpeas", allergenUids: [] }, + { uid: "whiteBeans", allergenUids: [] }, + { uid: "kidneyBeans", allergenUids: [] }, + { uid: "blackBeans", allergenUids: [] }, + { uid: "splitPeas", allergenUids: [] }, + { uid: "favaBeans", allergenUids: [] }, + { uid: "edamame", allergenUids: ["soy"] }, + { uid: "pintoBeans", allergenUids: [] }, // Second Ciqual pass. - { name: "Haricots flageolets", allergenNames: [] }, - { name: "Lentilles blondes", allergenNames: [] }, + { uid: "flageoletBeans", allergenUids: [] }, + { uid: "goldenLentils", allergenUids: [] }, ], }, { - category: "EPICERIE_SECHE", - subcategory: "GRAINES_FRUITS_SECS", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "dryGoods", + subcategory: "nutsAndSeeds", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "NUT_SEED", items: [ - { name: "Cacahuètes", allergenNames: ["Arachides"] }, - { name: "Amandes", allergenNames: ["Fruits à coque"] }, - { name: "Noix", allergenNames: ["Fruits à coque"] }, - { name: "Noisettes", allergenNames: ["Fruits à coque"] }, - { name: "Noix de cajou", allergenNames: ["Fruits à coque"] }, - { name: "Pistaches", allergenNames: ["Fruits à coque"] }, - { name: "Noix de pécan", allergenNames: ["Fruits à coque"] }, - { name: "Poudre d'amande", allergenNames: ["Fruits à coque"] }, - { name: "Pignons de pin", allergenNames: [] }, - { name: "Graines de tournesol", allergenNames: [] }, - { name: "Graines de courge", allergenNames: [] }, - { name: "Noix de coco râpée", allergenNames: [] }, - { name: "Raisins secs", allergenNames: ["Sulfites"] }, - { name: "Pruneaux", allergenNames: ["Sulfites"] }, - { name: "Abricots secs", allergenNames: ["Sulfites"] }, - { name: "Graines de sésame", allergenNames: ["Graines de sésame"] }, + { uid: "peanutsShelled", allergenUids: ["peanuts"] }, + { uid: "almonds", allergenUids: ["treeNuts"] }, + { uid: "walnuts", allergenUids: ["treeNuts"] }, + { uid: "hazelnuts", allergenUids: ["treeNuts"] }, + { uid: "cashews", allergenUids: ["treeNuts"] }, + { uid: "pistachios", allergenUids: ["treeNuts"] }, + { uid: "pecans", allergenUids: ["treeNuts"] }, + { uid: "almondPowder", allergenUids: ["treeNuts"] }, + { uid: "pineNuts", allergenUids: [] }, + { uid: "sunflowerSeeds", allergenUids: [] }, + { uid: "pumpkinSeeds", allergenUids: [] }, + { uid: "shreddedCoconut", allergenUids: [] }, + { uid: "raisins", allergenUids: ["sulfites"] }, + { uid: "prunes", allergenUids: ["sulfites"] }, + { uid: "driedApricots", allergenUids: ["sulfites"] }, + { uid: "sesameSeeds", allergenUids: ["sesameSeeds"] }, ], }, { - category: "EPICERIE_SECHE", - subcategory: "AUTRES", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "dryGoods", + subcategory: "other", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "JAR", items: [ - { name: "Champignons noirs", allergenNames: [] }, - { name: "Algue nori", allergenNames: [] }, - { name: "Algue wakamé", allergenNames: [] }, - { name: "Algue kombu", allergenNames: [] }, - { name: "Pousses de bambou", allergenNames: [] }, - { name: "Châtaignes d'eau", allergenNames: [] }, + { uid: "blackMushrooms", allergenUids: [] }, + { uid: "noriSeaweed", allergenUids: [] }, + { uid: "wakameSeaweed", allergenUids: [] }, + { uid: "kombuSeaweed", allergenUids: [] }, + { uid: "bambooShoots", allergenUids: [] }, + { uid: "waterChestnuts", allergenUids: [] }, ], }, // ========================================================================= // Boulangerie // ========================================================================= { - category: "BOULANGERIE", - subcategory: "PAINS", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "bakery", + subcategory: "breads", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "BREAD", items: [ - { name: "Pain", allergenNames: ["Gluten"] }, - { name: "Pain de mie", allergenNames: ["Gluten"] }, - { name: "Pain complet", allergenNames: ["Gluten"] }, - { name: "Baguette", allergenNames: ["Gluten"] }, - { name: "Pain de seigle", allergenNames: ["Gluten"] }, - { name: "Chapelure", allergenNames: ["Gluten"] }, + { uid: "bread", reproducible: true, allergenUids: ["gluten"] }, + { uid: "sandwichBread", reproducible: true, allergenUids: ["gluten"] }, + { uid: "wholeWheatBread", allergenUids: ["gluten"] }, + { uid: "baguette", allergenUids: ["gluten"] }, + { uid: "ryeBread", allergenUids: ["gluten"] }, + { uid: "breadcrumbs", reproducible: true, allergenUids: ["gluten"] }, { - name: "Pain à burger", - allergenNames: ["Gluten", "Lait", "Œufs"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "burgerBun", + reproducible: true, + allergenUids: ["gluten", "milk", "eggs"], + dietUids: ["vegetarian", "pescatarian"], }, { - name: "Pain brioché", - allergenNames: ["Gluten", "Lait", "Œufs"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "briocheBun", + allergenUids: ["gluten", "milk", "eggs"], + dietUids: ["vegetarian", "pescatarian"], }, - { name: "Pain à hot-dog", allergenNames: ["Gluten"] }, - { name: "Pain pita", allergenNames: ["Gluten"] }, - { name: "Pain bagel", allergenNames: ["Gluten"] }, - { name: "Naan", allergenNames: ["Gluten"] }, - { name: "Pain wrap", allergenNames: ["Gluten"] }, + { uid: "hotDogBun", reproducible: true, allergenUids: ["gluten"] }, + { uid: "pitaBread", reproducible: true, allergenUids: ["gluten"] }, + { uid: "bagel", reproducible: true, allergenUids: ["gluten"] }, + { uid: "naan", reproducible: true, allergenUids: ["gluten"] }, + { uid: "wrapBread", allergenUids: ["gluten"] }, { - name: "Pain viennois", - allergenNames: ["Gluten", "Lait"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "vienneseBread", + allergenUids: ["gluten", "milk"], + dietUids: ["vegetarian", "pescatarian"], }, - { name: "Pain de campagne", allergenNames: ["Gluten"] }, - { name: "Pain aux céréales", allergenNames: ["Gluten"] }, - { name: "Petit pain", allergenNames: ["Gluten"] }, - { name: "Pain suédois", allergenNames: ["Gluten"] }, - { name: "Pain sans gluten", allergenNames: [] }, - { name: "Biscotte", allergenNames: ["Gluten"] }, - { name: "Croûtons", allergenNames: ["Gluten"] }, - { name: "Focaccia", allergenNames: ["Gluten"] }, - { name: "Ciabatta", allergenNames: ["Gluten"] }, - { name: "Tortilla de maïs", allergenNames: [] }, - { name: "Tortilla de blé", allergenNames: ["Gluten"] }, + { uid: "countryBread", allergenUids: ["gluten"] }, + { uid: "multigrainBread", allergenUids: ["gluten"] }, + { uid: "breadRoll", allergenUids: ["gluten"] }, + { uid: "swedishBread", allergenUids: ["gluten"] }, + { uid: "glutenFreeBread", allergenUids: [] }, + { uid: "rusk", allergenUids: ["gluten"] }, + { uid: "croutons", reproducible: true, allergenUids: ["gluten"] }, + { uid: "focaccia", reproducible: true, allergenUids: ["gluten"] }, + { uid: "ciabatta", reproducible: true, allergenUids: ["gluten"] }, + { uid: "cornTortilla", allergenUids: [] }, + { uid: "wheatTortilla", allergenUids: ["gluten"] }, // Second Ciqual pass. - { name: "Gressin", allergenNames: ["Gluten"] }, + { uid: "breadstick", reproducible: true, allergenUids: ["gluten"] }, ], }, { - category: "BOULANGERIE", - subcategory: "PATES_A_CUIRE", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "bakery", + subcategory: "rawDough", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "DOUGH", items: [ { - name: "Pâte feuilletée", - allergenNames: ["Gluten", "Lait"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "puffPastry", + reproducible: true, + allergenUids: ["gluten", "milk"], + dietUids: ["vegetarian", "pescatarian"], }, { - name: "Pâte brisée", - allergenNames: ["Gluten", "Lait"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "shortcrustPastry", + reproducible: true, + allergenUids: ["gluten", "milk"], + dietUids: ["vegetarian", "pescatarian"], }, - { name: "Pâte à pizza", allergenNames: ["Gluten"] }, + { uid: "pizzaDough", reproducible: true, allergenUids: ["gluten"] }, { - name: "Pâte à tarte sablée", - allergenNames: ["Gluten", "Lait"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "sweetShortcrustPastry", + reproducible: true, + allergenUids: ["gluten", "milk"], + dietUids: ["vegetarian", "pescatarian"], }, ], }, @@ -572,437 +590,467 @@ export const INGREDIENT_GROUPS: Array<{ // Crémerie & fromage // ========================================================================= { - category: "CREMERIE_FROMAGE", - subcategory: "PRODUITS_LAITIERS", - defaultDiets: ["Végétarien", "Pescétarien"], + category: "dairyAndCheese", + subcategory: "dairy", + defaultDiets: ["vegetarian", "pescatarian"], defaultIcon: "MILK", items: [ - { name: "Lait", allergenNames: ["Lait"] }, - { name: "Beurre", allergenNames: ["Lait"] }, - { name: "Crème fraîche", allergenNames: ["Lait"] }, - { name: "Crème liquide", allergenNames: ["Lait"] }, - { name: "Fromage", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Emmental", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Gruyère", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Parmesan", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Mozzarella", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Chèvre (fromage)", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Feta", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Comté", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Fromage blanc", allergenNames: ["Lait"] }, - { name: "Mascarpone", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Yaourt", allergenNames: ["Lait"] }, - { name: "Burrata", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Ricotta", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Pecorino", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Gorgonzola", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Cheddar", icon: "CHEESE", allergenNames: ["Lait"] }, + { uid: "milk", allergenUids: ["milk"] }, + { uid: "butter", allergenUids: ["milk"] }, + { uid: "cremeFraiche", allergenUids: ["milk"] }, + { uid: "liquidCream", allergenUids: ["milk"] }, + { uid: "cheese", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "emmental", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "gruyere", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "parmesan", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "mozzarella", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "goatCheese", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "feta", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "comte", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "fromageBlanc", allergenUids: ["milk"] }, + { uid: "mascarpone", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "yogurt", allergenUids: ["milk"] }, + { uid: "burrata", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "ricotta", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "pecorino", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "gorgonzola", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "cheddar", icon: "CHEESE", allergenUids: ["milk"] }, // Ciqual 2025 additions — classic French regional cheeses the // catalog only had a handful of internationally-known ones for // (Emmental, Parmesan, Mozzarella…), missing the actual French // aisle staples Ciqual's "aliments moyens" table surfaced. - { name: "Brie", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Camembert", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Roquefort", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Munster", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Reblochon", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Cantal", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Beaufort", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Saint-Nectaire", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Bleu (fromage)", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Cancoillotte", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Tomme", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Époisses", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Chaource", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Livarot", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Pont-l'Évêque", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Morbier", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Raclette (fromage)", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Fourme d'Ambert", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Salers", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Ossau-Iraty", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Vacherin", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Saint-Marcellin", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Neufchâtel", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Crottin de Chavignol", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Abondance", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Carré de l'Est", icon: "CHEESE", allergenNames: ["Lait"] }, + { uid: "brie", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "camembert", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "roquefort", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "munster", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "reblochon", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "cantal", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "beaufort", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "saintNectaire", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "blueCheese", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "cancoillotte", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "tomme", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "epoisses", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "chaource", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "livarot", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "pontLeveque", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "morbier", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "racletteCheese", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "fourmeDAmbert", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "salers", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "ossauIraty", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "vacherin", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "saintMarcellin", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "neufchatel", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "crottinDeChavignol", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "abondanceCheese", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "carreDeLEst", icon: "CHEESE", allergenUids: ["milk"] }, // Second Ciqual pass. - { name: "Edam", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Gouda", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Mimolette", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Maroilles", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Mont d'or", icon: "CHEESE", allergenNames: ["Lait"] }, - { name: "Kéfir", allergenNames: ["Lait"] }, - { name: "Yaourt à la grecque", allergenNames: ["Lait"] }, + { uid: "edam", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "gouda", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "mimolette", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "maroilles", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "montDor", icon: "CHEESE", allergenUids: ["milk"] }, + { uid: "kefir", allergenUids: ["milk"] }, + { uid: "greekYogurt", allergenUids: ["milk"] }, ], }, { - category: "CREMERIE_FROMAGE", - subcategory: "OEUFS", - defaultDiets: ["Végétarien", "Pescétarien"], + category: "dairyAndCheese", + subcategory: "eggs", + defaultDiets: ["vegetarian", "pescatarian"], defaultIcon: "EGG", - items: [{ name: "Œuf", allergenNames: ["Œufs"] }], + items: [{ uid: "egg", allergenUids: ["eggs"] }], }, { - category: "CREMERIE_FROMAGE", - subcategory: "ALTERNATIVES", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "dairyAndCheese", + subcategory: "plantBasedAlternatives", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "SPROUT", items: [ - { name: "Lait de coco", icon: "MILK", allergenNames: [] }, - { name: "Crème de coco", icon: "MILK", allergenNames: [] }, - { name: "Lait d'amande", icon: "MILK", allergenNames: ["Fruits à coque"] }, - { name: "Lait d'avoine", icon: "MILK", allergenNames: ["Gluten"] }, - { name: "Tofu", allergenNames: ["Soja"] }, - { name: "Tofu soyeux", allergenNames: ["Soja"] }, + { uid: "coconutMilk", icon: "MILK", allergenUids: [] }, + { uid: "coconutCream", icon: "MILK", allergenUids: [] }, + { uid: "almondMilk", icon: "MILK", allergenUids: ["treeNuts"] }, + { uid: "oatMilk", icon: "MILK", allergenUids: ["gluten"] }, + { uid: "tofu", allergenUids: ["soy"] }, + { uid: "silkenTofu", allergenUids: ["soy"] }, ], }, // ========================================================================= // Condiments & épices // ========================================================================= { - category: "CONDIMENTS_EPICES", - subcategory: "EPICES", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "condimentsAndSpices", + subcategory: "spices", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "SPICE", items: [ - { name: "Herbes de Provence", allergenNames: [] }, - { name: "Poivre noir", allergenNames: [] }, - { name: "Paprika", allergenNames: [] }, - { name: "Piment d'Espelette", allergenNames: [] }, - { name: "Piment de Cayenne", allergenNames: [] }, - { name: "Cumin", allergenNames: [] }, - { name: "Curry (poudre)", allergenNames: [] }, - { name: "Curcuma", allergenNames: [] }, - { name: "Cannelle", allergenNames: [] }, - { name: "Muscade", allergenNames: [] }, - { name: "Safran", allergenNames: [] }, - { name: "Clou de girofle", allergenNames: [] }, - { name: "Vanille (gousse)", allergenNames: [] }, - { name: "Poivre blanc", allergenNames: [] }, - { name: "Poivre rose", allergenNames: [] }, - { name: "Poivre du Sichuan", allergenNames: [] }, - { name: "Paprika fumé", allergenNames: [] }, - { name: "Piment oiseau", allergenNames: [] }, - { name: "Baies de genièvre", allergenNames: [] }, - { name: "Anis étoilé (badiane)", allergenNames: [] }, - { name: "Anis vert", allergenNames: [] }, - { name: "Graines de fenouil", allergenNames: [] }, - { name: "Sumac", allergenNames: [] }, - { name: "Nigelle", allergenNames: [] }, - { name: "Quatre épices", allergenNames: [] }, - { name: "Colombo (poudre)", allergenNames: [] }, - { name: "Baharat", allergenNames: [] }, - { name: "Raifort", allergenNames: [] }, - { name: "Sel aux herbes", allergenNames: [] }, - { name: "Sel de céleri", allergenNames: ["Céleri"] }, - { name: "Fleur de sel", allergenNames: [] }, - { name: "Sel", allergenNames: [] }, - { name: "Cinq épices", allergenNames: [] }, - { name: "Garam masala", allergenNames: [] }, - { name: "Graines de coriandre", allergenNames: [] }, - { name: "Cardamome", allergenNames: [] }, - { name: "Fenugrec", allergenNames: [] }, - { name: "Piment jalapeño", allergenNames: [] }, - { name: "Piment chipotle", allergenNames: [] }, - { name: "Piment poblano", allergenNames: [] }, - { name: "Piment habanero", allergenNames: [] }, - { name: "Ras el hanout", allergenNames: [] }, - { name: "Za'atar", allergenNames: ["Graines de sésame"] }, + { uid: "herbesDeProvence", allergenUids: [] }, + { uid: "blackPepper", allergenUids: [] }, + { uid: "paprika", allergenUids: [] }, + { uid: "espelettePepper", allergenUids: [] }, + { uid: "cayennePepper", allergenUids: [] }, + { uid: "cumin", allergenUids: [] }, + { uid: "curryPowder", allergenUids: [] }, + { uid: "turmeric", allergenUids: [] }, + { uid: "cinnamon", allergenUids: [] }, + { uid: "nutmeg", allergenUids: [] }, + { uid: "saffron", allergenUids: [] }, + { uid: "clove", allergenUids: [] }, + { uid: "vanillaBean", allergenUids: [] }, + { uid: "whitePepper", allergenUids: [] }, + { uid: "pinkPepper", allergenUids: [] }, + { uid: "sichuanPepper", allergenUids: [] }, + { uid: "smokedPaprika", allergenUids: [] }, + { uid: "birdEyeChili", allergenUids: [] }, + { uid: "juniperBerries", allergenUids: [] }, + { uid: "starAnise", allergenUids: [] }, + { uid: "greenAnise", allergenUids: [] }, + { uid: "fennelSeeds", allergenUids: [] }, + { uid: "sumac", allergenUids: [] }, + { uid: "nigella", allergenUids: [] }, + { uid: "allspice", allergenUids: [] }, + { uid: "colomboPowder", allergenUids: [] }, + { uid: "baharat", allergenUids: [] }, + { uid: "horseradish", allergenUids: [] }, + { uid: "herbSalt", allergenUids: [] }, + { uid: "celerySalt", allergenUids: ["celery"] }, + { uid: "fleurDeSel", allergenUids: [] }, + { uid: "salt", allergenUids: [] }, + { uid: "fiveSpice", allergenUids: [] }, + { uid: "garamMasala", allergenUids: [] }, + { uid: "corianderSeeds", allergenUids: [] }, + { uid: "cardamom", allergenUids: [] }, + { uid: "fenugreek", allergenUids: [] }, + { uid: "jalapeno", allergenUids: [] }, + { uid: "chipotle", allergenUids: [] }, + { uid: "poblanoPepper", allergenUids: [] }, + { uid: "habanero", allergenUids: [] }, + { uid: "rasElHanout", allergenUids: [] }, + { uid: "zaatar", allergenUids: ["sesameSeeds"] }, ], }, { - category: "CONDIMENTS_EPICES", - subcategory: "SAUCES", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "condimentsAndSpices", + subcategory: "sauces", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "JAR", items: [ - { name: "Sauce soja", allergenNames: ["Soja"] }, - { name: "Moutarde", allergenNames: ["Moutarde"] }, + { uid: "soySauce", allergenUids: ["soy"] }, + { uid: "mustard", allergenUids: ["mustard"] }, { - name: "Mayonnaise", - allergenNames: ["Œufs"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "mayonnaise", + reproducible: true, + allergenUids: ["eggs"], + dietUids: ["vegetarian", "pescatarian"], }, - { name: "Ketchup", allergenNames: [] }, - { name: "Tabasco", allergenNames: [] }, + { uid: "ketchup", reproducible: true, allergenUids: [] }, + { uid: "tabasco", allergenUids: [] }, { - name: "Sauce Worcestershire", - allergenNames: ["Poissons"], - dietNames: ["Pescétarien"], + uid: "worcestershireSauce", + allergenUids: ["fish"], + dietUids: ["pescatarian"], }, { - name: "Sauce nuoc-mâm", - allergenNames: ["Poissons"], - dietNames: ["Pescétarien"], + uid: "fishSauce", + allergenUids: ["fish"], + dietUids: ["pescatarian"], }, - { name: "Wasabi", allergenNames: [] }, - { name: "Harissa", allergenNames: [] }, - { name: "Pâte de curry", allergenNames: [] }, - { name: "Beurre de cacahuète", allergenNames: ["Arachides"] }, - { name: "Moutarde de Dijon", allergenNames: ["Moutarde"] }, - { name: "Moutarde à l'ancienne", allergenNames: ["Moutarde"] }, - { name: "Sauce barbecue", allergenNames: [] }, + { uid: "wasabi", allergenUids: [] }, + { uid: "harissa", allergenUids: [] }, + { uid: "curryPaste", allergenUids: [] }, + { uid: "peanutButter", allergenUids: ["peanuts"] }, + { uid: "dijonMustard", allergenUids: ["mustard"] }, + { uid: "wholegrainMustard", allergenUids: ["mustard"] }, + { uid: "barbecueSauce", reproducible: true, allergenUids: [] }, { - name: "Sauce tartare", - allergenNames: ["Œufs"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "tartarSauce", + allergenUids: ["eggs"], + dietUids: ["vegetarian", "pescatarian"], }, { - name: "Sauce cocktail", - allergenNames: ["Œufs"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "cocktailSauce", + allergenUids: ["eggs"], + dietUids: ["vegetarian", "pescatarian"], }, { - name: "Sauce béarnaise", - allergenNames: ["Œufs", "Lait"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "bearnaiseSauce", + allergenUids: ["eggs", "milk"], + dietUids: ["vegetarian", "pescatarian"], }, { - name: "Sauce hollandaise", - allergenNames: ["Œufs", "Lait"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "hollandaiseSauce", + allergenUids: ["eggs", "milk"], + dietUids: ["vegetarian", "pescatarian"], }, { - name: "Sauce béchamel", - allergenNames: ["Lait", "Gluten"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "bechamelSauce", + reproducible: true, + allergenUids: ["milk", "gluten"], + dietUids: ["vegetarian", "pescatarian"], }, - { name: "Sauce teriyaki", allergenNames: ["Soja"] }, + { uid: "teriyakiSauce", allergenUids: ["soy"] }, { - name: "Sauce ponzu", - allergenNames: ["Soja", "Poissons"], - dietNames: ["Pescétarien"], + uid: "ponzuSauce", + allergenUids: ["soy", "fish"], + dietUids: ["pescatarian"], }, - { name: "Chimichurri", allergenNames: [] }, + { uid: "chimichurri", allergenUids: [] }, { - name: "Pesto rouge (tomates séchées)", - allergenNames: ["Lait", "Fruits à coque"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "redPesto", + allergenUids: ["milk", "treeNuts"], + dietUids: ["vegetarian", "pescatarian"], }, { - name: "Pesto", - allergenNames: ["Lait", "Fruits à coque"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "pesto", + reproducible: true, + allergenUids: ["milk", "treeNuts"], + dietUids: ["vegetarian", "pescatarian"], }, { - name: "Sauce huître", - allergenNames: ["Mollusques"], - dietNames: ["Pescétarien"], + uid: "oysterSauce", + allergenUids: ["molluscs"], + dietUids: ["pescatarian"], }, - { name: "Sauce hoisin", allergenNames: ["Soja"] }, - { name: "Sauce sriracha", allergenNames: [] }, - { name: "Sauce sweet chili", allergenNames: [] }, - { name: "Miso", allergenNames: ["Soja"] }, + { uid: "hoisinSauce", allergenUids: ["soy"] }, + { uid: "sriracha", allergenUids: [] }, + { uid: "sweetChiliSauce", allergenUids: [] }, + { uid: "miso", allergenUids: ["soy"] }, { - name: "Pâte de crevettes", - allergenNames: ["Crustacés"], - dietNames: ["Pescétarien"], + uid: "shrimpPaste", + allergenUids: ["crustaceans"], + dietUids: ["pescatarian"], }, - { name: "Pâte de curry rouge (thaï)", allergenNames: [] }, - { name: "Pâte de curry vert (thaï)", allergenNames: [] }, - { name: "Tahini", allergenNames: ["Graines de sésame"] }, + { uid: "redCurryPaste", allergenUids: [] }, + { uid: "greenCurryPaste", allergenUids: [] }, + { uid: "tahini", reproducible: true, allergenUids: ["sesameSeeds"] }, // Second Ciqual pass. { - name: "Aïoli", - allergenNames: ["Œufs"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "aioli", + reproducible: true, + allergenUids: ["eggs"], + dietUids: ["vegetarian", "pescatarian"], }, - { name: "Sauce vinaigrette", allergenNames: [] }, - { name: "Houmous", allergenNames: ["Graines de sésame"] }, + { uid: "vinaigrette", reproducible: true, allergenUids: [] }, + { uid: "hummus", reproducible: true, allergenUids: ["sesameSeeds"] }, ], }, { - category: "CONDIMENTS_EPICES", - subcategory: "ASSAISONNEMENTS", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "condimentsAndSpices", + subcategory: "seasonings", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "BOTTLE", items: [ - { name: "Huile d'olive", allergenNames: [] }, - { name: "Huile de tournesol", allergenNames: [] }, - { name: "Huile de colza", allergenNames: [] }, - { name: "Huile de coco", allergenNames: [] }, - { name: "Huile de sésame", allergenNames: ["Graines de sésame"] }, - { name: "Vinaigre de cidre", allergenNames: [] }, - { name: "Vinaigre blanc", allergenNames: [] }, - { name: "Vinaigre balsamique", allergenNames: ["Sulfites"] }, - { name: "Câpres", icon: "JAR", allergenNames: [] }, - { name: "Olives", icon: "JAR", allergenNames: [] }, - { name: "Olives noires", icon: "JAR", allergenNames: [] }, - { name: "Olives vertes", icon: "JAR", allergenNames: [] }, - { name: "Vin blanc (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, - { name: "Vin rouge (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, - { name: "Vin rosé (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, - { name: "Vinaigre de vin rouge", allergenNames: ["Sulfites"] }, - { name: "Vinaigre de vin blanc", allergenNames: ["Sulfites"] }, - { name: "Vinaigre de xérès", allergenNames: ["Sulfites"] }, - { name: "Huile de noix", allergenNames: ["Fruits à coque"] }, - { name: "Huile de noisette", allergenNames: ["Fruits à coque"] }, - { name: "Huile d'arachide", allergenNames: ["Arachides"] }, - { name: "Huile pimentée", allergenNames: [] }, - { name: "Vinaigre de riz", allergenNames: [] }, + { uid: "oliveOil", allergenUids: [] }, + { uid: "sunflowerOil", allergenUids: [] }, + { uid: "rapeseedOil", allergenUids: [] }, + { uid: "coconutOil", allergenUids: [] }, + { uid: "sesameOil", allergenUids: ["sesameSeeds"] }, + { uid: "ciderVinegar", allergenUids: [] }, + { uid: "whiteVinegar", allergenUids: [] }, + { uid: "balsamicVinegar", allergenUids: ["sulfites"] }, + { uid: "capers", icon: "JAR", allergenUids: [] }, + { uid: "olives", icon: "JAR", allergenUids: [] }, + { uid: "blackOlives", icon: "JAR", allergenUids: [] }, + { uid: "greenOlives", icon: "JAR", allergenUids: [] }, + { uid: "whiteWine", icon: "DRINK", allergenUids: ["sulfites"] }, + { uid: "redWine", icon: "DRINK", allergenUids: ["sulfites"] }, + { uid: "roseWine", icon: "DRINK", allergenUids: ["sulfites"] }, + { uid: "redWineVinegar", allergenUids: ["sulfites"] }, + { uid: "whiteWineVinegar", allergenUids: ["sulfites"] }, + { uid: "sherryVinegar", allergenUids: ["sulfites"] }, + { uid: "walnutOil", allergenUids: ["treeNuts"] }, + { uid: "hazelnutOil", allergenUids: ["treeNuts"] }, + { uid: "peanutOil", allergenUids: ["peanuts"] }, + { uid: "chiliOil", allergenUids: [] }, + { uid: "riceVinegar", allergenUids: [] }, // Second Ciqual pass. - { name: "Huile de maïs", allergenNames: [] }, - { name: "Huile de pépins de raisin", allergenNames: [] }, - { name: "Huile de soja", allergenNames: ["Soja"] }, - { name: "Huile de palme", allergenNames: [] }, - { name: "Mirin", icon: "DRINK", allergenNames: [] }, - { name: "Saké (cuisine)", icon: "DRINK", allergenNames: [] }, - { name: "Jus de citron", icon: "DRINK", allergenNames: [] }, - { name: "Jus de citron vert", icon: "DRINK", allergenNames: [] }, - { name: "Jus d'orange", icon: "DRINK", allergenNames: [] }, - { name: "Jus de pomme", icon: "DRINK", allergenNames: [] }, - { name: "Jus de raisin", icon: "DRINK", allergenNames: [] }, - { name: "Jus de tomate", icon: "DRINK", allergenNames: [] }, - { name: "Jus de cranberry", icon: "DRINK", allergenNames: [] }, - { name: "Café", icon: "DRINK", allergenNames: [] }, - { name: "Thé", icon: "DRINK", allergenNames: [] }, - { name: "Bière (cuisine)", icon: "DRINK", allergenNames: ["Gluten"] }, - { name: "Cidre (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, + { uid: "cornOil", allergenUids: [] }, + { uid: "grapeseedOil", allergenUids: [] }, + { uid: "soybeanOil", allergenUids: ["soy"] }, + { uid: "palmOil", allergenUids: [] }, + { uid: "mirin", icon: "DRINK", allergenUids: [] }, + { uid: "sake", icon: "DRINK", allergenUids: [] }, + { uid: "lemonJuice", icon: "DRINK", allergenUids: [] }, + { uid: "limeJuice", icon: "DRINK", allergenUids: [] }, + { uid: "orangeJuice", icon: "DRINK", allergenUids: [] }, + { uid: "appleJuice", icon: "DRINK", allergenUids: [] }, + { uid: "grapeJuice", icon: "DRINK", allergenUids: [] }, + { uid: "tomatoJuice", icon: "DRINK", allergenUids: [] }, + { uid: "cranberryJuice", icon: "DRINK", allergenUids: [] }, + { uid: "coffee", icon: "DRINK", allergenUids: [] }, + { uid: "tea", icon: "DRINK", allergenUids: [] }, + { uid: "beer", icon: "DRINK", allergenUids: ["gluten"] }, + { uid: "cider", icon: "DRINK", allergenUids: ["sulfites"] }, { - name: "Champagne / vin pétillant (cuisine)", + uid: "champagne", icon: "DRINK", - allergenNames: ["Sulfites"], + allergenUids: ["sulfites"], }, - { name: "Porto (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, - { name: "Vin jaune (cuisine)", icon: "DRINK", allergenNames: ["Sulfites"] }, - { name: "Cognac", icon: "DRINK", allergenNames: [] }, - { name: "Rhum", icon: "DRINK", allergenNames: [] }, - { name: "Whisky", icon: "DRINK", allergenNames: [] }, - { name: "Vodka", icon: "DRINK", allergenNames: [] }, + { uid: "portWine", icon: "DRINK", allergenUids: ["sulfites"] }, + { uid: "vinJaune", icon: "DRINK", allergenUids: ["sulfites"] }, + { uid: "cognac", icon: "DRINK", allergenUids: [] }, + { uid: "rum", icon: "DRINK", allergenUids: [] }, + { uid: "whisky", icon: "DRINK", allergenUids: [] }, + { uid: "vodka", icon: "DRINK", allergenUids: [] }, ], }, // ========================================================================= // Aides culinaires // ========================================================================= { - category: "AIDES_CULINAIRES", - subcategory: "BASES", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "cookingEssentials", + subcategory: "bases", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "GRAIN", items: [ - { name: "Farine de blé", allergenNames: ["Gluten"] }, - { name: "Farine complète", allergenNames: ["Gluten"] }, - { name: "Farine de maïs", allergenNames: [] }, - { name: "Farine de sarrasin", allergenNames: [] }, - { name: "Farine de riz", allergenNames: [] }, - { name: "Bouillon cube légumes", icon: "STOCK_POT", allergenNames: ["Céleri"] }, + { uid: "wheatFlour", allergenUids: ["gluten"] }, + { uid: "wholeWheatFlour", allergenUids: ["gluten"] }, + { uid: "cornFlour", allergenUids: [] }, + { uid: "buckwheatFlour", allergenUids: [] }, + { uid: "riceFlour", allergenUids: [] }, + { uid: "vegetableStockCube", icon: "STOCK_POT", allergenUids: ["celery"] }, { - name: "Bouillon cube volaille", + uid: "chickenStockCube", icon: "STOCK_POT", - allergenNames: ["Céleri"], - dietNames: [], + allergenUids: ["celery"], + dietUids: [], }, - { name: "Concentré de tomate", icon: "JAR", allergenNames: [] }, - { name: "Coulis de tomate", icon: "JAR", allergenNames: [] }, - { name: "Tomates pelées (conserve)", icon: "JAR", allergenNames: [] }, - { name: "Tomates séchées", icon: "JAR", allergenNames: [] }, - { name: "Fond de veau", icon: "STOCK_POT", allergenNames: [], dietNames: [] }, - { name: "Fond de volaille", icon: "STOCK_POT", allergenNames: [], dietNames: [] }, + { uid: "tomatoPaste", icon: "JAR", allergenUids: [] }, + { uid: "tomatoCoulis", icon: "JAR", allergenUids: [] }, + { uid: "cannedPeeledTomatoes", icon: "JAR", allergenUids: [] }, + { uid: "sunDriedTomatoes", icon: "JAR", allergenUids: [] }, { - name: "Bouillon cube bœuf", + uid: "vealStock", icon: "STOCK_POT", - allergenNames: ["Céleri"], - dietNames: [], + reproducible: true, + allergenUids: [], + dietUids: [], }, { - name: "Bouillon cube poisson", + uid: "chickenStock", icon: "STOCK_POT", - allergenNames: ["Poissons", "Céleri"], - dietNames: ["Pescétarien"], - }, - { name: "Bouillon de légumes", icon: "STOCK_POT", allergenNames: ["Céleri"] }, - { - name: "Bouillon de volaille", - icon: "STOCK_POT", - allergenNames: ["Céleri"], - dietNames: [], - }, - { name: "Bouillon de bœuf", icon: "STOCK_POT", allergenNames: ["Céleri"], dietNames: [] }, - { name: "Court-bouillon", icon: "STOCK_POT", allergenNames: [] }, - { - name: "Dashi (bouillon japonais)", - icon: "STOCK_POT", - allergenNames: ["Poissons"], - dietNames: ["Pescétarien"], + reproducible: true, + allergenUids: [], + dietUids: [], }, { - name: "Bisque de crustacés", + uid: "beefStockCube", icon: "STOCK_POT", - allergenNames: ["Crustacés"], - dietNames: ["Pescétarien"], + allergenUids: ["celery"], + dietUids: [], }, - { name: "Farine de tapioca", allergenNames: [] }, - { name: "Masa harina", allergenNames: [] }, - { name: "Eau", icon: "DRINK", allergenNames: [] }, - { name: "Eau gazeuse", icon: "DRINK", allergenNames: [] }, - { name: "Eau de fleur d'oranger", icon: "DRINK", allergenNames: [] }, - { name: "Eau de rose", icon: "DRINK", allergenNames: [] }, { - name: "Fumet de poisson", + uid: "fishStockCube", icon: "STOCK_POT", - allergenNames: ["Poissons"], - dietNames: ["Pescétarien"], + allergenUids: ["fish", "celery"], + dietUids: ["pescatarian"], + }, + { + uid: "vegetableBroth", + icon: "STOCK_POT", + reproducible: true, + allergenUids: ["celery"], + }, + { + uid: "chickenBroth", + icon: "STOCK_POT", + reproducible: true, + allergenUids: ["celery"], + dietUids: [], + }, + { + uid: "beefBroth", + icon: "STOCK_POT", + reproducible: true, + allergenUids: ["celery"], + dietUids: [], + }, + { uid: "courtBouillon", icon: "STOCK_POT", allergenUids: [] }, + { + uid: "dashi", + icon: "STOCK_POT", + allergenUids: ["fish"], + dietUids: ["pescatarian"], + }, + { + uid: "shellfishBisque", + icon: "STOCK_POT", + allergenUids: ["crustaceans"], + dietUids: ["pescatarian"], + }, + { uid: "tapiocaFlour", allergenUids: [] }, + { uid: "masaHarina", allergenUids: [] }, + { uid: "water", icon: "DRINK", allergenUids: [] }, + { uid: "sparklingWater", icon: "DRINK", allergenUids: [] }, + { uid: "orangeBlossomWater", icon: "DRINK", allergenUids: [] }, + { uid: "roseWater", icon: "DRINK", allergenUids: [] }, + { + uid: "fishFumet", + icon: "STOCK_POT", + reproducible: true, + allergenUids: ["fish"], + dietUids: ["pescatarian"], }, ], }, { - category: "AIDES_CULINAIRES", - subcategory: "EPAISSISSANTS", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "cookingEssentials", + subcategory: "thickeners", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "JAR", items: [ - { name: "Levure boulangère", allergenNames: [] }, - { name: "Levure chimique", allergenNames: [] }, - { name: "Maïzena", allergenNames: [] }, - { name: "Farine de lupin", allergenNames: ["Lupin"] }, + { uid: "bakersYeast", allergenUids: [] }, + { uid: "bakingPowder", allergenUids: [] }, + { uid: "cornstarch", allergenUids: [] }, + { uid: "lupinFlour", allergenUids: ["lupin"] }, // Animal collagen (bones/skin, usually pork or beef) — not // vegetarian/vegan, and not reliably fish-derived either, so no // pescetarian flag. - { name: "Gélatine", allergenNames: [], dietNames: [] }, - { name: "Bicarbonate de soude", allergenNames: [] }, - { name: "Fécule de pomme de terre", allergenNames: [] }, + { uid: "gelatin", allergenUids: [], dietUids: [] }, + { uid: "bakingSoda", allergenUids: [] }, + { uid: "potatoStarch", allergenUids: [] }, ], }, { - category: "AIDES_CULINAIRES", - subcategory: "SUCRES", - defaultDiets: ["Végétarien", "Végan", "Pescétarien"], + category: "cookingEssentials", + subcategory: "sugars", + defaultDiets: ["vegetarian", "vegan", "pescatarian"], defaultIcon: "SUGAR", items: [ - { name: "Sucre", allergenNames: [] }, - { name: "Miel", allergenNames: [], dietNames: ["Végétarien", "Pescétarien"] }, - { name: "Sirop d'érable", allergenNames: [] }, - { name: "Sucre roux", allergenNames: [] }, - { name: "Sucre glace", allergenNames: [] }, - { name: "Cassonade", allergenNames: [] }, - { name: "Chocolat noir", allergenNames: [] }, + { uid: "sugar", allergenUids: [] }, + { uid: "honey", allergenUids: [], dietUids: ["vegetarian", "pescatarian"] }, + { uid: "mapleSyrup", allergenUids: [] }, + { uid: "brownSugar", allergenUids: [] }, + { uid: "powderedSugar", allergenUids: [] }, + { uid: "demeraraSugar", allergenUids: [] }, + { uid: "darkChocolate", allergenUids: [] }, { - name: "Chocolat au lait", - allergenNames: ["Lait"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "milkChocolate", + allergenUids: ["milk"], + dietUids: ["vegetarian", "pescatarian"], }, { - name: "Chocolat blanc", - allergenNames: ["Lait"], - dietNames: ["Végétarien", "Pescétarien"], + uid: "whiteChocolate", + allergenUids: ["milk"], + dietUids: ["vegetarian", "pescatarian"], }, - { name: "Pépites de chocolat", allergenNames: [] }, - { name: "Cacao en poudre", allergenNames: [] }, - { name: "Extrait de vanille", allergenNames: [] }, - { name: "Sucre de palme", allergenNames: [] }, - { name: "Sirop de sucre de canne", allergenNames: [] }, + { uid: "chocolateChips", allergenUids: [] }, + { uid: "cocoaPowder", allergenUids: [] }, + { uid: "vanillaExtract", allergenUids: [] }, + { uid: "palmSugar", allergenUids: [] }, + { uid: "caneSyrup", allergenUids: [] }, ], }, ]; const INGREDIENTS: Array< - Omit & { + Omit & { category: IngredientCategory; subcategory: IngredientSubcategory; icon: IngredientIcon; - dietNames: string[]; + dietUids: string[]; + reproducible: boolean; } > = INGREDIENT_GROUPS.flatMap(({ category, subcategory, defaultDiets, defaultIcon, items }) => items.map((item) => ({ @@ -1010,7 +1058,8 @@ const INGREDIENTS: Array< category, subcategory, icon: item.icon ?? defaultIcon, - dietNames: item.dietNames ?? defaultDiets, + dietUids: item.dietUids ?? defaultDiets, + reproducible: item.reproducible ?? false, })), ); @@ -1022,18 +1071,18 @@ const INGREDIENTS: Array< * test starts from the same realistic reference data the real app seeds, * not an empty table). * - * Every `name` below (`DIETS`, `ALLERGENS`, `INGREDIENT_GROUPS`) is an - * *authoring* label, never written to the database or seen by a client — - * {@link getEnglishKey} resolves each row's real, stable, English `key` - * from it (see `catalog-en-keys.ts` for why the key must be English even - * though this file's labels are French). The database only ever stores - * that key; the French label itself lives in `apps/web`'s - * `locales/fr/translation.json` (`catalog.*` - * namespace, kept in sync by `scripts/generate-catalog-i18n.ts`). + * Every `uid` below (`DIETS`, `ALLERGENS`, `INGREDIENT_GROUPS`) *is* the + * database `key`, authored directly — no French label, no separate + * lookup/translation table (that used to be `catalog-en-keys.ts` + + * `getEnglishKey()`, removed: product decision recorded in chat, this file + * carries zero natural-language text now). The French display label for + * each uid lives solely in `apps/web`'s `locales/fr/translation.json` + * (`catalog.*` namespace), maintained independently — adding a new + * diet/allergen/ingredient here means also adding its translation there by + * hand, tied together only by the matching uid string. */ export async function seedReferenceData(prisma: PrismaClient): Promise { - for (const name of DIETS) { - const key = getEnglishKey(name); + for (const key of DIETS) { await prisma.diet.upsert({ where: { key }, update: {}, create: { key } }); } @@ -1043,8 +1092,7 @@ export async function seedReferenceData(prisma: PrismaClient): Promise { // created only the first time. `update: { kind }` (not `{}`) — a reseed // must correct `kind` on an already-existing category if the // classification above ever changes, not just skip it. - for (const { name, kind } of ALLERGENS) { - const key = getEnglishKey(name); + for (const { uid: key, kind } of ALLERGENS) { const category = await prisma.category.upsert({ where: { key }, update: { kind }, @@ -1065,38 +1113,47 @@ export async function seedReferenceData(prisma: PrismaClient): Promise { // test-suite case) that's zero updates, on a real re-deploy it's however // many rows were edited in code since the last deploy, never the full // list. - const ingredientKeys = INGREDIENTS.map((i) => getEnglishKey(i.name)); + const ingredientKeys = INGREDIENTS.map((i) => i.uid); const existingIngredients = await prisma.ingredient.findMany({ where: { key: { in: ingredientKeys } }, - select: { id: true, key: true, icon: true, category: true, subcategory: true }, + select: { + id: true, + key: true, + icon: true, + category: true, + subcategory: true, + reproducible: true, + }, }); const existingByKey = new Map(existingIngredients.map((i) => [i.key, i])); - const missingIngredients = INGREDIENTS.filter((i) => !existingByKey.has(getEnglishKey(i.name))); + const missingIngredients = INGREDIENTS.filter((i) => !existingByKey.has(i.uid)); if (missingIngredients.length > 0) { await prisma.ingredient.createMany({ - data: missingIngredients.map(({ name, icon, category, subcategory }) => ({ - key: getEnglishKey(name), + data: missingIngredients.map(({ uid, icon, category, subcategory, reproducible }) => ({ + key: uid, icon, category, subcategory, + reproducible, })), }); } const changed = INGREDIENTS.filter((i) => { - const existing = existingByKey.get(getEnglishKey(i.name)); + const existing = existingByKey.get(i.uid); return ( existing && (existing.icon !== i.icon || existing.category !== i.category || - existing.subcategory !== i.subcategory) + existing.subcategory !== i.subcategory || + existing.reproducible !== i.reproducible) ); }); - for (const { name, icon, category, subcategory } of changed) { + for (const { uid, icon, category, subcategory, reproducible } of changed) { await prisma.ingredient.update({ - where: { key: getEnglishKey(name) }, - data: { icon, category, subcategory }, + where: { key: uid }, + data: { icon, category, subcategory, reproducible }, }); } @@ -1115,11 +1172,11 @@ export async function seedReferenceData(prisma: PrismaClient): Promise { const allergyIdByCategoryKey = new Map(allergies.map((a) => [a.category.key, a.id])); const links: Array<{ ingredientId: number; allergyId: number }> = []; - for (const { name, allergenNames } of INGREDIENTS) { - const ingredientId = ingredientIdByKey.get(getEnglishKey(name)); + for (const { uid, allergenUids } of INGREDIENTS) { + const ingredientId = ingredientIdByKey.get(uid); if (ingredientId === undefined) continue; - for (const allergenName of allergenNames) { - const allergyId = allergyIdByCategoryKey.get(getEnglishKey(allergenName)); + for (const allergenUid of allergenUids) { + const allergyId = allergyIdByCategoryKey.get(allergenUid); if (allergyId !== undefined) links.push({ ingredientId, allergyId }); } } @@ -1128,17 +1185,17 @@ export async function seedReferenceData(prisma: PrismaClient): Promise { } // Same bulk-insert approach as the allergy links above, resolved against - // `dietNames` (item override, falling back to its group's `defaultDiets` - // in the `INGREDIENTS` flatten step) instead of `allergenNames`. + // `dietUids` (item override, falling back to its group's `defaultDiets` + // in the `INGREDIENTS` flatten step) instead of `allergenUids`. const diets = await prisma.diet.findMany(); const dietIdByKey = new Map(diets.map((d) => [d.key, d.id])); const dietLinks: Array<{ ingredientId: number; dietId: number }> = []; - for (const { name, dietNames } of INGREDIENTS) { - const ingredientId = ingredientIdByKey.get(getEnglishKey(name)); + for (const { uid, dietUids } of INGREDIENTS) { + const ingredientId = ingredientIdByKey.get(uid); if (ingredientId === undefined) continue; - for (const dietName of dietNames) { - const dietId = dietIdByKey.get(getEnglishKey(dietName)); + for (const dietUid of dietUids) { + const dietId = dietIdByKey.get(dietUid); if (dietId !== undefined) dietLinks.push({ ingredientId, dietId }); } } diff --git a/apps/api/src/modules/recipe/recipe.service.ts b/apps/api/src/modules/recipe/recipe.service.ts index bcb1608..aa8baa4 100644 --- a/apps/api/src/modules/recipe/recipe.service.ts +++ b/apps/api/src/modules/recipe/recipe.service.ts @@ -43,6 +43,7 @@ function toIngredientView(ingredient: IngredientWithDetails): IngredientView { icon: ingredient.icon, category: ingredient.category, subcategory: ingredient.subcategory, + reproducible: ingredient.reproducible, allergens: ingredient.allergies.map(({ allergy }) => ({ id: allergy.id, key: allergy.category.key, diff --git a/apps/api/src/modules/reference/reference.service.ts b/apps/api/src/modules/reference/reference.service.ts index 247d4d2..749285a 100644 --- a/apps/api/src/modules/reference/reference.service.ts +++ b/apps/api/src/modules/reference/reference.service.ts @@ -53,6 +53,7 @@ export async function getIngredients(): Promise { icon: ingredient.icon, category: ingredient.category, subcategory: ingredient.subcategory, + reproducible: ingredient.reproducible, allergens: ingredient.allergies.map(({ allergy }) => ({ id: allergy.id, key: allergy.category.key, diff --git a/apps/api/src/utils/slugify.ts b/apps/api/src/utils/slugify.ts deleted file mode 100644 index 9649aed..0000000 --- a/apps/api/src/utils/slugify.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Deterministic slug for a French reference-data label — used as the - * stable, storage-safe `key` for `Diet`/`Category`/`Ingredient` rows (see - * `db/reference-seed-data.ts`), decoupled from the display label so the - * label itself can live in `apps/web`'s `locales/fr/translation.json` - * (`catalog.*` namespace) instead of the database. A row's `key` is derived - * from its seed-time French name once and then never changes — renaming the - * *label* later (a translation fix, a rewording) never touches the key, the - * FK-referencing rows, or any code that looks a row up by key. - * - * Handles the two French ligatures NFD decomposition doesn't touch (`œ`, - * `æ` aren't accented letters, they're distinct glyphs) explicitly, then - * strips every other accent via NFD decomposition + Unicode "Mark" removal - * (`\p{M}`, every combining diacritic NFD can produce), then collapses - * whatever isn't `[a-z0-9]` into single underscores. - */ -export function slugify(label: string): string { - return label - .toLowerCase() - .replace(/œ/g, "oe") - .replace(/æ/g, "ae") - .normalize("NFD") - .replace(/\p{M}/gu, "") - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, ""); -} diff --git a/apps/api/test/profile.test.ts b/apps/api/test/profile.test.ts index 4368267..87a221c 100644 --- a/apps/api/test/profile.test.ts +++ b/apps/api/test/profile.test.ts @@ -3,7 +3,6 @@ import { faker } from "@faker-js/faker"; import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; -import { getEnglishKey } from "../src/db/catalog-en-keys.js"; import { prisma } from "../src/db/prisma.js"; import { resetDatabase } from "../test-support/reset-db.js"; @@ -41,7 +40,7 @@ describe("Profile", () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const diet = await prisma.diet.findFirstOrThrow({ - where: { key: getEnglishKey("Végétarien") }, + where: { key: "vegetarian" }, }); const res = await agent.patch("/profile/diet").send({ dietId: diet.id }); @@ -53,7 +52,7 @@ describe("Profile", () => { it("clears the regime when dietId is null", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); - const diet = await prisma.diet.findFirstOrThrow({ where: { key: getEnglishKey("Végan") } }); + const diet = await prisma.diet.findFirstOrThrow({ where: { key: "vegan" } }); await agent.patch("/profile/diet").send({ dietId: diet.id }); const res = await agent.patch("/profile/diet").send({ dietId: null }); @@ -86,8 +85,8 @@ describe("Profile", () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const allergies = await prisma.allergy.findMany({ include: { category: true } }); - const peanuts = allergies.find((a) => a.category.key === getEnglishKey("Arachides")); - const gluten = allergies.find((a) => a.category.key === getEnglishKey("Gluten")); + const peanuts = allergies.find((a) => a.category.key === "peanuts"); + const gluten = allergies.find((a) => a.category.key === "gluten"); if (!peanuts || !gluten) throw new Error("expected seeded allergens missing"); const initial = await agent.get("/profile/allergies"); @@ -107,8 +106,8 @@ describe("Profile", () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const allergies = await prisma.allergy.findMany({ include: { category: true } }); - const peanuts = allergies.find((a) => a.category.key === getEnglishKey("Arachides")); - const gluten = allergies.find((a) => a.category.key === getEnglishKey("Gluten")); + const peanuts = allergies.find((a) => a.category.key === "peanuts"); + const gluten = allergies.find((a) => a.category.key === "gluten"); if (!peanuts || !gluten) throw new Error("expected seeded allergens missing"); await agent.patch("/profile/allergies").send({ allergyIds: [peanuts.id] }); @@ -144,10 +143,10 @@ describe("Profile", () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const tomate = await prisma.ingredient.findFirstOrThrow({ - where: { key: getEnglishKey("Tomate") }, + where: { key: "tomato" }, }); const oignon = await prisma.ingredient.findFirstOrThrow({ - where: { key: getEnglishKey("Oignon") }, + where: { key: "onion" }, }); const initial = await agent.get("/profile/disliked-ingredients"); @@ -167,10 +166,10 @@ describe("Profile", () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const tomate = await prisma.ingredient.findFirstOrThrow({ - where: { key: getEnglishKey("Tomate") }, + where: { key: "tomato" }, }); const oignon = await prisma.ingredient.findFirstOrThrow({ - where: { key: getEnglishKey("Oignon") }, + where: { key: "onion" }, }); await agent diff --git a/apps/api/test/recipe.test.ts b/apps/api/test/recipe.test.ts index 4d7935e..8bb51e3 100644 --- a/apps/api/test/recipe.test.ts +++ b/apps/api/test/recipe.test.ts @@ -4,7 +4,6 @@ import { faker } from "@faker-js/faker"; import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; -import { getEnglishKey } from "../src/db/catalog-en-keys.js"; import { prisma } from "../src/db/prisma.js"; import { resetDatabase } from "../test-support/reset-db.js"; @@ -20,11 +19,9 @@ function buildSignupPayload(): SignupInput { }; } -/** Resolves a reference ingredient's id by its `reference-seed-data.ts` French name (slugified to match its `key`). */ -async function ingredientId(name: string): Promise { - const ingredient = await prisma.ingredient.findFirstOrThrow({ - where: { key: getEnglishKey(name) }, - }); +/** Resolves a reference ingredient's id by its `reference-seed-data.ts` uid (also its DB `key`). */ +async function ingredientId(key: string): Promise { + const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { key } }); return ingredient.id; } @@ -164,10 +161,10 @@ describe("Recipes", () => { describe("POST /recipes", () => { it("creates a recipe with its ingredients, ordered steps and diet tags", async () => { const { agent } = await signup(); - const tomate = await ingredientId("Tomate"); - const oeuf = await ingredientId("Œuf"); + const tomate = await ingredientId("tomato"); + const oeuf = await ingredientId("egg"); const vegetarien = await prisma.diet.findFirstOrThrow({ - where: { key: getEnglishKey("Végétarien") }, + where: { key: "vegetarian" }, }); const res = await agent.post("/recipes").send({ @@ -188,18 +185,14 @@ describe("Recipes", () => { res.body.steps.map((s: { description: string; order: number }) => s.order), ).to.deep.equal([0, 1]); // Allergens aggregated across ingredients — "Œuf" carries "Œufs". - expect(res.body.allergens.map((a: { key: string }) => a.key)).to.include( - getEnglishKey("Œufs"), - ); - expect(res.body.diets.map((d: { key: string }) => d.key)).to.deep.equal([ - getEnglishKey("Végétarien"), - ]); + expect(res.body.allergens.map((a: { key: string }) => a.key)).to.include("eggs"); + expect(res.body.diets.map((d: { key: string }) => d.key)).to.deep.equal(["vegetarian"]); }); it("defaults to PERSONAL visibility, and stamps the author's current household", async () => { const { agent } = await signup(); const houseRes = await agent.post("/house").send({ name: "Chez moi" }); - const tomate = await ingredientId("Tomate"); + const tomate = await ingredientId("tomato"); const res = await agent.post("/recipes").send({ name: "Test", @@ -239,7 +232,7 @@ describe("Recipes", () => { it("rejects an unknown dietId with 404 DIET_NOT_FOUND", async () => { const { agent } = await signup(); - const tomate = await ingredientId("Tomate"); + const tomate = await ingredientId("tomato"); const res = await agent.post("/recipes").send({ name: "Test", @@ -276,7 +269,7 @@ describe("Recipes", () => { it("returns the full recipe detail", async () => { const { agent } = await signup(); - const tomate = await ingredientId("Tomate"); + const tomate = await ingredientId("tomato"); const created = await agent.post("/recipes").send({ name: "Salade", dietIds: [], @@ -288,7 +281,7 @@ describe("Recipes", () => { expect(res.status).to.equal(200); expect(res.body.name).to.equal("Salade"); - expect(res.body.ingredients[0].ingredient.key).to.equal(getEnglishKey("Tomate")); + expect(res.body.ingredients[0].ingredient.key).to.equal("tomato"); expect(res.body.isFavorite).to.equal(false); }); @@ -352,8 +345,8 @@ describe("Recipes", () => { describe("PATCH /recipes/:id", () => { it("replaces the recipe's whole content", async () => { const { agent } = await signup(); - const tomate = await ingredientId("Tomate"); - const oignon = await ingredientId("Oignon"); + const tomate = await ingredientId("tomato"); + const oignon = await ingredientId("onion"); const created = await agent.post("/recipes").send({ name: "Salade", dietIds: [], @@ -373,13 +366,13 @@ describe("Recipes", () => { expect(res.body.name).to.equal("Salade composée"); expect(res.body.visibility).to.equal("PUBLIC"); expect(res.body.ingredients).to.have.length(1); - expect(res.body.ingredients[0].ingredient.key).to.equal(getEnglishKey("Oignon")); + expect(res.body.ingredients[0].ingredient.key).to.equal("onion"); expect(res.body.steps).to.have.length(2); }); it("returns 404 RECIPE_NOT_FOUND for an unknown id", async () => { const { agent } = await signup(); - const tomate = await ingredientId("Tomate"); + const tomate = await ingredientId("tomato"); const res = await agent.patch("/recipes/999999").send({ name: "Test", @@ -394,7 +387,7 @@ describe("Recipes", () => { it("rejects an unknown dietId with 404 DIET_NOT_FOUND", async () => { const { agent } = await signup(); - const tomate = await ingredientId("Tomate"); + const tomate = await ingredientId("tomato"); const created = await agent.post("/recipes").send({ name: "Salade", dietIds: [], @@ -416,7 +409,7 @@ describe("Recipes", () => { it("rejects an edit from anyone other than the recipe's author with 403 NOT_RECIPE_AUTHOR", async () => { const { agent, profileId } = await signup(); const { agent: otherAgent } = await signup(); - const tomate = await ingredientId("Tomate"); + const tomate = await ingredientId("tomato"); const recipe = await prisma.recipe.create({ data: { name: "Publique", authorId: profileId, visibility: "PUBLIC" }, }); diff --git a/apps/api/test/reference.test.ts b/apps/api/test/reference.test.ts index 8fb423d..ce101d7 100644 --- a/apps/api/test/reference.test.ts +++ b/apps/api/test/reference.test.ts @@ -1,7 +1,6 @@ import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; -import { getEnglishKey } from "../src/db/catalog-en-keys.js"; import { prisma } from "../src/db/prisma.js"; import { resetDatabase } from "../test-support/reset-db.js"; @@ -22,7 +21,7 @@ describe("Reference data", () => { expect(res.status).to.equal(200); expect(res.body).to.have.length(5); - expect(res.body.map((d: { key: string }) => d.key)).to.include(getEnglishKey("Végétarien")); + expect(res.body.map((d: { key: string }) => d.key)).to.include("vegetarian"); expect(res.body[0]).to.have.keys(["id", "key"]); }); }); @@ -33,18 +32,17 @@ describe("Reference data", () => { expect(res.status).to.equal(200); expect(res.body).to.have.length(14); - expect(res.body.map((a: { key: string }) => a.key)).to.include(getEnglishKey("Arachides")); + expect(res.body.map((a: { key: string }) => a.key)).to.include("peanuts"); expect(res.body[0]).to.have.keys(["id", "key", "kind"]); }); it("classifies Gluten and Sulfites as intolerances, the rest as allergies", async () => { const res = await request(app).get("/reference/allergies"); - const byKey = (name: string) => - res.body.find((a: { key: string }) => a.key === getEnglishKey(name)); - expect(byKey("Gluten").kind).to.equal("INTOLERANCE"); - expect(byKey("Sulfites").kind).to.equal("INTOLERANCE"); - expect(byKey("Arachides").kind).to.equal("ALLERGY"); + const byKey = (key: string) => res.body.find((a: { key: string }) => a.key === key); + expect(byKey("gluten").kind).to.equal("INTOLERANCE"); + expect(byKey("sulfites").kind).to.equal("INTOLERANCE"); + expect(byKey("peanuts").kind).to.equal("ALLERGY"); expect(res.body.filter((a: { kind: string }) => a.kind === "INTOLERANCE")).to.have.length(2); }); }); @@ -55,13 +53,14 @@ describe("Reference data", () => { expect(res.status).to.equal(200); expect(res.body.length).to.be.greaterThan(0); - expect(res.body.map((i: { key: string }) => i.key)).to.include(getEnglishKey("Tomate")); + expect(res.body.map((i: { key: string }) => i.key)).to.include("tomato"); expect(res.body[0]).to.have.keys([ "id", "key", "icon", "category", "subcategory", + "reproducible", "allergens", "diets", ]); @@ -70,12 +69,9 @@ describe("Reference data", () => { it("resolves each ingredient's linked allergens, empty for one with none", async () => { const res = await request(app).get("/reference/ingredients"); - const byKey = (name: string) => - res.body.find((i: { key: string }) => i.key === getEnglishKey(name)); - expect(byKey("Œuf").allergens.map((a: { key: string }) => a.key)).to.include( - getEnglishKey("Œufs"), - ); - expect(byKey("Tomate").allergens).to.deep.equal([]); + const byKey = (key: string) => res.body.find((i: { key: string }) => i.key === key); + expect(byKey("egg").allergens.map((a: { key: string }) => a.key)).to.include("eggs"); + expect(byKey("tomato").allergens).to.deep.equal([]); }); }); }); diff --git a/apps/web/cypress/e2e/recipe-form.ts b/apps/web/cypress/e2e/recipe-form.ts index 962f16c..7840121 100644 --- a/apps/web/cypress/e2e/recipe-form.ts +++ b/apps/web/cypress/e2e/recipe-form.ts @@ -4,8 +4,8 @@ const tomato = { id: 1, key: "tomato", icon: "VEGETABLE", - category: "PRODUITS_FRAIS", - subcategory: "LEGUMES", + category: "freshProduce", + subcategory: "vegetables", allergens: [], diets: [{ id: 2, key: "vegetarian" }], }; @@ -13,8 +13,8 @@ const egg = { id: 2, key: "egg", icon: "EGG", - category: "CREMERIE_FROMAGE", - subcategory: "OEUFS", + category: "dairyAndCheese", + subcategory: "eggs", allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }], diets: [], }; @@ -22,8 +22,8 @@ const carrot = { id: 3, key: "carrot", icon: "VEGETABLE", - category: "PRODUITS_FRAIS", - subcategory: "LEGUMES", + category: "freshProduce", + subcategory: "vegetables", allergens: [], diets: [{ id: 2, key: "vegetarian" }], }; diff --git a/apps/web/cypress/e2e/recipes.cy.ts b/apps/web/cypress/e2e/recipes.cy.ts index 955f14e..cae0124 100644 --- a/apps/web/cypress/e2e/recipes.cy.ts +++ b/apps/web/cypress/e2e/recipes.cy.ts @@ -52,8 +52,8 @@ const omeletteDetail = { id: 10, key: "egg", icon: "EGG", - category: "CREMERIE_FROMAGE", - subcategory: "OEUFS", + category: "dairyAndCheese", + subcategory: "eggs", allergens: [oeufs], diets: [], }, diff --git a/apps/web/cypress/e2e/recipes.ts b/apps/web/cypress/e2e/recipes.ts index 02c0e80..867b801 100644 --- a/apps/web/cypress/e2e/recipes.ts +++ b/apps/web/cypress/e2e/recipes.ts @@ -23,8 +23,8 @@ const omeletteDetail = { id: 10, key: "egg", icon: "EGG", - category: "CREMERIE_FROMAGE", - subcategory: "OEUFS", + category: "dairyAndCheese", + subcategory: "eggs", allergens: [oeufs], diets: [], }, diff --git a/apps/web/src/features/recipes/IngredientPicker.tsx b/apps/web/src/features/recipes/IngredientPicker.tsx index 307d3a6..20ac680 100644 --- a/apps/web/src/features/recipes/IngredientPicker.tsx +++ b/apps/web/src/features/recipes/IngredientPicker.tsx @@ -11,6 +11,7 @@ import { CheckboxOption } from "../../components/ui/Checkbox"; import { SettingsIcon } from "../../layouts/nav-icons"; import { AllergenBadges } from "./AllergenBadges"; import { DietBadges } from "./DietBadges"; +import { ReproducibleBadge } from "./ReproducibleBadge"; import { CategoryIcon, IngredientTypeIcon, SubcategoryIcon } from "./ingredient-icons"; import "./recipes.scss"; @@ -60,6 +61,7 @@ export function IngredientPicker({ // badges per card too noisy while just browsing/searching by name. const [showAllergens, setShowAllergens] = useState(true); const [showDiets, setShowDiets] = useState(true); + const [showReproducible, setShowReproducible] = useState(true); const [isDisplayMenuOpen, setIsDisplayMenuOpen] = useState(false); function selectCategory(next: IngredientCategory | typeof ALL) { @@ -116,6 +118,9 @@ export function IngredientPicker({ {t("recipes.form.showAllergensLabel")} + + {t("recipes.form.showReproducibleLabel")} + )} @@ -184,6 +189,7 @@ export function IngredientPicker({ {showAllergens && } {showDiets && } + {showReproducible && } ))} diff --git a/apps/web/src/features/recipes/IngredientRow.tsx b/apps/web/src/features/recipes/IngredientRow.tsx index 7cb4a7e..7410f52 100644 --- a/apps/web/src/features/recipes/IngredientRow.tsx +++ b/apps/web/src/features/recipes/IngredientRow.tsx @@ -2,6 +2,7 @@ import type { IngredientView } from "@batch-cooking/shared"; import { useTranslation } from "react-i18next"; import { AllergenBadges } from "./AllergenBadges"; import { DietBadges } from "./DietBadges"; +import { ReproducibleBadge } from "./ReproducibleBadge"; import { IngredientTypeIcon } from "./ingredient-icons"; import "./recipes.scss"; @@ -48,6 +49,10 @@ export function IngredientRow({ /> +