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