Merge pull request #30 from kyuno053/refactor/catalog-camel-case-uids

refactor(catalog): authoring 100% anglais — uid camelCase, plus de français dans le code
This commit is contained in:
kyuno053 2026-08-19 20:58:42 +02:00 committed by GitHub
commit 54ab10d826
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1540 additions and 2044 deletions

View file

@ -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";

View file

@ -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.<key>` (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.<key>` (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
@ -313,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
@ -345,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
@ -433,8 +431,8 @@ model Ingredient {
id Int @id @default(autoincrement())
key String @unique
icon IngredientIcon @default(JAR)
category IngredientCategory @default(EPICERIE_SECHE)
subcategory IngredientSubcategory @default(AUTRES)
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

View file

@ -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<string, string> {
const map: Record<string, string> = {};
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}`);

View file

@ -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<string, string>();
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)),
);

View file

@ -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<string, string> = {
Omnivore: "omnivore",
Végétarien: "vegetarian",
Végan: "vegan",
Pescétarien: "pescatarian",
"Sans gluten": "gluten_free",
};
const ALLERGEN_KEYS: Record<string, string> = {
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<string, string> = {
// --- 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<string, string> = {
...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);
}

File diff suppressed because it is too large Load diff

View file

@ -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, "");
}

View file

@ -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

View file

@ -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<number> {
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<number> {
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" },
});

View file

@ -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,7 +53,7 @@ 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",
@ -71,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([]);
});
});
});

View file

@ -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" }],
};

View file

@ -52,8 +52,8 @@ const omeletteDetail = {
id: 10,
key: "egg",
icon: "EGG",
category: "CREMERIE_FROMAGE",
subcategory: "OEUFS",
category: "dairyAndCheese",
subcategory: "eggs",
allergens: [oeufs],
diets: [],
},

View file

@ -23,8 +23,8 @@ const omeletteDetail = {
id: 10,
key: "egg",
icon: "EGG",
category: "CREMERIE_FROMAGE",
subcategory: "OEUFS",
category: "dairyAndCheese",
subcategory: "eggs",
allergens: [oeufs],
diets: [],
},

View file

@ -48,7 +48,7 @@ function FilledIcon({ children }: { children: ReactNode }) {
);
}
/** Carrot (foodiconpack.com, CC BY 4.0 — see the credits page) — root vegetables, leafy greens, and produce generally (`PRODUITS_FRAIS`/`LEGUMES`). */
/** Carrot (foodiconpack.com, CC BY 4.0 — see the credits page) — root vegetables, leafy greens, and produce generally (`freshProduce`/`vegetables`). */
export function VegetableIcon() {
return (
<FilledIcon>
@ -57,7 +57,7 @@ export function VegetableIcon() {
);
}
/** Apple (foodiconpack.com, CC BY 4.0) — `PRODUITS_FRAIS`/`FRUITS`. */
/** Apple (foodiconpack.com, CC BY 4.0) — `freshProduce`/`fruits`. */
export function FruitIcon() {
return (
<FilledIcon>
@ -69,7 +69,7 @@ export function FruitIcon() {
);
}
/** Basil sprig (foodiconpack.com, CC BY 4.0) — `PRODUITS_FRAIS`/`HERBES_FRAICHES`. */
/** Basil sprig (foodiconpack.com, CC BY 4.0) — `freshProduce`/`freshHerbs`. */
export function HerbIcon() {
return (
<FilledIcon>
@ -78,7 +78,7 @@ export function HerbIcon() {
);
}
/** Cut of beef (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`VIANDES`. */
/** Cut of beef (foodiconpack.com, CC BY 4.0) — `meatAndSeafood`/`meats`. */
export function MeatIcon() {
return (
<FilledIcon>
@ -90,7 +90,7 @@ export function MeatIcon() {
);
}
/** Chicken (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`VOLAILLES`. */
/** Chicken (foodiconpack.com, CC BY 4.0) — `meatAndSeafood`/`poultry`. */
export function PoultryIcon() {
return (
<FilledIcon>
@ -101,7 +101,7 @@ export function PoultryIcon() {
);
}
/** Salmon (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`POISSONS`. */
/** Salmon (foodiconpack.com, CC BY 4.0) — `meatAndSeafood`/`fish`. */
export function FishIcon() {
return (
<FilledIcon>
@ -113,7 +113,7 @@ export function FishIcon() {
);
}
/** Shrimp (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`CRUSTACES_FRUITS_DE_MER`. */
/** Shrimp (foodiconpack.com, CC BY 4.0) — `meatAndSeafood`/`shellfish`. */
export function ShellfishIcon() {
return (
<FilledIcon>
@ -123,7 +123,7 @@ export function ShellfishIcon() {
);
}
/** Bowl of rice (foodiconpack.com, CC BY 4.0) — grains, pasta, rice, flour (`EPICERIE_SECHE`/`FECULENTS`, and flours under `AIDES_CULINAIRES`/`BASES`). */
/** Bowl of rice (foodiconpack.com, CC BY 4.0) — grains, pasta, rice, flour (`dryGoods`/`starches`, and flours under `cookingEssentials`/`bases`). */
export function GrainIcon() {
return (
<FilledIcon>
@ -142,7 +142,7 @@ export function GrainIcon() {
);
}
/** Chickpeas (foodiconpack.com, CC BY 4.0) — `EPICERIE_SECHE`/`LEGUMINEUSES`. */
/** Chickpeas (foodiconpack.com, CC BY 4.0) — `dryGoods`/`legumes`. */
export function LegumeIcon() {
return (
<FilledIcon>
@ -158,7 +158,7 @@ export function LegumeIcon() {
);
}
/** Almonds (foodiconpack.com, CC BY 4.0) — nuts, seeds, dried fruit (`EPICERIE_SECHE`/`GRAINES_FRUITS_SECS`). */
/** Almonds (foodiconpack.com, CC BY 4.0) — nuts, seeds, dried fruit (`dryGoods`/`nutsAndSeeds`). */
export function NutSeedIcon() {
return (
<FilledIcon>
@ -174,7 +174,7 @@ export function NutSeedIcon() {
);
}
/** A loaf, scored on top — `BOULANGERIE`/`PAINS`. */
/** A loaf, scored on top — `bakery`/`breads`. */
export function BreadIcon() {
return (
<Icon>
@ -184,7 +184,7 @@ export function BreadIcon() {
);
}
/** Rolling pin — raw, uncooked pastry (`BOULANGERIE`/`PATES_A_CUIRE`). */
/** Rolling pin — raw, uncooked pastry (`bakery`/`rawDough`). */
export function DoughIcon() {
return (
<Icon>
@ -195,7 +195,7 @@ export function DoughIcon() {
);
}
/** Milk carton (foodiconpack.com, CC BY 4.0) — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (non-cheese items). */
/** Milk carton (foodiconpack.com, CC BY 4.0) — `dairyAndCheese`/`dairy` (non-cheese items). */
export function MilkIcon() {
return (
<FilledIcon>
@ -206,7 +206,7 @@ export function MilkIcon() {
);
}
/** Wedge of cheddar (foodiconpack.com, CC BY 4.0) — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (cheese items). */
/** Wedge of cheddar (foodiconpack.com, CC BY 4.0) — `dairyAndCheese`/`dairy` (cheese items). */
export function CheeseIcon() {
return (
<FilledIcon>
@ -225,7 +225,7 @@ export function CheeseIcon() {
);
}
/** Eggs (foodiconpack.com, CC BY 4.0) — `CREMERIE_FROMAGE`/`OEUFS`. */
/** Eggs (foodiconpack.com, CC BY 4.0) — `dairyAndCheese`/`eggs`. */
export function EggIcon() {
return (
<FilledIcon>
@ -238,7 +238,7 @@ export function EggIcon() {
);
}
/** A seedling — plant-based dairy/meat alternatives (`CREMERIE_FROMAGE`/`ALTERNATIVES`). */
/** A seedling — plant-based dairy/meat alternatives (`dairyAndCheese`/`plantBasedAlternatives`). */
export function SproutIcon() {
return (
<Icon>
@ -249,7 +249,7 @@ export function SproutIcon() {
);
}
/** Cinnamon sticks (foodiconpack.com, CC BY 4.0) — dried spices/herbs (`CONDIMENTS_EPICES`/`EPICES`). */
/** Cinnamon sticks (foodiconpack.com, CC BY 4.0) — dried spices/herbs (`condimentsAndSpices`/`spices`). */
export function SpiceIcon() {
return (
<FilledIcon>
@ -262,7 +262,7 @@ export function SpiceIcon() {
);
}
/** Honey jar (foodiconpack.com, CC BY 4.0) — sauces, pickles, tinned/preserved goods (`CONDIMENTS_EPICES`/`SAUCES` and the `EPICERIE_SECHE`/`AUTRES` catch-all). */
/** Honey jar (foodiconpack.com, CC BY 4.0) — sauces, pickles, tinned/preserved goods (`condimentsAndSpices`/`sauces` and the `dryGoods`/`other` catch-all). */
export function JarIcon() {
return (
<FilledIcon>
@ -272,7 +272,7 @@ export function JarIcon() {
);
}
/** Oil bottle (foodiconpack.com, CC BY 4.0) — oils, vinegars (`CONDIMENTS_EPICES`/`ASSAISONNEMENTS`, the pourable subset). */
/** Oil bottle (foodiconpack.com, CC BY 4.0) — oils, vinegars (`condimentsAndSpices`/`seasonings`, the pourable subset). */
export function BottleIcon() {
return (
<FilledIcon>
@ -283,7 +283,7 @@ export function BottleIcon() {
);
}
/** Glass (foodiconpack.com, CC BY 4.0) — juices, coffee/tea, cooking alcohols, water (`CONDIMENTS_EPICES`/`ASSAISONNEMENTS`'s drinkable subset). */
/** Glass (foodiconpack.com, CC BY 4.0) — juices, coffee/tea, cooking alcohols, water (`condimentsAndSpices`/`seasonings`'s drinkable subset). */
export function DrinkIcon() {
return (
<FilledIcon>
@ -295,7 +295,7 @@ export function DrinkIcon() {
);
}
/** Stockpot (foodiconpack.com, CC BY 4.0) — broths, stocks, water bases (`AIDES_CULINAIRES`/`BASES`'s liquid-base subset). */
/** Stockpot (foodiconpack.com, CC BY 4.0) — broths, stocks, water bases (`cookingEssentials`/`bases`'s liquid-base subset). */
export function StockPotIcon() {
return (
<FilledIcon>
@ -304,7 +304,7 @@ export function StockPotIcon() {
);
}
/** Sugar (foodiconpack.com, CC BY 4.0) — `AIDES_CULINAIRES`/`SUCRES`. */
/** Sugar (foodiconpack.com, CC BY 4.0) — `cookingEssentials`/`sugars`. */
export function SugarIcon() {
return (
<FilledIcon>
@ -355,13 +355,13 @@ export function IngredientTypeIcon({ icon }: { icon: IngredientIconType }) {
* derived fact.
*/
export const CATEGORY_ICON: Record<IngredientCategory, IngredientIconType> = {
PRODUITS_FRAIS: "VEGETABLE",
BOUCHERIE_POISSONNERIE: "MEAT",
EPICERIE_SECHE: "GRAIN",
BOULANGERIE: "BREAD",
CREMERIE_FROMAGE: "CHEESE",
CONDIMENTS_EPICES: "SPICE",
AIDES_CULINAIRES: "STOCK_POT",
freshProduce: "VEGETABLE",
meatAndSeafood: "MEAT",
dryGoods: "GRAIN",
bakery: "BREAD",
dairyAndCheese: "CHEESE",
condimentsAndSpices: "SPICE",
cookingEssentials: "STOCK_POT",
};
/** Renders {@link CATEGORY_ICON}'s pictogram for one category — used by the category chip row. */
@ -373,33 +373,33 @@ export function CategoryIcon({ category }: { category: IngredientCategory }) {
* One representative {@link IngredientIconType} per {@link IngredientSubcategory}
* rack, for `IngredientPicker`'s second-tier subcategory chips. Most map
* 1:1 onto their subcategory's dominant shape; a couple of heterogeneous
* subcategories (`ASSAISONNEMENTS` mixes oils with juices and coffee,
* `BASES` mixes flour with stock and canned tomato) get one illustrative
* subcategories (`seasonings` mixes oils with juices and coffee,
* `bases` mixes flour with stock and canned tomato) get one illustrative
* pick rather than a derived fact, same reasoning as {@link CATEGORY_ICON}.
*/
export const SUBCATEGORY_ICON: Record<IngredientSubcategory, IngredientIconType> = {
LEGUMES: "VEGETABLE",
FRUITS: "FRUIT",
HERBES_FRAICHES: "HERB",
VIANDES: "MEAT",
VOLAILLES: "POULTRY",
POISSONS: "FISH",
CRUSTACES_FRUITS_DE_MER: "SHELLFISH",
FECULENTS: "GRAIN",
LEGUMINEUSES: "LEGUME",
GRAINES_FRUITS_SECS: "NUT_SEED",
AUTRES: "JAR",
PAINS: "BREAD",
PATES_A_CUIRE: "DOUGH",
PRODUITS_LAITIERS: "MILK",
OEUFS: "EGG",
ALTERNATIVES: "SPROUT",
EPICES: "SPICE",
SAUCES: "JAR",
ASSAISONNEMENTS: "BOTTLE",
BASES: "STOCK_POT",
EPAISSISSANTS: "JAR",
SUCRES: "SUGAR",
vegetables: "VEGETABLE",
fruits: "FRUIT",
freshHerbs: "HERB",
meats: "MEAT",
poultry: "POULTRY",
fish: "FISH",
shellfish: "SHELLFISH",
starches: "GRAIN",
legumes: "LEGUME",
nutsAndSeeds: "NUT_SEED",
other: "JAR",
breads: "BREAD",
rawDough: "DOUGH",
dairy: "MILK",
eggs: "EGG",
plantBasedAlternatives: "SPROUT",
spices: "SPICE",
sauces: "JAR",
seasonings: "BOTTLE",
bases: "STOCK_POT",
thickeners: "JAR",
sugars: "SUGAR",
};
/** Renders {@link SUBCATEGORY_ICON}'s pictogram for one subcategory — used by the subcategory chip row. */

View file

@ -192,37 +192,37 @@
"allSubcategories": "Tout",
"noIngredientFound": "Aucun ingrédient trouvé.",
"category": {
"PRODUITS_FRAIS": "Produits frais",
"BOUCHERIE_POISSONNERIE": "Boucherie & poissonnerie",
"EPICERIE_SECHE": "Épicerie sèche",
"BOULANGERIE": "Boulangerie",
"CREMERIE_FROMAGE": "Crémerie & fromage",
"CONDIMENTS_EPICES": "Condiments & épices",
"AIDES_CULINAIRES": "Aides culinaires"
"freshProduce": "Produits frais",
"meatAndSeafood": "Boucherie & poissonnerie",
"dryGoods": "Épicerie sèche",
"bakery": "Boulangerie",
"dairyAndCheese": "Crémerie & fromage",
"condimentsAndSpices": "Condiments & épices",
"cookingEssentials": "Aides culinaires"
},
"subcategory": {
"LEGUMES": "Légumes",
"FRUITS": "Fruits",
"HERBES_FRAICHES": "Herbes fraîches",
"VIANDES": "Viandes",
"VOLAILLES": "Volailles",
"POISSONS": "Poissons",
"CRUSTACES_FRUITS_DE_MER": "Crustacés & fruits de mer",
"FECULENTS": "Féculents",
"LEGUMINEUSES": "Légumineuses",
"GRAINES_FRUITS_SECS": "Graines & fruits secs",
"AUTRES": "Autres",
"PAINS": "Pains",
"PATES_A_CUIRE": "Pâtes à cuire",
"PRODUITS_LAITIERS": "Produits laitiers",
"OEUFS": "Œufs",
"ALTERNATIVES": "Alternatives végétales",
"EPICES": "Épices",
"SAUCES": "Sauces",
"ASSAISONNEMENTS": "Assaisonnements",
"BASES": "Bases",
"EPAISSISSANTS": "Épaississants",
"SUCRES": "Sucres"
"vegetables": "Légumes",
"fruits": "Fruits",
"freshHerbs": "Herbes fraîches",
"meats": "Viandes",
"poultry": "Volailles",
"fish": "Poissons",
"shellfish": "Crustacés & fruits de mer",
"starches": "Féculents",
"legumes": "Légumineuses",
"nutsAndSeeds": "Graines & fruits secs",
"other": "Autres",
"breads": "Pains",
"rawDough": "Pâtes à cuire",
"dairy": "Produits laitiers",
"eggs": "Œufs",
"plantBasedAlternatives": "Alternatives végétales",
"spices": "Épices",
"sauces": "Sauces",
"seasonings": "Assaisonnements",
"bases": "Bases",
"thickeners": "Épaississants",
"sugars": "Sucres"
},
"quantityLabel": "Quantité",
"unitLabel": "Unité",
@ -322,7 +322,7 @@
"vegetarian": "Végétarien",
"vegan": "Végan",
"pescatarian": "Pescétarien",
"gluten_free": "Sans gluten"
"glutenFree": "Sans gluten"
},
"allergens": {
"gluten": "Gluten",
@ -332,10 +332,10 @@
"peanuts": "Arachides",
"soy": "Soja",
"milk": "Lait",
"tree_nuts": "Fruits à coque",
"treeNuts": "Fruits à coque",
"celery": "Céleri",
"mustard": "Moutarde",
"sesame_seeds": "Graines de sésame",
"sesameSeeds": "Graines de sésame",
"sulfites": "Sulfites",
"lupin": "Lupin",
"molluscs": "Mollusques"
@ -349,17 +349,17 @@
"zucchini": "Courgette",
"cucumber": "Concombre",
"gherkins": "Cornichons",
"bell_pepper": "Poivron",
"bellPepper": "Poivron",
"mushroom": "Champignon",
"porcini": "Cèpes",
"eggplant": "Aubergine",
"broccoli": "Brocoli",
"cauliflower": "Chou-fleur",
"white_cabbage": "Chou blanc",
"red_cabbage": "Chou rouge",
"brussels_sprouts": "Chou de Bruxelles",
"whiteCabbage": "Chou blanc",
"redCabbage": "Chou rouge",
"brusselsSprouts": "Chou de Bruxelles",
"spinach": "Épinard",
"swiss_chard": "Blette",
"swissChard": "Blette",
"lettuce": "Salade",
"arugula": "Roquette",
"watercress": "Cresson",
@ -369,37 +369,37 @@
"beetroot": "Betterave",
"turnip": "Navet",
"parsnip": "Panais",
"green_bean": "Haricot vert",
"greenBean": "Haricot vert",
"pea": "Petit pois",
"corn": "Maïs",
"artichoke": "Artichaut",
"fennel": "Fenouil",
"endive": "Endive",
"pumpkin": "Potiron",
"butternut_squash": "Butternut",
"butternutSquash": "Butternut",
"asparagus": "Asperge",
"avocado": "Avocat",
"potato": "Pomme de terre",
"sweet_potato": "Patate douce",
"cherry_tomato": "Tomates cerises",
"bok_choy": "Pak-choï",
"soybean_sprouts": "Germes de soja",
"sweetPotato": "Patate douce",
"cherryTomato": "Tomates cerises",
"bokChoy": "Pak-choï",
"soybeanSprouts": "Germes de soja",
"shiitake": "Shiitake",
"daikon": "Daikon",
"fresh_green_chili": "Piment vert frais",
"freshGreenChili": "Piment vert frais",
"cardoon": "Cardon",
"radicchio": "Chicorée rouge",
"romanesco": "Chou romanesco",
"kohlrabi": "Chou-rave",
"napa_cabbage": "Chou chinois",
"napaCabbage": "Chou chinois",
"celeriac": "Céleri-rave",
"okra": "Gombo",
"spring_onion": "Oignon nouveau",
"red_kuri_squash": "Potimarron",
"springOnion": "Oignon nouveau",
"redKuriSquash": "Potimarron",
"rutabaga": "Rutabaga",
"samphire": "Salicorne",
"salsify": "Salsifis",
"lambs_lettuce": "Mâche",
"lambsLettuce": "Mâche",
"escarole": "Scarole",
"lemon": "Citron",
"lime": "Citron vert",
@ -439,9 +439,9 @@
"parsley": "Persil",
"thyme": "Thym",
"rosemary": "Romarin",
"bay_leaf": "Laurier",
"bayLeaf": "Laurier",
"chives": "Ciboulette",
"fresh_cilantro": "Coriandre fraîche",
"freshCilantro": "Coriandre fraîche",
"mint": "Menthe",
"oregano": "Origan",
"dill": "Aneth",
@ -452,20 +452,20 @@
"chervil": "Cerfeuil",
"ginger": "Gingembre",
"lemongrass": "Citronnelle",
"kaffir_lime": "Combava",
"kaffirLime": "Combava",
"rabbit": "Lapin",
"ground_beef": "Bœuf haché",
"beef_steak": "Steak de bœuf",
"beef_roast": "Rôti de bœuf",
"veal_cutlet": "Escalope de veau",
"pork_tenderloin": "Filet mignon de porc",
"pork_chop": "Côte de porc",
"groundBeef": "Bœuf haché",
"beefSteak": "Steak de bœuf",
"beefRoast": "Rôti de bœuf",
"vealCutlet": "Escalope de veau",
"porkTenderloin": "Filet mignon de porc",
"porkChop": "Côte de porc",
"lamb": "Agneau",
"leg_of_lamb": "Gigot d'agneau",
"bacon_lardons": "Lardons",
"legOfLamb": "Gigot d'agneau",
"baconLardons": "Lardons",
"bacon": "Bacon",
"ham": "Jambon blanc",
"cured_ham": "Jambon cru",
"curedHam": "Jambon cru",
"sausage": "Saucisse",
"chorizo": "Chorizo",
"merguez": "Merguez",
@ -475,36 +475,36 @@
"salami": "Salami",
"andouille": "Andouille",
"andouillette": "Andouillette",
"white_pudding": "Boudin blanc",
"black_pudding": "Boudin noir",
"whitePudding": "Boudin blanc",
"blackPudding": "Boudin noir",
"cervelat": "Cervelas",
"rillettes": "Rillettes",
"dry_cured_sausage": "Saucisson sec",
"bayonne_ham": "Jambon de Bayonne",
"dryCuredSausage": "Saucisson sec",
"bayonneHam": "Jambon de Bayonne",
"coppa": "Coppa",
"rosette_sausage": "Rosette (saucisson)",
"veal_liver": "Foie de veau",
"veal_kidneys": "Rognons de veau",
"veal_brain": "Cervelle de veau",
"veal_sweetbread": "Ris de veau",
"beef_tongue": "Langue de bœuf",
"rosetteSausage": "Rosette (saucisson)",
"vealLiver": "Foie de veau",
"vealKidneys": "Rognons de veau",
"vealBrain": "Cervelle de veau",
"vealSweetbread": "Ris de veau",
"beefTongue": "Langue de bœuf",
"tripe": "Tripes",
"venison": "Cerf",
"roe_deer": "Chevreuil",
"wild_boar": "Sanglier",
"horse_meat": "Cheval",
"beef_heart": "Cœur de bœuf",
"foie_gras": "Foie gras",
"beef_muzzle": "Museau de bœuf",
"grisons_dried_beef": "Viande des Grisons",
"roeDeer": "Chevreuil",
"wildBoar": "Sanglier",
"horseMeat": "Cheval",
"beefHeart": "Cœur de bœuf",
"foieGras": "Foie gras",
"beefMuzzle": "Museau de bœuf",
"grisonsDriedBeef": "Viande des Grisons",
"chicken": "Poulet",
"turkey": "Dinde",
"duck": "Canard",
"duck_breast": "Magret de canard",
"duckBreast": "Magret de canard",
"quail": "Caille",
"guinea_fowl": "Pintade",
"guineaFowl": "Pintade",
"goose": "Oie",
"poultry_liver": "Foie de volaille",
"poultryLiver": "Foie de volaille",
"capon": "Chapon",
"pigeon": "Pigeon",
"pheasant": "Faisan",
@ -516,8 +516,8 @@
"anchovy": "Anchois",
"whiting": "Merlan",
"surimi": "Surimi",
"sea_bass": "Bar (loup de mer)",
"sea_bream": "Dorade",
"seaBass": "Bar (loup de mer)",
"seaBream": "Dorade",
"sole": "Sole",
"turbot": "Turbot",
"hake": "Merlu",
@ -526,7 +526,7 @@
"haddock": "Églefin",
"mackerel": "Maquereau",
"herring": "Hareng",
"red_mullet": "Rouget",
"redMullet": "Rouget",
"skate": "Raie",
"monkfish": "Lotte",
"halibut": "Flétan",
@ -536,18 +536,18 @@
"perch": "Perche",
"tilapia": "Tilapia",
"pangasius": "Panga",
"smoked_salmon": "Saumon fumé",
"dried_fish": "Poisson séché",
"smokedSalmon": "Saumon fumé",
"driedFish": "Poisson séché",
"eel": "Anguille",
"plaice": "Carrelet (ou plie)",
"salt_cod": "Morue",
"lemon_sole": "Limande",
"saltCod": "Morue",
"lemonSole": "Limande",
"scorpionfish": "Rascasse",
"shrimp": "Crevettes",
"langoustine": "Langoustines",
"lobster": "Homard",
"crab": "Crabe",
"spiny_lobster": "Langouste",
"spinyLobster": "Langouste",
"mussels": "Moules",
"oysters": "Huîtres",
"scallops": "Saint-Jacques",
@ -555,10 +555,10 @@
"octopus": "Poulpe",
"clams": "Palourdes",
"whelks": "Bulots",
"spider_crab": "Araignée de mer",
"spiderCrab": "Araignée de mer",
"periwinkle": "Bigorneau",
"crayfish": "Écrevisse",
"grey_shrimp": "Crevette grise",
"greyShrimp": "Crevette grise",
"cockle": "Coque",
"snail": "Escargot",
"cuttlefish": "Seiche",
@ -568,103 +568,103 @@
"polenta": "Polenta",
"quinoa": "Quinoa",
"pasta": "Pâtes",
"whole_wheat_pasta": "Pâtes complètes",
"wholeWheatPasta": "Pâtes complètes",
"rice": "Riz",
"basmati_rice": "Riz basmati",
"brown_rice": "Riz complet",
"basmatiRice": "Riz basmati",
"brownRice": "Riz complet",
"oats": "Flocons d'avoine",
"spaghetti": "Spaghetti",
"penne": "Penne",
"tagliatelle": "Tagliatelles",
"lasagna_sheets": "Lasagnes (feuilles)",
"lasagnaSheets": "Lasagnes (feuilles)",
"gnocchi": "Gnocchi",
"arborio_rice": "Riz arborio",
"rice_noodles": "Nouilles de riz",
"udon_noodles": "Nouilles udon",
"soba_noodles": "Nouilles soba",
"chinese_noodles": "Nouilles chinoises",
"rice_vermicelli": "Vermicelles de riz",
"soy_vermicelli": "Vermicelles de soja",
"sticky_rice": "Riz gluant",
"sushi_rice": "Riz à sushi",
"jasmine_rice": "Riz jasmin",
"green_lentils": "Lentilles vertes",
"red_lentils": "Lentilles corail",
"arborioRice": "Riz arborio",
"riceNoodles": "Nouilles de riz",
"udonNoodles": "Nouilles udon",
"sobaNoodles": "Nouilles soba",
"chineseNoodles": "Nouilles chinoises",
"riceVermicelli": "Vermicelles de riz",
"soyVermicelli": "Vermicelles de soja",
"stickyRice": "Riz gluant",
"sushiRice": "Riz à sushi",
"jasmineRice": "Riz jasmin",
"greenLentils": "Lentilles vertes",
"redLentils": "Lentilles corail",
"chickpeas": "Pois chiches",
"white_beans": "Haricots blancs",
"kidney_beans": "Haricots rouges",
"black_beans": "Haricots noirs",
"split_peas": "Pois cassés",
"fava_beans": "Fèves",
"whiteBeans": "Haricots blancs",
"kidneyBeans": "Haricots rouges",
"blackBeans": "Haricots noirs",
"splitPeas": "Pois cassés",
"favaBeans": "Fèves",
"edamame": "Edamame",
"pinto_beans": "Haricots pinto",
"flageolet_beans": "Haricots flageolets",
"golden_lentils": "Lentilles blondes",
"peanuts_shelled": "Cacahuètes",
"pintoBeans": "Haricots pinto",
"flageoletBeans": "Haricots flageolets",
"goldenLentils": "Lentilles blondes",
"peanutsShelled": "Cacahuètes",
"almonds": "Amandes",
"walnuts": "Noix",
"hazelnuts": "Noisettes",
"cashews": "Noix de cajou",
"pistachios": "Pistaches",
"pecans": "Noix de pécan",
"almond_powder": "Poudre d'amande",
"pine_nuts": "Pignons de pin",
"sunflower_seeds": "Graines de tournesol",
"pumpkin_seeds": "Graines de courge",
"shredded_coconut": "Noix de coco râpée",
"almondPowder": "Poudre d'amande",
"pineNuts": "Pignons de pin",
"sunflowerSeeds": "Graines de tournesol",
"pumpkinSeeds": "Graines de courge",
"shreddedCoconut": "Noix de coco râpée",
"raisins": "Raisins secs",
"prunes": "Pruneaux",
"dried_apricots": "Abricots secs",
"sesame_seeds": "Graines de sésame",
"black_mushrooms": "Champignons noirs",
"nori_seaweed": "Algue nori",
"wakame_seaweed": "Algue wakamé",
"kombu_seaweed": "Algue kombu",
"bamboo_shoots": "Pousses de bambou",
"water_chestnuts": "Châtaignes d'eau",
"driedApricots": "Abricots secs",
"sesameSeeds": "Graines de sésame",
"blackMushrooms": "Champignons noirs",
"noriSeaweed": "Algue nori",
"wakameSeaweed": "Algue wakamé",
"kombuSeaweed": "Algue kombu",
"bambooShoots": "Pousses de bambou",
"waterChestnuts": "Châtaignes d'eau",
"bread": "Pain",
"sandwich_bread": "Pain de mie",
"whole_wheat_bread": "Pain complet",
"sandwichBread": "Pain de mie",
"wholeWheatBread": "Pain complet",
"baguette": "Baguette",
"rye_bread": "Pain de seigle",
"ryeBread": "Pain de seigle",
"breadcrumbs": "Chapelure",
"burger_bun": "Pain à burger",
"brioche_bun": "Pain brioché",
"hot_dog_bun": "Pain à hot-dog",
"pita_bread": "Pain pita",
"burgerBun": "Pain à burger",
"briocheBun": "Pain brioché",
"hotDogBun": "Pain à hot-dog",
"pitaBread": "Pain pita",
"bagel": "Pain bagel",
"naan": "Naan",
"wrap_bread": "Pain wrap",
"viennese_bread": "Pain viennois",
"country_bread": "Pain de campagne",
"multigrain_bread": "Pain aux céréales",
"bread_roll": "Petit pain",
"swedish_bread": "Pain suédois",
"gluten_free_bread": "Pain sans gluten",
"wrapBread": "Pain wrap",
"vienneseBread": "Pain viennois",
"countryBread": "Pain de campagne",
"multigrainBread": "Pain aux céréales",
"breadRoll": "Petit pain",
"swedishBread": "Pain suédois",
"glutenFreeBread": "Pain sans gluten",
"rusk": "Biscotte",
"croutons": "Croûtons",
"focaccia": "Focaccia",
"ciabatta": "Ciabatta",
"corn_tortilla": "Tortilla de maïs",
"wheat_tortilla": "Tortilla de blé",
"cornTortilla": "Tortilla de maïs",
"wheatTortilla": "Tortilla de blé",
"breadstick": "Gressin",
"puff_pastry": "Pâte feuilletée",
"shortcrust_pastry": "Pâte brisée",
"pizza_dough": "Pâte à pizza",
"sweet_shortcrust_pastry": "Pâte à tarte sablée",
"puffPastry": "Pâte feuilletée",
"shortcrustPastry": "Pâte brisée",
"pizzaDough": "Pâte à pizza",
"sweetShortcrustPastry": "Pâte à tarte sablée",
"milk": "Lait",
"butter": "Beurre",
"creme_fraiche": "Crème fraîche",
"liquid_cream": "Crème liquide",
"cremeFraiche": "Crème fraîche",
"liquidCream": "Crème liquide",
"cheese": "Fromage",
"emmental": "Emmental",
"gruyere": "Gruyère",
"parmesan": "Parmesan",
"mozzarella": "Mozzarella",
"goat_cheese": "Chèvre (fromage)",
"goatCheese": "Chèvre (fromage)",
"feta": "Feta",
"comte": "Comté",
"fromage_blanc": "Fromage blanc",
"fromageBlanc": "Fromage blanc",
"mascarpone": "Mascarpone",
"yogurt": "Yaourt",
"burrata": "Burrata",
@ -679,214 +679,214 @@
"reblochon": "Reblochon",
"cantal": "Cantal",
"beaufort": "Beaufort",
"saint_nectaire": "Saint-Nectaire",
"blue_cheese": "Bleu (fromage)",
"saintNectaire": "Saint-Nectaire",
"blueCheese": "Bleu (fromage)",
"cancoillotte": "Cancoillotte",
"tomme": "Tomme",
"epoisses": "Époisses",
"chaource": "Chaource",
"livarot": "Livarot",
"pont_leveque": "Pont-l'Évêque",
"pontLeveque": "Pont-l'Évêque",
"morbier": "Morbier",
"raclette_cheese": "Raclette (fromage)",
"fourme_d_ambert": "Fourme d'Ambert",
"racletteCheese": "Raclette (fromage)",
"fourmeDAmbert": "Fourme d'Ambert",
"salers": "Salers",
"ossau_iraty": "Ossau-Iraty",
"ossauIraty": "Ossau-Iraty",
"vacherin": "Vacherin",
"saint_marcellin": "Saint-Marcellin",
"saintMarcellin": "Saint-Marcellin",
"neufchatel": "Neufchâtel",
"crottin_de_chavignol": "Crottin de Chavignol",
"abondance_cheese": "Abondance",
"carre_de_l_est": "Carré de l'Est",
"crottinDeChavignol": "Crottin de Chavignol",
"abondanceCheese": "Abondance",
"carreDeLEst": "Carré de l'Est",
"edam": "Edam",
"gouda": "Gouda",
"mimolette": "Mimolette",
"maroilles": "Maroilles",
"mont_dor": "Mont d'or",
"montDor": "Mont d'or",
"kefir": "Kéfir",
"greek_yogurt": "Yaourt à la grecque",
"greekYogurt": "Yaourt à la grecque",
"egg": "Œuf",
"coconut_milk": "Lait de coco",
"coconut_cream": "Crème de coco",
"almond_milk": "Lait d'amande",
"oat_milk": "Lait d'avoine",
"coconutMilk": "Lait de coco",
"coconutCream": "Crème de coco",
"almondMilk": "Lait d'amande",
"oatMilk": "Lait d'avoine",
"tofu": "Tofu",
"silken_tofu": "Tofu soyeux",
"herbes_de_provence": "Herbes de Provence",
"black_pepper": "Poivre noir",
"silkenTofu": "Tofu soyeux",
"herbesDeProvence": "Herbes de Provence",
"blackPepper": "Poivre noir",
"paprika": "Paprika",
"espelette_pepper": "Piment d'Espelette",
"cayenne_pepper": "Piment de Cayenne",
"espelettePepper": "Piment d'Espelette",
"cayennePepper": "Piment de Cayenne",
"cumin": "Cumin",
"curry_powder": "Curry (poudre)",
"curryPowder": "Curry (poudre)",
"turmeric": "Curcuma",
"cinnamon": "Cannelle",
"nutmeg": "Muscade",
"saffron": "Safran",
"clove": "Clou de girofle",
"vanilla_bean": "Vanille (gousse)",
"white_pepper": "Poivre blanc",
"pink_pepper": "Poivre rose",
"sichuan_pepper": "Poivre du Sichuan",
"smoked_paprika": "Paprika fumé",
"bird_eye_chili": "Piment oiseau",
"juniper_berries": "Baies de genièvre",
"star_anise": "Anis étoilé (badiane)",
"green_anise": "Anis vert",
"fennel_seeds": "Graines de fenouil",
"vanillaBean": "Vanille (gousse)",
"whitePepper": "Poivre blanc",
"pinkPepper": "Poivre rose",
"sichuanPepper": "Poivre du Sichuan",
"smokedPaprika": "Paprika fumé",
"birdEyeChili": "Piment oiseau",
"juniperBerries": "Baies de genièvre",
"starAnise": "Anis étoilé (badiane)",
"greenAnise": "Anis vert",
"fennelSeeds": "Graines de fenouil",
"sumac": "Sumac",
"nigella": "Nigelle",
"allspice": "Quatre épices",
"colombo_powder": "Colombo (poudre)",
"colomboPowder": "Colombo (poudre)",
"baharat": "Baharat",
"horseradish": "Raifort",
"herb_salt": "Sel aux herbes",
"celery_salt": "Sel de céleri",
"fleur_de_sel": "Fleur de sel",
"herbSalt": "Sel aux herbes",
"celerySalt": "Sel de céleri",
"fleurDeSel": "Fleur de sel",
"salt": "Sel",
"five_spice": "Cinq épices",
"garam_masala": "Garam masala",
"coriander_seeds": "Graines de coriandre",
"fiveSpice": "Cinq épices",
"garamMasala": "Garam masala",
"corianderSeeds": "Graines de coriandre",
"cardamom": "Cardamome",
"fenugreek": "Fenugrec",
"jalapeno": "Piment jalapeño",
"chipotle": "Piment chipotle",
"poblano_pepper": "Piment poblano",
"poblanoPepper": "Piment poblano",
"habanero": "Piment habanero",
"ras_el_hanout": "Ras el hanout",
"rasElHanout": "Ras el hanout",
"zaatar": "Za'atar",
"soy_sauce": "Sauce soja",
"soySauce": "Sauce soja",
"mustard": "Moutarde",
"mayonnaise": "Mayonnaise",
"ketchup": "Ketchup",
"tabasco": "Tabasco",
"worcestershire_sauce": "Sauce Worcestershire",
"fish_sauce": "Sauce nuoc-mâm",
"worcestershireSauce": "Sauce Worcestershire",
"fishSauce": "Sauce nuoc-mâm",
"wasabi": "Wasabi",
"harissa": "Harissa",
"curry_paste": "Pâte de curry",
"peanut_butter": "Beurre de cacahuète",
"dijon_mustard": "Moutarde de Dijon",
"wholegrain_mustard": "Moutarde à l'ancienne",
"barbecue_sauce": "Sauce barbecue",
"tartar_sauce": "Sauce tartare",
"cocktail_sauce": "Sauce cocktail",
"bearnaise_sauce": "Sauce béarnaise",
"hollandaise_sauce": "Sauce hollandaise",
"bechamel_sauce": "Sauce béchamel",
"teriyaki_sauce": "Sauce teriyaki",
"ponzu_sauce": "Sauce ponzu",
"curryPaste": "Pâte de curry",
"peanutButter": "Beurre de cacahuète",
"dijonMustard": "Moutarde de Dijon",
"wholegrainMustard": "Moutarde à l'ancienne",
"barbecueSauce": "Sauce barbecue",
"tartarSauce": "Sauce tartare",
"cocktailSauce": "Sauce cocktail",
"bearnaiseSauce": "Sauce béarnaise",
"hollandaiseSauce": "Sauce hollandaise",
"bechamelSauce": "Sauce béchamel",
"teriyakiSauce": "Sauce teriyaki",
"ponzuSauce": "Sauce ponzu",
"chimichurri": "Chimichurri",
"red_pesto": "Pesto rouge (tomates séchées)",
"redPesto": "Pesto rouge (tomates séchées)",
"pesto": "Pesto",
"oyster_sauce": "Sauce huître",
"hoisin_sauce": "Sauce hoisin",
"oysterSauce": "Sauce huître",
"hoisinSauce": "Sauce hoisin",
"sriracha": "Sauce sriracha",
"sweet_chili_sauce": "Sauce sweet chili",
"sweetChiliSauce": "Sauce sweet chili",
"miso": "Miso",
"shrimp_paste": "Pâte de crevettes",
"red_curry_paste": "Pâte de curry rouge (thaï)",
"green_curry_paste": "Pâte de curry vert (thaï)",
"shrimpPaste": "Pâte de crevettes",
"redCurryPaste": "Pâte de curry rouge (thaï)",
"greenCurryPaste": "Pâte de curry vert (thaï)",
"tahini": "Tahini",
"aioli": "Aïoli",
"vinaigrette": "Sauce vinaigrette",
"hummus": "Houmous",
"olive_oil": "Huile d'olive",
"sunflower_oil": "Huile de tournesol",
"rapeseed_oil": "Huile de colza",
"coconut_oil": "Huile de coco",
"sesame_oil": "Huile de sésame",
"cider_vinegar": "Vinaigre de cidre",
"white_vinegar": "Vinaigre blanc",
"balsamic_vinegar": "Vinaigre balsamique",
"oliveOil": "Huile d'olive",
"sunflowerOil": "Huile de tournesol",
"rapeseedOil": "Huile de colza",
"coconutOil": "Huile de coco",
"sesameOil": "Huile de sésame",
"ciderVinegar": "Vinaigre de cidre",
"whiteVinegar": "Vinaigre blanc",
"balsamicVinegar": "Vinaigre balsamique",
"capers": "Câpres",
"olives": "Olives",
"black_olives": "Olives noires",
"green_olives": "Olives vertes",
"white_wine": "Vin blanc (cuisine)",
"red_wine": "Vin rouge (cuisine)",
"rose_wine": "Vin rosé (cuisine)",
"red_wine_vinegar": "Vinaigre de vin rouge",
"white_wine_vinegar": "Vinaigre de vin blanc",
"sherry_vinegar": "Vinaigre de xérès",
"walnut_oil": "Huile de noix",
"hazelnut_oil": "Huile de noisette",
"peanut_oil": "Huile d'arachide",
"chili_oil": "Huile pimentée",
"rice_vinegar": "Vinaigre de riz",
"corn_oil": "Huile de maïs",
"grapeseed_oil": "Huile de pépins de raisin",
"soybean_oil": "Huile de soja",
"palm_oil": "Huile de palme",
"blackOlives": "Olives noires",
"greenOlives": "Olives vertes",
"whiteWine": "Vin blanc (cuisine)",
"redWine": "Vin rouge (cuisine)",
"roseWine": "Vin rosé (cuisine)",
"redWineVinegar": "Vinaigre de vin rouge",
"whiteWineVinegar": "Vinaigre de vin blanc",
"sherryVinegar": "Vinaigre de xérès",
"walnutOil": "Huile de noix",
"hazelnutOil": "Huile de noisette",
"peanutOil": "Huile d'arachide",
"chiliOil": "Huile pimentée",
"riceVinegar": "Vinaigre de riz",
"cornOil": "Huile de maïs",
"grapeseedOil": "Huile de pépins de raisin",
"soybeanOil": "Huile de soja",
"palmOil": "Huile de palme",
"mirin": "Mirin",
"sake": "Saké (cuisine)",
"lemon_juice": "Jus de citron",
"lime_juice": "Jus de citron vert",
"orange_juice": "Jus d'orange",
"apple_juice": "Jus de pomme",
"grape_juice": "Jus de raisin",
"tomato_juice": "Jus de tomate",
"cranberry_juice": "Jus de cranberry",
"lemonJuice": "Jus de citron",
"limeJuice": "Jus de citron vert",
"orangeJuice": "Jus d'orange",
"appleJuice": "Jus de pomme",
"grapeJuice": "Jus de raisin",
"tomatoJuice": "Jus de tomate",
"cranberryJuice": "Jus de cranberry",
"coffee": "Café",
"tea": "Thé",
"beer": "Bière (cuisine)",
"cider": "Cidre (cuisine)",
"champagne": "Champagne / vin pétillant (cuisine)",
"port_wine": "Porto (cuisine)",
"vin_jaune": "Vin jaune (cuisine)",
"portWine": "Porto (cuisine)",
"vinJaune": "Vin jaune (cuisine)",
"cognac": "Cognac",
"rum": "Rhum",
"whisky": "Whisky",
"vodka": "Vodka",
"wheat_flour": "Farine de blé",
"whole_wheat_flour": "Farine complète",
"corn_flour": "Farine de maïs",
"buckwheat_flour": "Farine de sarrasin",
"rice_flour": "Farine de riz",
"vegetable_stock_cube": "Bouillon cube légumes",
"chicken_stock_cube": "Bouillon cube volaille",
"tomato_paste": "Concentré de tomate",
"tomato_coulis": "Coulis de tomate",
"canned_peeled_tomatoes": "Tomates pelées (conserve)",
"sun_dried_tomatoes": "Tomates séchées",
"veal_stock": "Fond de veau",
"chicken_stock": "Fond de volaille",
"beef_stock_cube": "Bouillon cube bœuf",
"fish_stock_cube": "Bouillon cube poisson",
"vegetable_broth": "Bouillon de légumes",
"chicken_broth": "Bouillon de volaille",
"beef_broth": "Bouillon de bœuf",
"court_bouillon": "Court-bouillon",
"wheatFlour": "Farine de blé",
"wholeWheatFlour": "Farine complète",
"cornFlour": "Farine de maïs",
"buckwheatFlour": "Farine de sarrasin",
"riceFlour": "Farine de riz",
"vegetableStockCube": "Bouillon cube légumes",
"chickenStockCube": "Bouillon cube volaille",
"tomatoPaste": "Concentré de tomate",
"tomatoCoulis": "Coulis de tomate",
"cannedPeeledTomatoes": "Tomates pelées (conserve)",
"sunDriedTomatoes": "Tomates séchées",
"vealStock": "Fond de veau",
"chickenStock": "Fond de volaille",
"beefStockCube": "Bouillon cube bœuf",
"fishStockCube": "Bouillon cube poisson",
"vegetableBroth": "Bouillon de légumes",
"chickenBroth": "Bouillon de volaille",
"beefBroth": "Bouillon de bœuf",
"courtBouillon": "Court-bouillon",
"dashi": "Dashi (bouillon japonais)",
"shellfish_bisque": "Bisque de crustacés",
"tapioca_flour": "Farine de tapioca",
"masa_harina": "Masa harina",
"shellfishBisque": "Bisque de crustacés",
"tapiocaFlour": "Farine de tapioca",
"masaHarina": "Masa harina",
"water": "Eau",
"sparkling_water": "Eau gazeuse",
"orange_blossom_water": "Eau de fleur d'oranger",
"rose_water": "Eau de rose",
"fish_fumet": "Fumet de poisson",
"bakers_yeast": "Levure boulangère",
"baking_powder": "Levure chimique",
"sparklingWater": "Eau gazeuse",
"orangeBlossomWater": "Eau de fleur d'oranger",
"roseWater": "Eau de rose",
"fishFumet": "Fumet de poisson",
"bakersYeast": "Levure boulangère",
"bakingPowder": "Levure chimique",
"cornstarch": "Maïzena",
"lupin_flour": "Farine de lupin",
"lupinFlour": "Farine de lupin",
"gelatin": "Gélatine",
"baking_soda": "Bicarbonate de soude",
"potato_starch": "Fécule de pomme de terre",
"bakingSoda": "Bicarbonate de soude",
"potatoStarch": "Fécule de pomme de terre",
"sugar": "Sucre",
"honey": "Miel",
"maple_syrup": "Sirop d'érable",
"brown_sugar": "Sucre roux",
"powdered_sugar": "Sucre glace",
"demerara_sugar": "Cassonade",
"dark_chocolate": "Chocolat noir",
"milk_chocolate": "Chocolat au lait",
"white_chocolate": "Chocolat blanc",
"chocolate_chips": "Pépites de chocolat",
"cocoa_powder": "Cacao en poudre",
"vanilla_extract": "Extrait de vanille",
"palm_sugar": "Sucre de palme",
"cane_syrup": "Sirop de sucre de canne"
"mapleSyrup": "Sirop d'érable",
"brownSugar": "Sucre roux",
"powderedSugar": "Sucre glace",
"demeraraSugar": "Cassonade",
"darkChocolate": "Chocolat noir",
"milkChocolate": "Chocolat au lait",
"whiteChocolate": "Chocolat blanc",
"chocolateChips": "Pépites de chocolat",
"cocoaPowder": "Cacao en poudre",
"vanillaExtract": "Extrait de vanille",
"palmSugar": "Sucre de palme",
"caneSyrup": "Sirop de sucre de canne"
}
}
}

View file

@ -2,12 +2,13 @@
* A dietary regime, as returned by `GET /reference/diets` reference data
* (`Diet`, seeded via `apps/api/prisma/seed.ts`), not user-specific.
*
* `key` is a stable slug (e.g. `"vegetarien"`), not a display label it
* never changes once seeded, unlike the label it stands in for. Callers
* resolve the label themselves via i18n (`t(\`catalog.diets.${key}\`)`,
* `apps/web`'s `locales/fr/translation.json`), the same way
* `IngredientCategory`/`IngredientSubcategory` enum values already do (see
* `recipes.form.category.*`/`recipes.form.subcategory.*` in that file).
* `key` is a stable, English camelCase uid (e.g. `"vegetarian"`), not a
* display label it never changes once seeded, unlike the label it stands
* in for. Callers resolve the label themselves via i18n
* (`t(\`catalog.diets.${key}\`)`, `apps/web`'s `locales/fr/translation.json`),
* the same way `IngredientCategory`/`IngredientSubcategory` enum values
* already do (see `recipes.form.category.*`/`recipes.form.subcategory.*` in
* that file).
*/
export interface DietView {
id: number;
@ -29,11 +30,12 @@ export type AllergenKind = "ALLERGY" | "INTOLERANCE";
* table itself carries no key of its own (see `schema.prisma`), so this
* flattens that split away: callers just get `{id, key}` and never need to
* know a `Category` exists underneath. Like {@link DietView.key}, it's a
* stable slug (e.g. `"gluten"`), not a display label resolved via
* `t(\`catalog.allergens.${key}\`)`. `kind` groups allergens into two
* separate lists client-side (`AllergySelect`, `apps/web`) rather than one
* flat "allergies & intolérances" list a single `PATCH /profile/allergies`
* call still covers both, this is a display grouping only.
* stable English camelCase uid (e.g. `"gluten"`), not a display label
* resolved via `t(\`catalog.allergens.${key}\`)`. `kind` groups allergens
* into two separate lists client-side (`AllergySelect`, `apps/web`) rather
* than one flat "allergies & intolerances" list a single
* `PATCH /profile/allergies` call still covers both, this is a display
* grouping only.
*/
export interface AllergyView {
id: number;
@ -51,13 +53,13 @@ export interface AllergyView {
* rack within each aisle.
*/
export const INGREDIENT_CATEGORIES = [
"PRODUITS_FRAIS",
"BOUCHERIE_POISSONNERIE",
"EPICERIE_SECHE",
"BOULANGERIE",
"CREMERIE_FROMAGE",
"CONDIMENTS_EPICES",
"AIDES_CULINAIRES",
"freshProduce",
"meatAndSeafood",
"dryGoods",
"bakery",
"dairyAndCheese",
"condimentsAndSpices",
"cookingEssentials",
] as const;
/** Inferred TS type for one {@link INGREDIENT_CATEGORIES} member. */
export type IngredientCategory = (typeof INGREDIENT_CATEGORIES)[number];
@ -69,28 +71,28 @@ export type IngredientCategory = (typeof INGREDIENT_CATEGORIES)[number];
* which category, and in what display order.
*/
export const INGREDIENT_SUBCATEGORIES = [
"LEGUMES",
"FRUITS",
"HERBES_FRAICHES",
"VIANDES",
"VOLAILLES",
"POISSONS",
"CRUSTACES_FRUITS_DE_MER",
"FECULENTS",
"LEGUMINEUSES",
"GRAINES_FRUITS_SECS",
"AUTRES",
"PAINS",
"PATES_A_CUIRE",
"PRODUITS_LAITIERS",
"OEUFS",
"ALTERNATIVES",
"EPICES",
"SAUCES",
"ASSAISONNEMENTS",
"BASES",
"EPAISSISSANTS",
"SUCRES",
"vegetables",
"fruits",
"freshHerbs",
"meats",
"poultry",
"fish",
"shellfish",
"starches",
"legumes",
"nutsAndSeeds",
"other",
"breads",
"rawDough",
"dairy",
"eggs",
"plantBasedAlternatives",
"spices",
"sauces",
"seasonings",
"bases",
"thickeners",
"sugars",
] as const;
/** Inferred TS type for one {@link INGREDIENT_SUBCATEGORIES} member. */
export type IngredientSubcategory = (typeof INGREDIENT_SUBCATEGORIES)[number];
@ -109,13 +111,13 @@ export const INGREDIENT_CATEGORY_SUBCATEGORIES: Record<
IngredientCategory,
readonly IngredientSubcategory[]
> = {
PRODUITS_FRAIS: ["LEGUMES", "FRUITS", "HERBES_FRAICHES"],
BOUCHERIE_POISSONNERIE: ["VIANDES", "VOLAILLES", "POISSONS", "CRUSTACES_FRUITS_DE_MER"],
EPICERIE_SECHE: ["FECULENTS", "LEGUMINEUSES", "GRAINES_FRUITS_SECS", "AUTRES"],
BOULANGERIE: ["PAINS", "PATES_A_CUIRE"],
CREMERIE_FROMAGE: ["PRODUITS_LAITIERS", "OEUFS", "ALTERNATIVES"],
CONDIMENTS_EPICES: ["EPICES", "SAUCES", "ASSAISONNEMENTS"],
AIDES_CULINAIRES: ["BASES", "EPAISSISSANTS", "SUCRES"],
freshProduce: ["vegetables", "fruits", "freshHerbs"],
meatAndSeafood: ["meats", "poultry", "fish", "shellfish"],
dryGoods: ["starches", "legumes", "nutsAndSeeds", "other"],
bakery: ["breads", "rawDough"],
dairyAndCheese: ["dairy", "eggs", "plantBasedAlternatives"],
condimentsAndSpices: ["spices", "sauces", "seasonings"],
cookingEssentials: ["bases", "thickeners", "sugars"],
};
/**
@ -165,17 +167,18 @@ export type IngredientIcon = (typeof INGREDIENT_ICONS)[number];
* `allergens` is resolved server-side from the `IngredientAllergy` join
* table empty for an ingredient that carries none of the 14 EU-regulated
* allergens. `diets` is resolved from `IngredientDiet` the same way the
* regimes this ingredient is compatible with (e.g. `Végétarien`, `gan`),
* regimes this ingredient is compatible with (e.g. `vegetarian`, `vegan`),
* so the picker can flag it without the user opening its packaging. Omits
* `Omnivore` (every ingredient qualifies, so it's never stored) and
* `Sans gluten` (already derivable from whether `allergens` contains
* `Gluten` see `IngredientDiet` in schema.prisma). Used by the recipe
* `omnivore` (every ingredient qualifies, so it's never stored) and
* `glutenFree` (already derivable from whether `allergens` contains
* `gluten` see `IngredientDiet` in schema.prisma). Used by the recipe
* catalog (`apps/web`'s recipe form and detail page) to pick ingredients and
* to surface which allergens/regimes a recipe contains, aggregated across
* its ingredients.
*
* `key` is a stable slug (e.g. `"tomate"`), not a display label like
* {@link DietView.key}, resolved via `t(\`catalog.ingredients.${key}\`)`.
* `key` is a stable English camelCase uid (e.g. `"tomato"`), not a display
* label like {@link DietView.key}, resolved via
* `t(\`catalog.ingredients.${key}\`)`.
*/
export interface IngredientView {
id: number;