chore(web): session de polish global — version, checkbox, danger zone, icônes (#20)

* chore(web): session de polish global — version, checkbox, danger zone, icônes

- Affiche le numéro de version (package.json, injecté via Vite) en bas de
  la sidebar, masqué en mode collapse et en mobile.
- Factorise les checkbox/radio dupliqués (AllergySelect, DietTagSelect,
  IngredientPicker, UserPreferencesPage) en composants partagés
  CheckboxOption/RadioOption (components/ui/), et inverse le layout pour
  que la case soit à gauche du label.
- Teinte la "zone de danger" de suppression de compte en rouge (fond +
  bordure), pas seulement le bouton.
- Migre les icônes de navigation générale vers lucide-react (nav-icons.tsx
  devient un fichier de ré-export) ; les pictogrammes d'ingrédients métier
  restent en SVG custom (pas d'équivalents fins côté lucide).

Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle manuelle (sidebar desktop/collapsed/mobile, light/dark).

* feat(web): icônes d'ingrédients depuis foodiconpack.com + page de crédits

- Remplace 19 des 22 pictogrammes génériques d'ingrédients par des icônes
  curées du pack gratuit "Common ingredient icons"/"Common Utensils" de
  foodiconpack.com (CC BY 4.0) : carotte, pomme, basilic, bœuf, poulet,
  saumon, crevette, riz, pois chiches, amandes, lait, cheddar, œufs,
  cannelle, miel, huile d'olive, bière, marmite, sucre.
- BREAD/DOUGH/SPROUT restent en SVG custom : pas d'équivalent net dans le
  pack (packs "ingrédients"/"ustensiles"/"plats"/"boissons" vérifiés).
  Architecture inchangée : `icon` reste un enum de 22 valeurs partagées en
  base (pas de migration, pas de mapping par ingrédient — cf. le
  commentaire du fichier sur l'historique emoji→enum générique).
- Nouveau wrapper FilledIcon (fill="currentColor", viewBox 2048) à côté du
  wrapper Icon existant (stroke) — les deux stylent au même endroit via
  CSS, donc le mélange des 19+3 icônes reste visuellement homogène.
- Ajoute /parametres/credits (CreditsPage) créditant foodiconpack.com et
  liant la licence CC BY 4.0, requis par la licence des icônes utilisées ;
  nouvelle entrée de nav "Crédits" (icône lucide Info).

Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle (grille des 22 icônes dans le picker, page crédits).

* feat(web,api): zone dangereuse rouge, préférences élargies, onglet favoris par défaut, e2e recettes, catalogue en uid+i18n

- Zone dangereuse (compte) : le bouton "Supprimer mon compte" est rouge.
- Pages préférences/paramétrage : contenu centré et élargi (32rem -> 56rem)
  au lieu de coller à gauche sur un écran large.
- Page recettes : l'onglet "Favoris" est sélectionné par défaut.
- Ajout de apps/web/cypress/e2e/recipes.cy.ts (onglets, recherche, sélection
  master-detail, favori, suppression, lien nouvelle recette).
- Catalogue de référence (ingrédients/régimes/allergènes) : la colonne
  `name` (le libellé français, utilisé comme clé unique) devient `key`, un
  slug stable et opaque au sens produit (ex. "vegetarien", "boeuf_hache").
  Le libellé lui-même déménage entièrement côté client, dans
  apps/web/src/locales/fr/translation.json sous le namespace `catalog.*`,
  résolu via `t(\`catalog.ingredients.${key}\`)` etc. — même schéma que
  IngredientCategory/IngredientSubcategory. Migration Prisma
  (rename + backfill des ~456 lignes déjà seedées), seed/service/tests API
  et composants web mis à jour en conséquence.
  - apps/api/src/utils/slugify.ts + scripts/generate-catalog-i18n.ts
    (regénère le fichier de traduction depuis reference-seed-data.ts).
  - 102 tests Mocha + 32 scénarios Cucumber passent contre la base migrée.

Note : cypress run plante dans cet environnement (le processus GPU
Chromium/Electron crash même headless, indépendamment des flags) — les
recipes.cy.ts n'ont pas pu être exécutés ici ; vérifiés par lecture du code
source des composants visés et par un passage manuel dans le navigateur de
prévisualisation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(api): les uids du catalogue sont en anglais, pas des slugs français

reference-seed-data.ts reste rédigé en français (c'est juste le libellé
d'autoring, jamais stocké/exposé), mais la clé stable (`Diet.key`/
`Category.key`/`Ingredient.key`) qu'on en dérive doit elle-même être un
identifiant anglais, indépendant de la langue d'autoring — pas juste le
même texte français passé à slugify().

- apps/api/src/db/catalog-en-keys.ts : dictionnaire écrit à la main
  (label français -> clé anglaise) pour les 5 régimes, 14 allergènes et
  437 ingrédients ; getEnglishKey() lève une erreur explicite si un
  nouvel élément n'a pas encore d'entrée plutôt que de retomber sur un
  slug français silencieux.
- scripts/validate-catalog-en-keys.ts : vérifie que chaque diet/allergène/
  ingrédient de reference-seed-data.ts a une entrée, et que les clés
  anglaises résultantes sont uniques (437/437, 14/14, 5/5 — zéro manquant,
  zéro collision).
- reference-seed-data.ts et scripts/generate-catalog-i18n.ts utilisent
  désormais getEnglishKey() au lieu de slugify(nom français).
- Nouvelle migration (20260818193000_catalog_keys_to_english) qui
  remappe les lignes déjà seedées avec un slug français (par la migration
  précédente) vers leur clé anglaise définitive.
- apps/web/src/locales/fr/translation.json régénéré : catalog.* est
  maintenant indexé par clé anglaise ("vegetarian", "eggs",
  "ground_beef"...), toujours avec le libellé français en valeur.
- Tests/step-definitions mis à jour (getEnglishKey() au lieu de
  slugify()) ; 102 tests Mocha + 32 scénarios Cucumber passent contre la
  base migrée. Vérifié aussi en direct via GET /reference/diets et
  /reference/allergies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(web): crypto.randomUUID plante hors contexte sécurisé, empêchant d'associer un ingrédient

Écran noir + "TypeError: crypto.randomUUID is not a function" au clic sur
une carte d'ingrédient dans le formulaire de recette. crypto.randomUUID()
n'est défini que dans un "contexte sécurisé" (https, ou littéralement le
host "localhost") — il est absent sur une IP locale (test sur un vrai
appareil), dans une WebView Capacitor (l'enrobage mobile prévu pour cette
app), ou en http sur un vrai domaine. RecipeFormPage/StepListEditor s'en
servaient pour générer l'identité React (`key`) de chaque ligne
d'ingrédient/étape en brouillon.

- apps/web/src/lib/client-key.ts : remplace par un générateur qui ne
  touche jamais `crypto` — un compteur + Math.random suffit, cette valeur
  n'a besoin d'être unique que le temps de la session de rendu, jamais
  envoyée au serveur.
- apps/web/cypress/e2e/recipe-form.cy.ts : couvre l'association d'un
  ingrédient (recherche, sélection, exclusion du picker une fois
  sélectionné, retrait), la création et l'édition d'une recette, et un
  test de non-régression dédié qui supprime crypto.randomUUID avant le
  chargement de la page (comme le ferait un vrai contexte non sécurisé)
  pour vérifier que l'ajout de plusieurs ingrédients/étapes ne plante
  plus.

Vérifié en direct dans le navigateur de prévisualisation en supprimant
crypto.randomUUID à la main (reproduit le crash), puis en confirmant que
l'ajout d'ingrédient fonctionne à nouveau après le correctif. cypress run
ne peut toujours pas s'exécuter dans cet environnement (voir le commit
précédent) — non exécutés avec Cypress lui-même, mais vérifiés par
lecture des sélecteurs réels et rejoués à la main dans le navigateur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ci): corrige les specs Cypress cassées par le refactor uid+i18n, applique biome

- onboarding.cy.ts / preferences.cy.ts / recipes.cy.ts mockaient encore
  GET /reference/diets|allergies avec l'ancienne forme {id, name}. Depuis
  les deux derniers commits l'API renvoie {id, key} (uid anglais) et le
  composant résout le libellé via i18n (t(`catalog.diets.${key}`)) — avec
  key manquant, ça affichait littéralement "catalog.diets.undefined" au
  lieu de "Végétarien"/"Omnivore"/etc., faisant échouer cy.select()/
  cy.contains() dans ces 3 specs. Corrigé pour mocker {key: "vegetarian"},
  {key: "peanuts"}, etc.
- recipes.cy.ts : le test "shows a not-found message" utilisait le
  mauvais code d'erreur (4041 au lieu de ErrorCode.RECIPE_NOT_FOUND =
  4045), donc RecipeDetailPanel tombait dans son état d'erreur générique
  au lieu du message "Cette recette n'existe pas." — bug dans mon propre
  test, sans rapport avec le refactor.
- pnpm lint (biome) : les fichiers touchés par le refactor précédent
  avaient quelques soucis de formatage/tri d'imports (des sed multi-
  fichiers, pas d'édition via l'outil habituel) — corrigés par
  `biome check --write`.

Vérifié : ces 3 specs + recipe-form.cy.ts passent maintenant dans le job
CI GitHub Actions (Linux, Cypress s'y exécute réellement — contrairement
à cet environnement Windows sandboxé, voir les commits précédents) ; 102
tests Mocha + 32 scénarios Cucumber toujours au vert en local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
kyuno053 2026-08-18 20:52:12 +02:00 committed by GitHub
parent c38097f522
commit c34eaa89d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 3221 additions and 454 deletions

View file

@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import { Then, When } from "@cucumber/cucumber";
import { getEnglishKey } from "../../src/db/catalog-en-keys.js";
import { prisma } from "../../src/db/prisma.js";
import type { CustomWorld } from "../support/world.js";
@ -11,25 +12,32 @@ function splitNames(names: string): string[] {
.filter(Boolean);
}
/** Resolves allergen names (Category.name) to their Allergy id — see reference.service.ts for why the name lives on Category, not Allergy. */
/**
* Resolves allergen names (as written in a `.feature` file, e.g.
* "Arachides") to their Allergy id scenarios still name allergens by their
* French label for readability, so this slugifies before matching against
* `Category.key` (see reference.service.ts for why the key lives on
* Category, not Allergy).
*/
async function allergyIdsFor(names: string[]): Promise<number[]> {
const allergies = await prisma.allergy.findMany({ include: { category: true } });
return names.map((name) => {
const match = allergies.find((allergy) => allergy.category.name === name);
const key = getEnglishKey(name);
const match = allergies.find((allergy) => allergy.category.key === key);
if (!match) throw new Error(`No seeded allergen named "${name}"`);
return match.id;
});
}
When("I set my regime to {string}", async function (this: CustomWorld, dietName: string) {
const diet = await prisma.diet.findFirstOrThrow({ where: { name: dietName } });
const diet = await prisma.diet.findFirstOrThrow({ where: { key: getEnglishKey(dietName) } });
this.response = await this.agent.patch("/profile/diet").send({ dietId: diet.id });
});
Then(
"my profile's regime should be {string}",
async function (this: CustomWorld, dietName: string) {
const diet = await prisma.diet.findFirstOrThrow({ where: { name: dietName } });
const diet = await prisma.diet.findFirstOrThrow({ where: { key: getEnglishKey(dietName) } });
assert.equal(this.response.body.dietId, diet.id);
},
);

View file

@ -1,12 +1,20 @@
import assert from "node:assert/strict";
import { Given, Then, When } from "@cucumber/cucumber";
import { getEnglishKey } from "../../src/db/catalog-en-keys.js";
import { prisma } from "../../src/db/prisma.js";
import { TEST_REFERENCE_DATE } from "../../test-support/reference-date.js";
import type { CustomWorld } from "../support/world.js";
/** Resolves a reference ingredient by its seeded name — every scenario below names an ingredient by its `reference-seed-data.ts` name, never a raw id. */
/**
* Resolves a reference ingredient by its seeded French name every
* scenario below names an ingredient by its `reference-seed-data.ts` name,
* never a raw id or its slug `key` directly, so this slugifies before
* matching.
*/
async function findIngredientId(name: string): Promise<number> {
const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { name } });
const ingredient = await prisma.ingredient.findFirstOrThrow({
where: { key: getEnglishKey(name) },
});
return ingredient.id;
}
@ -55,10 +63,11 @@ Then(
"the created recipe should have ingredient {string} and step {string}",
function (this: CustomWorld, ingredientName: string, step: string) {
const body = this.response.body as {
ingredients: Array<{ ingredient: { name: string } }>;
ingredients: Array<{ ingredient: { key: string } }>;
steps: Array<{ description: string }>;
};
assert.ok(body.ingredients.some((line) => line.ingredient.name === ingredientName));
const expectedKey = getEnglishKey(ingredientName);
assert.ok(body.ingredients.some((line) => line.ingredient.key === expectedKey));
assert.ok(body.steps.some((s) => s.description === step));
},
);

View file

@ -1,11 +1,20 @@
import assert from "node:assert/strict";
import { Then } from "@cucumber/cucumber";
import { getEnglishKey } from "../../src/db/catalog-en-keys.js";
import type { CustomWorld } from "../support/world.js";
// The feature file still names an item by its French label, for
// readability — the response itself carries only the slug `key` (see
// reference.service.ts), so this slugifies the expected label before
// comparing.
Then(
"the reference list response should include {string}",
function (this: CustomWorld, name: string) {
const names = (this.response.body as Array<{ name: string }>).map((item) => item.name);
assert.ok(names.includes(name), `expected ${JSON.stringify(names)} to include "${name}"`);
const keys = (this.response.body as Array<{ key: string }>).map((item) => item.key);
const expectedKey = getEnglishKey(name);
assert.ok(
keys.includes(expectedKey),
`expected ${JSON.stringify(keys)} to include "${expectedKey}"`,
);
},
);

View file

@ -0,0 +1,480 @@
-- Renames the reference-data label column to a stable slug key on Diet,
-- Category (allergens), and Ingredient — see `utils/slugify.ts` and
-- `scripts/generate-catalog-i18n.ts`. The display label these columns used
-- to hold moves to `apps/web`'s `locales/fr/translation.json` (`catalog.*`
-- namespace) instead.
ALTER TABLE "diet" RENAME COLUMN "name" TO "key";
ALTER INDEX "diet_name_key" RENAME TO "diet_key_key";
ALTER TABLE "category" RENAME COLUMN "name" TO "key";
ALTER INDEX "category_name_key" RENAME TO "category_key_key";
ALTER TABLE "ingredients" RENAME COLUMN "name" TO "key";
ALTER INDEX "ingredients_name_key" RENAME TO "ingredients_key_key";
-- Auto-generated by scripts/generate-catalog-i18n.ts — do not hand-edit.
-- Backfills the "key" column (just renamed from "name" by this migration's
-- preceding statement, so it still holds the old French label) to its slug
-- value, for every row already seeded in a database this migration runs
-- against. A fresh database has none of these rows yet (the seed script
-- inserts by "key" from the start), so this is a no-op there.
UPDATE "diet" SET "key" = 'omnivore' WHERE "key" = 'Omnivore';
UPDATE "diet" SET "key" = 'vegetarien' WHERE "key" = 'Végétarien';
UPDATE "diet" SET "key" = 'vegan' WHERE "key" = 'Végan';
UPDATE "diet" SET "key" = 'pescetarien' WHERE "key" = 'Pescétarien';
UPDATE "diet" SET "key" = 'sans_gluten' WHERE "key" = 'Sans gluten';
UPDATE "category" SET "key" = 'gluten' WHERE "key" = 'Gluten';
UPDATE "category" SET "key" = 'crustaces' WHERE "key" = 'Crustacés';
UPDATE "category" SET "key" = 'oeufs' WHERE "key" = 'Œufs';
UPDATE "category" SET "key" = 'poissons' WHERE "key" = 'Poissons';
UPDATE "category" SET "key" = 'arachides' WHERE "key" = 'Arachides';
UPDATE "category" SET "key" = 'soja' WHERE "key" = 'Soja';
UPDATE "category" SET "key" = 'lait' WHERE "key" = 'Lait';
UPDATE "category" SET "key" = 'fruits_a_coque' WHERE "key" = 'Fruits à coque';
UPDATE "category" SET "key" = 'celeri' WHERE "key" = 'Céleri';
UPDATE "category" SET "key" = 'moutarde' WHERE "key" = 'Moutarde';
UPDATE "category" SET "key" = 'graines_de_sesame' WHERE "key" = 'Graines de sésame';
UPDATE "category" SET "key" = 'sulfites' WHERE "key" = 'Sulfites';
UPDATE "category" SET "key" = 'lupin' WHERE "key" = 'Lupin';
UPDATE "category" SET "key" = 'mollusques' WHERE "key" = 'Mollusques';
UPDATE "ingredients" SET "key" = 'tomate' WHERE "key" = 'Tomate';
UPDATE "ingredients" SET "key" = 'oignon' WHERE "key" = 'Oignon';
UPDATE "ingredients" SET "key" = 'echalote' WHERE "key" = 'Échalote';
UPDATE "ingredients" SET "key" = 'ail' WHERE "key" = 'Ail';
UPDATE "ingredients" SET "key" = 'carotte' WHERE "key" = 'Carotte';
UPDATE "ingredients" SET "key" = 'courgette' WHERE "key" = 'Courgette';
UPDATE "ingredients" SET "key" = 'concombre' WHERE "key" = 'Concombre';
UPDATE "ingredients" SET "key" = 'cornichons' WHERE "key" = 'Cornichons';
UPDATE "ingredients" SET "key" = 'poivron' WHERE "key" = 'Poivron';
UPDATE "ingredients" SET "key" = 'champignon' WHERE "key" = 'Champignon';
UPDATE "ingredients" SET "key" = 'cepes' WHERE "key" = 'Cèpes';
UPDATE "ingredients" SET "key" = 'aubergine' WHERE "key" = 'Aubergine';
UPDATE "ingredients" SET "key" = 'brocoli' WHERE "key" = 'Brocoli';
UPDATE "ingredients" SET "key" = 'chou_fleur' WHERE "key" = 'Chou-fleur';
UPDATE "ingredients" SET "key" = 'chou_blanc' WHERE "key" = 'Chou blanc';
UPDATE "ingredients" SET "key" = 'chou_rouge' WHERE "key" = 'Chou rouge';
UPDATE "ingredients" SET "key" = 'chou_de_bruxelles' WHERE "key" = 'Chou de Bruxelles';
UPDATE "ingredients" SET "key" = 'epinard' WHERE "key" = 'Épinard';
UPDATE "ingredients" SET "key" = 'blette' WHERE "key" = 'Blette';
UPDATE "ingredients" SET "key" = 'salade' WHERE "key" = 'Salade';
UPDATE "ingredients" SET "key" = 'roquette' WHERE "key" = 'Roquette';
UPDATE "ingredients" SET "key" = 'cresson' WHERE "key" = 'Cresson';
UPDATE "ingredients" SET "key" = 'poireau' WHERE "key" = 'Poireau';
UPDATE "ingredients" SET "key" = 'celeri' WHERE "key" = 'Céleri';
UPDATE "ingredients" SET "key" = 'radis' WHERE "key" = 'Radis';
UPDATE "ingredients" SET "key" = 'betterave' WHERE "key" = 'Betterave';
UPDATE "ingredients" SET "key" = 'navet' WHERE "key" = 'Navet';
UPDATE "ingredients" SET "key" = 'panais' WHERE "key" = 'Panais';
UPDATE "ingredients" SET "key" = 'haricot_vert' WHERE "key" = 'Haricot vert';
UPDATE "ingredients" SET "key" = 'petit_pois' WHERE "key" = 'Petit pois';
UPDATE "ingredients" SET "key" = 'mais' WHERE "key" = 'Maïs';
UPDATE "ingredients" SET "key" = 'artichaut' WHERE "key" = 'Artichaut';
UPDATE "ingredients" SET "key" = 'fenouil' WHERE "key" = 'Fenouil';
UPDATE "ingredients" SET "key" = 'endive' WHERE "key" = 'Endive';
UPDATE "ingredients" SET "key" = 'potiron' WHERE "key" = 'Potiron';
UPDATE "ingredients" SET "key" = 'butternut' WHERE "key" = 'Butternut';
UPDATE "ingredients" SET "key" = 'asperge' WHERE "key" = 'Asperge';
UPDATE "ingredients" SET "key" = 'avocat' WHERE "key" = 'Avocat';
UPDATE "ingredients" SET "key" = 'pomme_de_terre' WHERE "key" = 'Pomme de terre';
UPDATE "ingredients" SET "key" = 'patate_douce' WHERE "key" = 'Patate douce';
UPDATE "ingredients" SET "key" = 'tomates_cerises' WHERE "key" = 'Tomates cerises';
UPDATE "ingredients" SET "key" = 'pak_choi' WHERE "key" = 'Pak-choï';
UPDATE "ingredients" SET "key" = 'germes_de_soja' WHERE "key" = 'Germes de soja';
UPDATE "ingredients" SET "key" = 'shiitake' WHERE "key" = 'Shiitake';
UPDATE "ingredients" SET "key" = 'daikon' WHERE "key" = 'Daikon';
UPDATE "ingredients" SET "key" = 'piment_vert_frais' WHERE "key" = 'Piment vert frais';
UPDATE "ingredients" SET "key" = 'citron' WHERE "key" = 'Citron';
UPDATE "ingredients" SET "key" = 'citron_vert' WHERE "key" = 'Citron vert';
UPDATE "ingredients" SET "key" = 'pomme' WHERE "key" = 'Pomme';
UPDATE "ingredients" SET "key" = 'poire' WHERE "key" = 'Poire';
UPDATE "ingredients" SET "key" = 'banane' WHERE "key" = 'Banane';
UPDATE "ingredients" SET "key" = 'orange' WHERE "key" = 'Orange';
UPDATE "ingredients" SET "key" = 'clementine' WHERE "key" = 'Clémentine';
UPDATE "ingredients" SET "key" = 'pamplemousse' WHERE "key" = 'Pamplemousse';
UPDATE "ingredients" SET "key" = 'fraise' WHERE "key" = 'Fraise';
UPDATE "ingredients" SET "key" = 'framboise' WHERE "key" = 'Framboise';
UPDATE "ingredients" SET "key" = 'myrtille' WHERE "key" = 'Myrtille';
UPDATE "ingredients" SET "key" = 'mure' WHERE "key" = 'Mûre';
UPDATE "ingredients" SET "key" = 'cerise' WHERE "key" = 'Cerise';
UPDATE "ingredients" SET "key" = 'abricot' WHERE "key" = 'Abricot';
UPDATE "ingredients" SET "key" = 'peche' WHERE "key" = 'Pêche';
UPDATE "ingredients" SET "key" = 'prune' WHERE "key" = 'Prune';
UPDATE "ingredients" SET "key" = 'raisin' WHERE "key" = 'Raisin';
UPDATE "ingredients" SET "key" = 'melon' WHERE "key" = 'Melon';
UPDATE "ingredients" SET "key" = 'pasteque' WHERE "key" = 'Pastèque';
UPDATE "ingredients" SET "key" = 'ananas' WHERE "key" = 'Ananas';
UPDATE "ingredients" SET "key" = 'mangue' WHERE "key" = 'Mangue';
UPDATE "ingredients" SET "key" = 'kiwi' WHERE "key" = 'Kiwi';
UPDATE "ingredients" SET "key" = 'figue' WHERE "key" = 'Figue';
UPDATE "ingredients" SET "key" = 'datte' WHERE "key" = 'Datte';
UPDATE "ingredients" SET "key" = 'litchi' WHERE "key" = 'Litchi';
UPDATE "ingredients" SET "key" = 'grenade' WHERE "key" = 'Grenade';
UPDATE "ingredients" SET "key" = 'rhubarbe' WHERE "key" = 'Rhubarbe';
UPDATE "ingredients" SET "key" = 'coing' WHERE "key" = 'Coing';
UPDATE "ingredients" SET "key" = 'basilic' WHERE "key" = 'Basilic';
UPDATE "ingredients" SET "key" = 'persil' WHERE "key" = 'Persil';
UPDATE "ingredients" SET "key" = 'thym' WHERE "key" = 'Thym';
UPDATE "ingredients" SET "key" = 'romarin' WHERE "key" = 'Romarin';
UPDATE "ingredients" SET "key" = 'laurier' WHERE "key" = 'Laurier';
UPDATE "ingredients" SET "key" = 'ciboulette' WHERE "key" = 'Ciboulette';
UPDATE "ingredients" SET "key" = 'coriandre_fraiche' WHERE "key" = 'Coriandre fraîche';
UPDATE "ingredients" SET "key" = 'menthe' WHERE "key" = 'Menthe';
UPDATE "ingredients" SET "key" = 'origan' WHERE "key" = 'Origan';
UPDATE "ingredients" SET "key" = 'aneth' WHERE "key" = 'Aneth';
UPDATE "ingredients" SET "key" = 'estragon' WHERE "key" = 'Estragon';
UPDATE "ingredients" SET "key" = 'sarriette' WHERE "key" = 'Sarriette';
UPDATE "ingredients" SET "key" = 'marjolaine' WHERE "key" = 'Marjolaine';
UPDATE "ingredients" SET "key" = 'sauge' WHERE "key" = 'Sauge';
UPDATE "ingredients" SET "key" = 'cerfeuil' WHERE "key" = 'Cerfeuil';
UPDATE "ingredients" SET "key" = 'gingembre' WHERE "key" = 'Gingembre';
UPDATE "ingredients" SET "key" = 'citronnelle' WHERE "key" = 'Citronnelle';
UPDATE "ingredients" SET "key" = 'combava' WHERE "key" = 'Combava';
UPDATE "ingredients" SET "key" = 'lapin' WHERE "key" = 'Lapin';
UPDATE "ingredients" SET "key" = 'boeuf_hache' WHERE "key" = 'Bœuf haché';
UPDATE "ingredients" SET "key" = 'steak_de_boeuf' WHERE "key" = 'Steak de bœuf';
UPDATE "ingredients" SET "key" = 'roti_de_boeuf' WHERE "key" = 'Rôti de bœuf';
UPDATE "ingredients" SET "key" = 'escalope_de_veau' WHERE "key" = 'Escalope de veau';
UPDATE "ingredients" SET "key" = 'filet_mignon_de_porc' WHERE "key" = 'Filet mignon de porc';
UPDATE "ingredients" SET "key" = 'cote_de_porc' WHERE "key" = 'Côte de porc';
UPDATE "ingredients" SET "key" = 'agneau' WHERE "key" = 'Agneau';
UPDATE "ingredients" SET "key" = 'gigot_d_agneau' WHERE "key" = 'Gigot d''agneau';
UPDATE "ingredients" SET "key" = 'lardons' WHERE "key" = 'Lardons';
UPDATE "ingredients" SET "key" = 'bacon' WHERE "key" = 'Bacon';
UPDATE "ingredients" SET "key" = 'jambon_blanc' WHERE "key" = 'Jambon blanc';
UPDATE "ingredients" SET "key" = 'jambon_cru' WHERE "key" = 'Jambon cru';
UPDATE "ingredients" SET "key" = 'saucisse' WHERE "key" = 'Saucisse';
UPDATE "ingredients" SET "key" = 'chorizo' WHERE "key" = 'Chorizo';
UPDATE "ingredients" SET "key" = 'merguez' WHERE "key" = 'Merguez';
UPDATE "ingredients" SET "key" = 'prosciutto' WHERE "key" = 'Prosciutto';
UPDATE "ingredients" SET "key" = 'pancetta' WHERE "key" = 'Pancetta';
UPDATE "ingredients" SET "key" = 'mortadelle' WHERE "key" = 'Mortadelle';
UPDATE "ingredients" SET "key" = 'salami' WHERE "key" = 'Salami';
UPDATE "ingredients" SET "key" = 'poulet' WHERE "key" = 'Poulet';
UPDATE "ingredients" SET "key" = 'dinde' WHERE "key" = 'Dinde';
UPDATE "ingredients" SET "key" = 'canard' WHERE "key" = 'Canard';
UPDATE "ingredients" SET "key" = 'magret_de_canard' WHERE "key" = 'Magret de canard';
UPDATE "ingredients" SET "key" = 'saumon' WHERE "key" = 'Saumon';
UPDATE "ingredients" SET "key" = 'thon' WHERE "key" = 'Thon';
UPDATE "ingredients" SET "key" = 'cabillaud' WHERE "key" = 'Cabillaud';
UPDATE "ingredients" SET "key" = 'truite' WHERE "key" = 'Truite';
UPDATE "ingredients" SET "key" = 'sardine' WHERE "key" = 'Sardine';
UPDATE "ingredients" SET "key" = 'anchois' WHERE "key" = 'Anchois';
UPDATE "ingredients" SET "key" = 'merlan' WHERE "key" = 'Merlan';
UPDATE "ingredients" SET "key" = 'surimi' WHERE "key" = 'Surimi';
UPDATE "ingredients" SET "key" = 'bar_loup_de_mer' WHERE "key" = 'Bar (loup de mer)';
UPDATE "ingredients" SET "key" = 'dorade' WHERE "key" = 'Dorade';
UPDATE "ingredients" SET "key" = 'sole' WHERE "key" = 'Sole';
UPDATE "ingredients" SET "key" = 'turbot' WHERE "key" = 'Turbot';
UPDATE "ingredients" SET "key" = 'merlu' WHERE "key" = 'Merlu';
UPDATE "ingredients" SET "key" = 'colin' WHERE "key" = 'Colin';
UPDATE "ingredients" SET "key" = 'lieu_noir' WHERE "key" = 'Lieu noir';
UPDATE "ingredients" SET "key" = 'eglefin' WHERE "key" = 'Églefin';
UPDATE "ingredients" SET "key" = 'maquereau' WHERE "key" = 'Maquereau';
UPDATE "ingredients" SET "key" = 'hareng' WHERE "key" = 'Hareng';
UPDATE "ingredients" SET "key" = 'rouget' WHERE "key" = 'Rouget';
UPDATE "ingredients" SET "key" = 'raie' WHERE "key" = 'Raie';
UPDATE "ingredients" SET "key" = 'lotte' WHERE "key" = 'Lotte';
UPDATE "ingredients" SET "key" = 'fletan' WHERE "key" = 'Flétan';
UPDATE "ingredients" SET "key" = 'espadon' WHERE "key" = 'Espadon';
UPDATE "ingredients" SET "key" = 'carpe' WHERE "key" = 'Carpe';
UPDATE "ingredients" SET "key" = 'brochet' WHERE "key" = 'Brochet';
UPDATE "ingredients" SET "key" = 'perche' WHERE "key" = 'Perche';
UPDATE "ingredients" SET "key" = 'tilapia' WHERE "key" = 'Tilapia';
UPDATE "ingredients" SET "key" = 'panga' WHERE "key" = 'Panga';
UPDATE "ingredients" SET "key" = 'saumon_fume' WHERE "key" = 'Saumon fumé';
UPDATE "ingredients" SET "key" = 'poisson_seche' WHERE "key" = 'Poisson séché';
UPDATE "ingredients" SET "key" = 'crevettes' WHERE "key" = 'Crevettes';
UPDATE "ingredients" SET "key" = 'langoustines' WHERE "key" = 'Langoustines';
UPDATE "ingredients" SET "key" = 'homard' WHERE "key" = 'Homard';
UPDATE "ingredients" SET "key" = 'crabe' WHERE "key" = 'Crabe';
UPDATE "ingredients" SET "key" = 'langouste' WHERE "key" = 'Langouste';
UPDATE "ingredients" SET "key" = 'moules' WHERE "key" = 'Moules';
UPDATE "ingredients" SET "key" = 'huitres' WHERE "key" = 'Huîtres';
UPDATE "ingredients" SET "key" = 'saint_jacques' WHERE "key" = 'Saint-Jacques';
UPDATE "ingredients" SET "key" = 'calamar' WHERE "key" = 'Calamar';
UPDATE "ingredients" SET "key" = 'poulpe' WHERE "key" = 'Poulpe';
UPDATE "ingredients" SET "key" = 'palourdes' WHERE "key" = 'Palourdes';
UPDATE "ingredients" SET "key" = 'bulots' WHERE "key" = 'Bulots';
UPDATE "ingredients" SET "key" = 'semoule' WHERE "key" = 'Semoule';
UPDATE "ingredients" SET "key" = 'couscous' WHERE "key" = 'Couscous';
UPDATE "ingredients" SET "key" = 'boulgour' WHERE "key" = 'Boulgour';
UPDATE "ingredients" SET "key" = 'polenta' WHERE "key" = 'Polenta';
UPDATE "ingredients" SET "key" = 'quinoa' WHERE "key" = 'Quinoa';
UPDATE "ingredients" SET "key" = 'pates' WHERE "key" = 'Pâtes';
UPDATE "ingredients" SET "key" = 'pates_completes' WHERE "key" = 'Pâtes complètes';
UPDATE "ingredients" SET "key" = 'riz' WHERE "key" = 'Riz';
UPDATE "ingredients" SET "key" = 'riz_basmati' WHERE "key" = 'Riz basmati';
UPDATE "ingredients" SET "key" = 'riz_complet' WHERE "key" = 'Riz complet';
UPDATE "ingredients" SET "key" = 'flocons_d_avoine' WHERE "key" = 'Flocons d''avoine';
UPDATE "ingredients" SET "key" = 'spaghetti' WHERE "key" = 'Spaghetti';
UPDATE "ingredients" SET "key" = 'penne' WHERE "key" = 'Penne';
UPDATE "ingredients" SET "key" = 'tagliatelles' WHERE "key" = 'Tagliatelles';
UPDATE "ingredients" SET "key" = 'lasagnes_feuilles' WHERE "key" = 'Lasagnes (feuilles)';
UPDATE "ingredients" SET "key" = 'gnocchi' WHERE "key" = 'Gnocchi';
UPDATE "ingredients" SET "key" = 'riz_arborio' WHERE "key" = 'Riz arborio';
UPDATE "ingredients" SET "key" = 'nouilles_de_riz' WHERE "key" = 'Nouilles de riz';
UPDATE "ingredients" SET "key" = 'nouilles_udon' WHERE "key" = 'Nouilles udon';
UPDATE "ingredients" SET "key" = 'nouilles_soba' WHERE "key" = 'Nouilles soba';
UPDATE "ingredients" SET "key" = 'nouilles_chinoises' WHERE "key" = 'Nouilles chinoises';
UPDATE "ingredients" SET "key" = 'vermicelles_de_riz' WHERE "key" = 'Vermicelles de riz';
UPDATE "ingredients" SET "key" = 'vermicelles_de_soja' WHERE "key" = 'Vermicelles de soja';
UPDATE "ingredients" SET "key" = 'riz_gluant' WHERE "key" = 'Riz gluant';
UPDATE "ingredients" SET "key" = 'riz_a_sushi' WHERE "key" = 'Riz à sushi';
UPDATE "ingredients" SET "key" = 'riz_jasmin' WHERE "key" = 'Riz jasmin';
UPDATE "ingredients" SET "key" = 'lentilles_vertes' WHERE "key" = 'Lentilles vertes';
UPDATE "ingredients" SET "key" = 'lentilles_corail' WHERE "key" = 'Lentilles corail';
UPDATE "ingredients" SET "key" = 'pois_chiches' WHERE "key" = 'Pois chiches';
UPDATE "ingredients" SET "key" = 'haricots_blancs' WHERE "key" = 'Haricots blancs';
UPDATE "ingredients" SET "key" = 'haricots_rouges' WHERE "key" = 'Haricots rouges';
UPDATE "ingredients" SET "key" = 'haricots_noirs' WHERE "key" = 'Haricots noirs';
UPDATE "ingredients" SET "key" = 'pois_casses' WHERE "key" = 'Pois cassés';
UPDATE "ingredients" SET "key" = 'feves' WHERE "key" = 'Fèves';
UPDATE "ingredients" SET "key" = 'edamame' WHERE "key" = 'Edamame';
UPDATE "ingredients" SET "key" = 'haricots_pinto' WHERE "key" = 'Haricots pinto';
UPDATE "ingredients" SET "key" = 'cacahuetes' WHERE "key" = 'Cacahuètes';
UPDATE "ingredients" SET "key" = 'amandes' WHERE "key" = 'Amandes';
UPDATE "ingredients" SET "key" = 'noix' WHERE "key" = 'Noix';
UPDATE "ingredients" SET "key" = 'noisettes' WHERE "key" = 'Noisettes';
UPDATE "ingredients" SET "key" = 'noix_de_cajou' WHERE "key" = 'Noix de cajou';
UPDATE "ingredients" SET "key" = 'pistaches' WHERE "key" = 'Pistaches';
UPDATE "ingredients" SET "key" = 'noix_de_pecan' WHERE "key" = 'Noix de pécan';
UPDATE "ingredients" SET "key" = 'poudre_d_amande' WHERE "key" = 'Poudre d''amande';
UPDATE "ingredients" SET "key" = 'pignons_de_pin' WHERE "key" = 'Pignons de pin';
UPDATE "ingredients" SET "key" = 'graines_de_tournesol' WHERE "key" = 'Graines de tournesol';
UPDATE "ingredients" SET "key" = 'graines_de_courge' WHERE "key" = 'Graines de courge';
UPDATE "ingredients" SET "key" = 'noix_de_coco_rapee' WHERE "key" = 'Noix de coco râpée';
UPDATE "ingredients" SET "key" = 'raisins_secs' WHERE "key" = 'Raisins secs';
UPDATE "ingredients" SET "key" = 'pruneaux' WHERE "key" = 'Pruneaux';
UPDATE "ingredients" SET "key" = 'abricots_secs' WHERE "key" = 'Abricots secs';
UPDATE "ingredients" SET "key" = 'graines_de_sesame' WHERE "key" = 'Graines de sésame';
UPDATE "ingredients" SET "key" = 'champignons_noirs' WHERE "key" = 'Champignons noirs';
UPDATE "ingredients" SET "key" = 'algue_nori' WHERE "key" = 'Algue nori';
UPDATE "ingredients" SET "key" = 'algue_wakame' WHERE "key" = 'Algue wakamé';
UPDATE "ingredients" SET "key" = 'algue_kombu' WHERE "key" = 'Algue kombu';
UPDATE "ingredients" SET "key" = 'pousses_de_bambou' WHERE "key" = 'Pousses de bambou';
UPDATE "ingredients" SET "key" = 'chataignes_d_eau' WHERE "key" = 'Châtaignes d''eau';
UPDATE "ingredients" SET "key" = 'pain' WHERE "key" = 'Pain';
UPDATE "ingredients" SET "key" = 'pain_de_mie' WHERE "key" = 'Pain de mie';
UPDATE "ingredients" SET "key" = 'pain_complet' WHERE "key" = 'Pain complet';
UPDATE "ingredients" SET "key" = 'baguette' WHERE "key" = 'Baguette';
UPDATE "ingredients" SET "key" = 'pain_de_seigle' WHERE "key" = 'Pain de seigle';
UPDATE "ingredients" SET "key" = 'chapelure' WHERE "key" = 'Chapelure';
UPDATE "ingredients" SET "key" = 'pain_a_burger' WHERE "key" = 'Pain à burger';
UPDATE "ingredients" SET "key" = 'pain_brioche' WHERE "key" = 'Pain brioché';
UPDATE "ingredients" SET "key" = 'pain_a_hot_dog' WHERE "key" = 'Pain à hot-dog';
UPDATE "ingredients" SET "key" = 'pain_pita' WHERE "key" = 'Pain pita';
UPDATE "ingredients" SET "key" = 'pain_bagel' WHERE "key" = 'Pain bagel';
UPDATE "ingredients" SET "key" = 'naan' WHERE "key" = 'Naan';
UPDATE "ingredients" SET "key" = 'pain_wrap' WHERE "key" = 'Pain wrap';
UPDATE "ingredients" SET "key" = 'pain_viennois' WHERE "key" = 'Pain viennois';
UPDATE "ingredients" SET "key" = 'pain_de_campagne' WHERE "key" = 'Pain de campagne';
UPDATE "ingredients" SET "key" = 'pain_aux_cereales' WHERE "key" = 'Pain aux céréales';
UPDATE "ingredients" SET "key" = 'petit_pain' WHERE "key" = 'Petit pain';
UPDATE "ingredients" SET "key" = 'pain_suedois' WHERE "key" = 'Pain suédois';
UPDATE "ingredients" SET "key" = 'pain_sans_gluten' WHERE "key" = 'Pain sans gluten';
UPDATE "ingredients" SET "key" = 'biscotte' WHERE "key" = 'Biscotte';
UPDATE "ingredients" SET "key" = 'croutons' WHERE "key" = 'Croûtons';
UPDATE "ingredients" SET "key" = 'focaccia' WHERE "key" = 'Focaccia';
UPDATE "ingredients" SET "key" = 'ciabatta' WHERE "key" = 'Ciabatta';
UPDATE "ingredients" SET "key" = 'tortilla_de_mais' WHERE "key" = 'Tortilla de maïs';
UPDATE "ingredients" SET "key" = 'tortilla_de_ble' WHERE "key" = 'Tortilla de blé';
UPDATE "ingredients" SET "key" = 'pate_feuilletee' WHERE "key" = 'Pâte feuilletée';
UPDATE "ingredients" SET "key" = 'pate_brisee' WHERE "key" = 'Pâte brisée';
UPDATE "ingredients" SET "key" = 'pate_a_pizza' WHERE "key" = 'Pâte à pizza';
UPDATE "ingredients" SET "key" = 'pate_a_tarte_sablee' WHERE "key" = 'Pâte à tarte sablée';
UPDATE "ingredients" SET "key" = 'lait' WHERE "key" = 'Lait';
UPDATE "ingredients" SET "key" = 'beurre' WHERE "key" = 'Beurre';
UPDATE "ingredients" SET "key" = 'creme_fraiche' WHERE "key" = 'Crème fraîche';
UPDATE "ingredients" SET "key" = 'creme_liquide' WHERE "key" = 'Crème liquide';
UPDATE "ingredients" SET "key" = 'fromage' WHERE "key" = 'Fromage';
UPDATE "ingredients" SET "key" = 'emmental' WHERE "key" = 'Emmental';
UPDATE "ingredients" SET "key" = 'gruyere' WHERE "key" = 'Gruyère';
UPDATE "ingredients" SET "key" = 'parmesan' WHERE "key" = 'Parmesan';
UPDATE "ingredients" SET "key" = 'mozzarella' WHERE "key" = 'Mozzarella';
UPDATE "ingredients" SET "key" = 'chevre_fromage' WHERE "key" = 'Chèvre (fromage)';
UPDATE "ingredients" SET "key" = 'feta' WHERE "key" = 'Feta';
UPDATE "ingredients" SET "key" = 'comte' WHERE "key" = 'Comté';
UPDATE "ingredients" SET "key" = 'fromage_blanc' WHERE "key" = 'Fromage blanc';
UPDATE "ingredients" SET "key" = 'mascarpone' WHERE "key" = 'Mascarpone';
UPDATE "ingredients" SET "key" = 'yaourt' WHERE "key" = 'Yaourt';
UPDATE "ingredients" SET "key" = 'burrata' WHERE "key" = 'Burrata';
UPDATE "ingredients" SET "key" = 'ricotta' WHERE "key" = 'Ricotta';
UPDATE "ingredients" SET "key" = 'pecorino' WHERE "key" = 'Pecorino';
UPDATE "ingredients" SET "key" = 'gorgonzola' WHERE "key" = 'Gorgonzola';
UPDATE "ingredients" SET "key" = 'cheddar' WHERE "key" = 'Cheddar';
UPDATE "ingredients" SET "key" = 'oeuf' WHERE "key" = 'Œuf';
UPDATE "ingredients" SET "key" = 'lait_de_coco' WHERE "key" = 'Lait de coco';
UPDATE "ingredients" SET "key" = 'creme_de_coco' WHERE "key" = 'Crème de coco';
UPDATE "ingredients" SET "key" = 'lait_d_amande' WHERE "key" = 'Lait d''amande';
UPDATE "ingredients" SET "key" = 'lait_d_avoine' WHERE "key" = 'Lait d''avoine';
UPDATE "ingredients" SET "key" = 'tofu' WHERE "key" = 'Tofu';
UPDATE "ingredients" SET "key" = 'tofu_soyeux' WHERE "key" = 'Tofu soyeux';
UPDATE "ingredients" SET "key" = 'herbes_de_provence' WHERE "key" = 'Herbes de Provence';
UPDATE "ingredients" SET "key" = 'poivre_noir' WHERE "key" = 'Poivre noir';
UPDATE "ingredients" SET "key" = 'paprika' WHERE "key" = 'Paprika';
UPDATE "ingredients" SET "key" = 'piment_d_espelette' WHERE "key" = 'Piment d''Espelette';
UPDATE "ingredients" SET "key" = 'piment_de_cayenne' WHERE "key" = 'Piment de Cayenne';
UPDATE "ingredients" SET "key" = 'cumin' WHERE "key" = 'Cumin';
UPDATE "ingredients" SET "key" = 'curry_poudre' WHERE "key" = 'Curry (poudre)';
UPDATE "ingredients" SET "key" = 'curcuma' WHERE "key" = 'Curcuma';
UPDATE "ingredients" SET "key" = 'cannelle' WHERE "key" = 'Cannelle';
UPDATE "ingredients" SET "key" = 'muscade' WHERE "key" = 'Muscade';
UPDATE "ingredients" SET "key" = 'safran' WHERE "key" = 'Safran';
UPDATE "ingredients" SET "key" = 'clou_de_girofle' WHERE "key" = 'Clou de girofle';
UPDATE "ingredients" SET "key" = 'vanille_gousse' WHERE "key" = 'Vanille (gousse)';
UPDATE "ingredients" SET "key" = 'poivre_blanc' WHERE "key" = 'Poivre blanc';
UPDATE "ingredients" SET "key" = 'poivre_rose' WHERE "key" = 'Poivre rose';
UPDATE "ingredients" SET "key" = 'poivre_du_sichuan' WHERE "key" = 'Poivre du Sichuan';
UPDATE "ingredients" SET "key" = 'paprika_fume' WHERE "key" = 'Paprika fumé';
UPDATE "ingredients" SET "key" = 'piment_oiseau' WHERE "key" = 'Piment oiseau';
UPDATE "ingredients" SET "key" = 'baies_de_genievre' WHERE "key" = 'Baies de genièvre';
UPDATE "ingredients" SET "key" = 'anis_etoile_badiane' WHERE "key" = 'Anis étoilé (badiane)';
UPDATE "ingredients" SET "key" = 'anis_vert' WHERE "key" = 'Anis vert';
UPDATE "ingredients" SET "key" = 'graines_de_fenouil' WHERE "key" = 'Graines de fenouil';
UPDATE "ingredients" SET "key" = 'sumac' WHERE "key" = 'Sumac';
UPDATE "ingredients" SET "key" = 'nigelle' WHERE "key" = 'Nigelle';
UPDATE "ingredients" SET "key" = 'quatre_epices' WHERE "key" = 'Quatre épices';
UPDATE "ingredients" SET "key" = 'colombo_poudre' WHERE "key" = 'Colombo (poudre)';
UPDATE "ingredients" SET "key" = 'baharat' WHERE "key" = 'Baharat';
UPDATE "ingredients" SET "key" = 'raifort' WHERE "key" = 'Raifort';
UPDATE "ingredients" SET "key" = 'sel_aux_herbes' WHERE "key" = 'Sel aux herbes';
UPDATE "ingredients" SET "key" = 'sel_de_celeri' WHERE "key" = 'Sel de céleri';
UPDATE "ingredients" SET "key" = 'fleur_de_sel' WHERE "key" = 'Fleur de sel';
UPDATE "ingredients" SET "key" = 'sel' WHERE "key" = 'Sel';
UPDATE "ingredients" SET "key" = 'cinq_epices' WHERE "key" = 'Cinq épices';
UPDATE "ingredients" SET "key" = 'garam_masala' WHERE "key" = 'Garam masala';
UPDATE "ingredients" SET "key" = 'graines_de_coriandre' WHERE "key" = 'Graines de coriandre';
UPDATE "ingredients" SET "key" = 'cardamome' WHERE "key" = 'Cardamome';
UPDATE "ingredients" SET "key" = 'fenugrec' WHERE "key" = 'Fenugrec';
UPDATE "ingredients" SET "key" = 'piment_jalapeno' WHERE "key" = 'Piment jalapeño';
UPDATE "ingredients" SET "key" = 'piment_chipotle' WHERE "key" = 'Piment chipotle';
UPDATE "ingredients" SET "key" = 'piment_poblano' WHERE "key" = 'Piment poblano';
UPDATE "ingredients" SET "key" = 'piment_habanero' WHERE "key" = 'Piment habanero';
UPDATE "ingredients" SET "key" = 'ras_el_hanout' WHERE "key" = 'Ras el hanout';
UPDATE "ingredients" SET "key" = 'za_atar' WHERE "key" = 'Za''atar';
UPDATE "ingredients" SET "key" = 'sauce_soja' WHERE "key" = 'Sauce soja';
UPDATE "ingredients" SET "key" = 'moutarde' WHERE "key" = 'Moutarde';
UPDATE "ingredients" SET "key" = 'mayonnaise' WHERE "key" = 'Mayonnaise';
UPDATE "ingredients" SET "key" = 'ketchup' WHERE "key" = 'Ketchup';
UPDATE "ingredients" SET "key" = 'tabasco' WHERE "key" = 'Tabasco';
UPDATE "ingredients" SET "key" = 'sauce_worcestershire' WHERE "key" = 'Sauce Worcestershire';
UPDATE "ingredients" SET "key" = 'sauce_nuoc_mam' WHERE "key" = 'Sauce nuoc-mâm';
UPDATE "ingredients" SET "key" = 'wasabi' WHERE "key" = 'Wasabi';
UPDATE "ingredients" SET "key" = 'harissa' WHERE "key" = 'Harissa';
UPDATE "ingredients" SET "key" = 'pate_de_curry' WHERE "key" = 'Pâte de curry';
UPDATE "ingredients" SET "key" = 'beurre_de_cacahuete' WHERE "key" = 'Beurre de cacahuète';
UPDATE "ingredients" SET "key" = 'moutarde_de_dijon' WHERE "key" = 'Moutarde de Dijon';
UPDATE "ingredients" SET "key" = 'moutarde_a_l_ancienne' WHERE "key" = 'Moutarde à l''ancienne';
UPDATE "ingredients" SET "key" = 'sauce_barbecue' WHERE "key" = 'Sauce barbecue';
UPDATE "ingredients" SET "key" = 'sauce_tartare' WHERE "key" = 'Sauce tartare';
UPDATE "ingredients" SET "key" = 'sauce_cocktail' WHERE "key" = 'Sauce cocktail';
UPDATE "ingredients" SET "key" = 'sauce_bearnaise' WHERE "key" = 'Sauce béarnaise';
UPDATE "ingredients" SET "key" = 'sauce_hollandaise' WHERE "key" = 'Sauce hollandaise';
UPDATE "ingredients" SET "key" = 'sauce_bechamel' WHERE "key" = 'Sauce béchamel';
UPDATE "ingredients" SET "key" = 'sauce_teriyaki' WHERE "key" = 'Sauce teriyaki';
UPDATE "ingredients" SET "key" = 'sauce_ponzu' WHERE "key" = 'Sauce ponzu';
UPDATE "ingredients" SET "key" = 'chimichurri' WHERE "key" = 'Chimichurri';
UPDATE "ingredients" SET "key" = 'pesto_rouge_tomates_sechees' WHERE "key" = 'Pesto rouge (tomates séchées)';
UPDATE "ingredients" SET "key" = 'pesto' WHERE "key" = 'Pesto';
UPDATE "ingredients" SET "key" = 'sauce_huitre' WHERE "key" = 'Sauce huître';
UPDATE "ingredients" SET "key" = 'sauce_hoisin' WHERE "key" = 'Sauce hoisin';
UPDATE "ingredients" SET "key" = 'sauce_sriracha' WHERE "key" = 'Sauce sriracha';
UPDATE "ingredients" SET "key" = 'sauce_sweet_chili' WHERE "key" = 'Sauce sweet chili';
UPDATE "ingredients" SET "key" = 'miso' WHERE "key" = 'Miso';
UPDATE "ingredients" SET "key" = 'pate_de_crevettes' WHERE "key" = 'Pâte de crevettes';
UPDATE "ingredients" SET "key" = 'pate_de_curry_rouge_thai' WHERE "key" = 'Pâte de curry rouge (thaï)';
UPDATE "ingredients" SET "key" = 'pate_de_curry_vert_thai' WHERE "key" = 'Pâte de curry vert (thaï)';
UPDATE "ingredients" SET "key" = 'tahini' WHERE "key" = 'Tahini';
UPDATE "ingredients" SET "key" = 'huile_d_olive' WHERE "key" = 'Huile d''olive';
UPDATE "ingredients" SET "key" = 'huile_de_tournesol' WHERE "key" = 'Huile de tournesol';
UPDATE "ingredients" SET "key" = 'huile_de_colza' WHERE "key" = 'Huile de colza';
UPDATE "ingredients" SET "key" = 'huile_de_coco' WHERE "key" = 'Huile de coco';
UPDATE "ingredients" SET "key" = 'huile_de_sesame' WHERE "key" = 'Huile de sésame';
UPDATE "ingredients" SET "key" = 'vinaigre_de_cidre' WHERE "key" = 'Vinaigre de cidre';
UPDATE "ingredients" SET "key" = 'vinaigre_blanc' WHERE "key" = 'Vinaigre blanc';
UPDATE "ingredients" SET "key" = 'vinaigre_balsamique' WHERE "key" = 'Vinaigre balsamique';
UPDATE "ingredients" SET "key" = 'capres' WHERE "key" = 'Câpres';
UPDATE "ingredients" SET "key" = 'olives' WHERE "key" = 'Olives';
UPDATE "ingredients" SET "key" = 'vin_blanc_cuisine' WHERE "key" = 'Vin blanc (cuisine)';
UPDATE "ingredients" SET "key" = 'vin_rouge_cuisine' WHERE "key" = 'Vin rouge (cuisine)';
UPDATE "ingredients" SET "key" = 'vinaigre_de_vin_rouge' WHERE "key" = 'Vinaigre de vin rouge';
UPDATE "ingredients" SET "key" = 'vinaigre_de_vin_blanc' WHERE "key" = 'Vinaigre de vin blanc';
UPDATE "ingredients" SET "key" = 'vinaigre_de_xeres' WHERE "key" = 'Vinaigre de xérès';
UPDATE "ingredients" SET "key" = 'huile_de_noix' WHERE "key" = 'Huile de noix';
UPDATE "ingredients" SET "key" = 'huile_de_noisette' WHERE "key" = 'Huile de noisette';
UPDATE "ingredients" SET "key" = 'huile_d_arachide' WHERE "key" = 'Huile d''arachide';
UPDATE "ingredients" SET "key" = 'huile_pimentee' WHERE "key" = 'Huile pimentée';
UPDATE "ingredients" SET "key" = 'vinaigre_de_riz' WHERE "key" = 'Vinaigre de riz';
UPDATE "ingredients" SET "key" = 'mirin' WHERE "key" = 'Mirin';
UPDATE "ingredients" SET "key" = 'sake_cuisine' WHERE "key" = 'Saké (cuisine)';
UPDATE "ingredients" SET "key" = 'jus_de_citron' WHERE "key" = 'Jus de citron';
UPDATE "ingredients" SET "key" = 'jus_de_citron_vert' WHERE "key" = 'Jus de citron vert';
UPDATE "ingredients" SET "key" = 'jus_d_orange' WHERE "key" = 'Jus d''orange';
UPDATE "ingredients" SET "key" = 'jus_de_pomme' WHERE "key" = 'Jus de pomme';
UPDATE "ingredients" SET "key" = 'jus_de_raisin' WHERE "key" = 'Jus de raisin';
UPDATE "ingredients" SET "key" = 'jus_de_tomate' WHERE "key" = 'Jus de tomate';
UPDATE "ingredients" SET "key" = 'jus_de_cranberry' WHERE "key" = 'Jus de cranberry';
UPDATE "ingredients" SET "key" = 'cafe' WHERE "key" = 'Café';
UPDATE "ingredients" SET "key" = 'the' WHERE "key" = 'Thé';
UPDATE "ingredients" SET "key" = 'biere_cuisine' WHERE "key" = 'Bière (cuisine)';
UPDATE "ingredients" SET "key" = 'cidre_cuisine' WHERE "key" = 'Cidre (cuisine)';
UPDATE "ingredients" SET "key" = 'champagne_vin_petillant_cuisine' WHERE "key" = 'Champagne / vin pétillant (cuisine)';
UPDATE "ingredients" SET "key" = 'porto_cuisine' WHERE "key" = 'Porto (cuisine)';
UPDATE "ingredients" SET "key" = 'vin_jaune_cuisine' WHERE "key" = 'Vin jaune (cuisine)';
UPDATE "ingredients" SET "key" = 'cognac' WHERE "key" = 'Cognac';
UPDATE "ingredients" SET "key" = 'rhum' WHERE "key" = 'Rhum';
UPDATE "ingredients" SET "key" = 'whisky' WHERE "key" = 'Whisky';
UPDATE "ingredients" SET "key" = 'vodka' WHERE "key" = 'Vodka';
UPDATE "ingredients" SET "key" = 'farine_de_ble' WHERE "key" = 'Farine de blé';
UPDATE "ingredients" SET "key" = 'farine_complete' WHERE "key" = 'Farine complète';
UPDATE "ingredients" SET "key" = 'farine_de_mais' WHERE "key" = 'Farine de maïs';
UPDATE "ingredients" SET "key" = 'farine_de_sarrasin' WHERE "key" = 'Farine de sarrasin';
UPDATE "ingredients" SET "key" = 'farine_de_riz' WHERE "key" = 'Farine de riz';
UPDATE "ingredients" SET "key" = 'bouillon_cube_legumes' WHERE "key" = 'Bouillon cube légumes';
UPDATE "ingredients" SET "key" = 'bouillon_cube_volaille' WHERE "key" = 'Bouillon cube volaille';
UPDATE "ingredients" SET "key" = 'concentre_de_tomate' WHERE "key" = 'Concentré de tomate';
UPDATE "ingredients" SET "key" = 'coulis_de_tomate' WHERE "key" = 'Coulis de tomate';
UPDATE "ingredients" SET "key" = 'tomates_pelees_conserve' WHERE "key" = 'Tomates pelées (conserve)';
UPDATE "ingredients" SET "key" = 'tomates_sechees' WHERE "key" = 'Tomates séchées';
UPDATE "ingredients" SET "key" = 'fond_de_veau' WHERE "key" = 'Fond de veau';
UPDATE "ingredients" SET "key" = 'fond_de_volaille' WHERE "key" = 'Fond de volaille';
UPDATE "ingredients" SET "key" = 'bouillon_cube_boeuf' WHERE "key" = 'Bouillon cube bœuf';
UPDATE "ingredients" SET "key" = 'bouillon_cube_poisson' WHERE "key" = 'Bouillon cube poisson';
UPDATE "ingredients" SET "key" = 'bouillon_de_legumes' WHERE "key" = 'Bouillon de légumes';
UPDATE "ingredients" SET "key" = 'bouillon_de_volaille' WHERE "key" = 'Bouillon de volaille';
UPDATE "ingredients" SET "key" = 'bouillon_de_boeuf' WHERE "key" = 'Bouillon de bœuf';
UPDATE "ingredients" SET "key" = 'court_bouillon' WHERE "key" = 'Court-bouillon';
UPDATE "ingredients" SET "key" = 'dashi_bouillon_japonais' WHERE "key" = 'Dashi (bouillon japonais)';
UPDATE "ingredients" SET "key" = 'bisque_de_crustaces' WHERE "key" = 'Bisque de crustacés';
UPDATE "ingredients" SET "key" = 'farine_de_tapioca' WHERE "key" = 'Farine de tapioca';
UPDATE "ingredients" SET "key" = 'masa_harina' WHERE "key" = 'Masa harina';
UPDATE "ingredients" SET "key" = 'eau' WHERE "key" = 'Eau';
UPDATE "ingredients" SET "key" = 'eau_gazeuse' WHERE "key" = 'Eau gazeuse';
UPDATE "ingredients" SET "key" = 'eau_de_fleur_d_oranger' WHERE "key" = 'Eau de fleur d''oranger';
UPDATE "ingredients" SET "key" = 'eau_de_rose' WHERE "key" = 'Eau de rose';
UPDATE "ingredients" SET "key" = 'fumet_de_poisson' WHERE "key" = 'Fumet de poisson';
UPDATE "ingredients" SET "key" = 'levure_boulangere' WHERE "key" = 'Levure boulangère';
UPDATE "ingredients" SET "key" = 'levure_chimique' WHERE "key" = 'Levure chimique';
UPDATE "ingredients" SET "key" = 'maizena' WHERE "key" = 'Maïzena';
UPDATE "ingredients" SET "key" = 'farine_de_lupin' WHERE "key" = 'Farine de lupin';
UPDATE "ingredients" SET "key" = 'gelatine' WHERE "key" = 'Gélatine';
UPDATE "ingredients" SET "key" = 'bicarbonate_de_soude' WHERE "key" = 'Bicarbonate de soude';
UPDATE "ingredients" SET "key" = 'fecule_de_pomme_de_terre' WHERE "key" = 'Fécule de pomme de terre';
UPDATE "ingredients" SET "key" = 'sucre' WHERE "key" = 'Sucre';
UPDATE "ingredients" SET "key" = 'miel' WHERE "key" = 'Miel';
UPDATE "ingredients" SET "key" = 'sirop_d_erable' WHERE "key" = 'Sirop d''érable';
UPDATE "ingredients" SET "key" = 'sucre_roux' WHERE "key" = 'Sucre roux';
UPDATE "ingredients" SET "key" = 'sucre_glace' WHERE "key" = 'Sucre glace';
UPDATE "ingredients" SET "key" = 'cassonade' WHERE "key" = 'Cassonade';
UPDATE "ingredients" SET "key" = 'chocolat_noir' WHERE "key" = 'Chocolat noir';
UPDATE "ingredients" SET "key" = 'chocolat_au_lait' WHERE "key" = 'Chocolat au lait';
UPDATE "ingredients" SET "key" = 'chocolat_blanc' WHERE "key" = 'Chocolat blanc';
UPDATE "ingredients" SET "key" = 'pepites_de_chocolat' WHERE "key" = 'Pépites de chocolat';
UPDATE "ingredients" SET "key" = 'cacao_en_poudre' WHERE "key" = 'Cacao en poudre';
UPDATE "ingredients" SET "key" = 'extrait_de_vanille' WHERE "key" = 'Extrait de vanille';
UPDATE "ingredients" SET "key" = 'sucre_de_palme' WHERE "key" = 'Sucre de palme';
UPDATE "ingredients" SET "key" = 'sirop_de_sucre_de_canne' WHERE "key" = 'Sirop de sucre de canne';

View file

@ -0,0 +1,465 @@
-- Auto-generated once by scripts/gen-english-remap-sql.ts — do not re-run,
-- do not hand-edit. Remaps every Diet/Category(allergen)/Ingredient row's
-- "key" from the French slug the previous migration
-- (20260818_catalog_labels_to_keys) produced to the hand-assigned English
-- key in catalog-en-keys.ts.
UPDATE "diet" SET "key" = 'omnivore' WHERE "key" = 'omnivore';
UPDATE "diet" SET "key" = 'vegetarian' WHERE "key" = 'vegetarien';
UPDATE "diet" SET "key" = 'vegan' WHERE "key" = 'vegan';
UPDATE "diet" SET "key" = 'pescatarian' WHERE "key" = 'pescetarien';
UPDATE "diet" SET "key" = 'gluten_free' WHERE "key" = 'sans_gluten';
UPDATE "category" SET "key" = 'gluten' WHERE "key" = 'gluten';
UPDATE "category" SET "key" = 'crustaceans' WHERE "key" = 'crustaces';
UPDATE "category" SET "key" = 'eggs' WHERE "key" = 'oeufs';
UPDATE "category" SET "key" = 'fish' WHERE "key" = 'poissons';
UPDATE "category" SET "key" = 'peanuts' WHERE "key" = 'arachides';
UPDATE "category" SET "key" = 'soy' WHERE "key" = 'soja';
UPDATE "category" SET "key" = 'milk' WHERE "key" = 'lait';
UPDATE "category" SET "key" = 'tree_nuts' WHERE "key" = 'fruits_a_coque';
UPDATE "category" SET "key" = 'celery' WHERE "key" = 'celeri';
UPDATE "category" SET "key" = 'mustard' WHERE "key" = 'moutarde';
UPDATE "category" SET "key" = 'sesame_seeds' WHERE "key" = 'graines_de_sesame';
UPDATE "category" SET "key" = 'sulfites' WHERE "key" = 'sulfites';
UPDATE "category" SET "key" = 'lupin' WHERE "key" = 'lupin';
UPDATE "category" SET "key" = 'molluscs' WHERE "key" = 'mollusques';
UPDATE "ingredients" SET "key" = 'tomato' WHERE "key" = 'tomate';
UPDATE "ingredients" SET "key" = 'onion' WHERE "key" = 'oignon';
UPDATE "ingredients" SET "key" = 'shallot' WHERE "key" = 'echalote';
UPDATE "ingredients" SET "key" = 'garlic' WHERE "key" = 'ail';
UPDATE "ingredients" SET "key" = 'carrot' WHERE "key" = 'carotte';
UPDATE "ingredients" SET "key" = 'zucchini' WHERE "key" = 'courgette';
UPDATE "ingredients" SET "key" = 'cucumber' WHERE "key" = 'concombre';
UPDATE "ingredients" SET "key" = 'gherkins' WHERE "key" = 'cornichons';
UPDATE "ingredients" SET "key" = 'bell_pepper' WHERE "key" = 'poivron';
UPDATE "ingredients" SET "key" = 'mushroom' WHERE "key" = 'champignon';
UPDATE "ingredients" SET "key" = 'porcini' WHERE "key" = 'cepes';
UPDATE "ingredients" SET "key" = 'eggplant' WHERE "key" = 'aubergine';
UPDATE "ingredients" SET "key" = 'broccoli' WHERE "key" = 'brocoli';
UPDATE "ingredients" SET "key" = 'cauliflower' WHERE "key" = 'chou_fleur';
UPDATE "ingredients" SET "key" = 'white_cabbage' WHERE "key" = 'chou_blanc';
UPDATE "ingredients" SET "key" = 'red_cabbage' WHERE "key" = 'chou_rouge';
UPDATE "ingredients" SET "key" = 'brussels_sprouts' WHERE "key" = 'chou_de_bruxelles';
UPDATE "ingredients" SET "key" = 'spinach' WHERE "key" = 'epinard';
UPDATE "ingredients" SET "key" = 'swiss_chard' WHERE "key" = 'blette';
UPDATE "ingredients" SET "key" = 'lettuce' WHERE "key" = 'salade';
UPDATE "ingredients" SET "key" = 'arugula' WHERE "key" = 'roquette';
UPDATE "ingredients" SET "key" = 'watercress' WHERE "key" = 'cresson';
UPDATE "ingredients" SET "key" = 'leek' WHERE "key" = 'poireau';
UPDATE "ingredients" SET "key" = 'celery' WHERE "key" = 'celeri';
UPDATE "ingredients" SET "key" = 'radish' WHERE "key" = 'radis';
UPDATE "ingredients" SET "key" = 'beetroot' WHERE "key" = 'betterave';
UPDATE "ingredients" SET "key" = 'turnip' WHERE "key" = 'navet';
UPDATE "ingredients" SET "key" = 'parsnip' WHERE "key" = 'panais';
UPDATE "ingredients" SET "key" = 'green_bean' WHERE "key" = 'haricot_vert';
UPDATE "ingredients" SET "key" = 'pea' WHERE "key" = 'petit_pois';
UPDATE "ingredients" SET "key" = 'corn' WHERE "key" = 'mais';
UPDATE "ingredients" SET "key" = 'artichoke' WHERE "key" = 'artichaut';
UPDATE "ingredients" SET "key" = 'fennel' WHERE "key" = 'fenouil';
UPDATE "ingredients" SET "key" = 'endive' WHERE "key" = 'endive';
UPDATE "ingredients" SET "key" = 'pumpkin' WHERE "key" = 'potiron';
UPDATE "ingredients" SET "key" = 'butternut_squash' WHERE "key" = 'butternut';
UPDATE "ingredients" SET "key" = 'asparagus' WHERE "key" = 'asperge';
UPDATE "ingredients" SET "key" = 'avocado' WHERE "key" = 'avocat';
UPDATE "ingredients" SET "key" = 'potato' WHERE "key" = 'pomme_de_terre';
UPDATE "ingredients" SET "key" = 'sweet_potato' WHERE "key" = 'patate_douce';
UPDATE "ingredients" SET "key" = 'cherry_tomato' WHERE "key" = 'tomates_cerises';
UPDATE "ingredients" SET "key" = 'bok_choy' WHERE "key" = 'pak_choi';
UPDATE "ingredients" SET "key" = 'soybean_sprouts' WHERE "key" = 'germes_de_soja';
UPDATE "ingredients" SET "key" = 'shiitake' WHERE "key" = 'shiitake';
UPDATE "ingredients" SET "key" = 'daikon' WHERE "key" = 'daikon';
UPDATE "ingredients" SET "key" = 'fresh_green_chili' WHERE "key" = 'piment_vert_frais';
UPDATE "ingredients" SET "key" = 'lemon' WHERE "key" = 'citron';
UPDATE "ingredients" SET "key" = 'lime' WHERE "key" = 'citron_vert';
UPDATE "ingredients" SET "key" = 'apple' WHERE "key" = 'pomme';
UPDATE "ingredients" SET "key" = 'pear' WHERE "key" = 'poire';
UPDATE "ingredients" SET "key" = 'banana' WHERE "key" = 'banane';
UPDATE "ingredients" SET "key" = 'orange' WHERE "key" = 'orange';
UPDATE "ingredients" SET "key" = 'clementine' WHERE "key" = 'clementine';
UPDATE "ingredients" SET "key" = 'grapefruit' WHERE "key" = 'pamplemousse';
UPDATE "ingredients" SET "key" = 'strawberry' WHERE "key" = 'fraise';
UPDATE "ingredients" SET "key" = 'raspberry' WHERE "key" = 'framboise';
UPDATE "ingredients" SET "key" = 'blueberry' WHERE "key" = 'myrtille';
UPDATE "ingredients" SET "key" = 'blackberry' WHERE "key" = 'mure';
UPDATE "ingredients" SET "key" = 'cherry' WHERE "key" = 'cerise';
UPDATE "ingredients" SET "key" = 'apricot' WHERE "key" = 'abricot';
UPDATE "ingredients" SET "key" = 'peach' WHERE "key" = 'peche';
UPDATE "ingredients" SET "key" = 'plum' WHERE "key" = 'prune';
UPDATE "ingredients" SET "key" = 'grape' WHERE "key" = 'raisin';
UPDATE "ingredients" SET "key" = 'melon' WHERE "key" = 'melon';
UPDATE "ingredients" SET "key" = 'watermelon' WHERE "key" = 'pasteque';
UPDATE "ingredients" SET "key" = 'pineapple' WHERE "key" = 'ananas';
UPDATE "ingredients" SET "key" = 'mango' WHERE "key" = 'mangue';
UPDATE "ingredients" SET "key" = 'kiwi' WHERE "key" = 'kiwi';
UPDATE "ingredients" SET "key" = 'fig' WHERE "key" = 'figue';
UPDATE "ingredients" SET "key" = 'date' WHERE "key" = 'datte';
UPDATE "ingredients" SET "key" = 'lychee' WHERE "key" = 'litchi';
UPDATE "ingredients" SET "key" = 'pomegranate' WHERE "key" = 'grenade';
UPDATE "ingredients" SET "key" = 'rhubarb' WHERE "key" = 'rhubarbe';
UPDATE "ingredients" SET "key" = 'quince' WHERE "key" = 'coing';
UPDATE "ingredients" SET "key" = 'basil' WHERE "key" = 'basilic';
UPDATE "ingredients" SET "key" = 'parsley' WHERE "key" = 'persil';
UPDATE "ingredients" SET "key" = 'thyme' WHERE "key" = 'thym';
UPDATE "ingredients" SET "key" = 'rosemary' WHERE "key" = 'romarin';
UPDATE "ingredients" SET "key" = 'bay_leaf' WHERE "key" = 'laurier';
UPDATE "ingredients" SET "key" = 'chives' WHERE "key" = 'ciboulette';
UPDATE "ingredients" SET "key" = 'fresh_cilantro' WHERE "key" = 'coriandre_fraiche';
UPDATE "ingredients" SET "key" = 'mint' WHERE "key" = 'menthe';
UPDATE "ingredients" SET "key" = 'oregano' WHERE "key" = 'origan';
UPDATE "ingredients" SET "key" = 'dill' WHERE "key" = 'aneth';
UPDATE "ingredients" SET "key" = 'tarragon' WHERE "key" = 'estragon';
UPDATE "ingredients" SET "key" = 'savory' WHERE "key" = 'sarriette';
UPDATE "ingredients" SET "key" = 'marjoram' WHERE "key" = 'marjolaine';
UPDATE "ingredients" SET "key" = 'sage' WHERE "key" = 'sauge';
UPDATE "ingredients" SET "key" = 'chervil' WHERE "key" = 'cerfeuil';
UPDATE "ingredients" SET "key" = 'ginger' WHERE "key" = 'gingembre';
UPDATE "ingredients" SET "key" = 'lemongrass' WHERE "key" = 'citronnelle';
UPDATE "ingredients" SET "key" = 'kaffir_lime' WHERE "key" = 'combava';
UPDATE "ingredients" SET "key" = 'rabbit' WHERE "key" = 'lapin';
UPDATE "ingredients" SET "key" = 'ground_beef' WHERE "key" = 'boeuf_hache';
UPDATE "ingredients" SET "key" = 'beef_steak' WHERE "key" = 'steak_de_boeuf';
UPDATE "ingredients" SET "key" = 'beef_roast' WHERE "key" = 'roti_de_boeuf';
UPDATE "ingredients" SET "key" = 'veal_cutlet' WHERE "key" = 'escalope_de_veau';
UPDATE "ingredients" SET "key" = 'pork_tenderloin' WHERE "key" = 'filet_mignon_de_porc';
UPDATE "ingredients" SET "key" = 'pork_chop' WHERE "key" = 'cote_de_porc';
UPDATE "ingredients" SET "key" = 'lamb' WHERE "key" = 'agneau';
UPDATE "ingredients" SET "key" = 'leg_of_lamb' WHERE "key" = 'gigot_d_agneau';
UPDATE "ingredients" SET "key" = 'bacon_lardons' WHERE "key" = 'lardons';
UPDATE "ingredients" SET "key" = 'bacon' WHERE "key" = 'bacon';
UPDATE "ingredients" SET "key" = 'ham' WHERE "key" = 'jambon_blanc';
UPDATE "ingredients" SET "key" = 'cured_ham' WHERE "key" = 'jambon_cru';
UPDATE "ingredients" SET "key" = 'sausage' WHERE "key" = 'saucisse';
UPDATE "ingredients" SET "key" = 'chorizo' WHERE "key" = 'chorizo';
UPDATE "ingredients" SET "key" = 'merguez' WHERE "key" = 'merguez';
UPDATE "ingredients" SET "key" = 'prosciutto' WHERE "key" = 'prosciutto';
UPDATE "ingredients" SET "key" = 'pancetta' WHERE "key" = 'pancetta';
UPDATE "ingredients" SET "key" = 'mortadella' WHERE "key" = 'mortadelle';
UPDATE "ingredients" SET "key" = 'salami' WHERE "key" = 'salami';
UPDATE "ingredients" SET "key" = 'chicken' WHERE "key" = 'poulet';
UPDATE "ingredients" SET "key" = 'turkey' WHERE "key" = 'dinde';
UPDATE "ingredients" SET "key" = 'duck' WHERE "key" = 'canard';
UPDATE "ingredients" SET "key" = 'duck_breast' WHERE "key" = 'magret_de_canard';
UPDATE "ingredients" SET "key" = 'salmon' WHERE "key" = 'saumon';
UPDATE "ingredients" SET "key" = 'tuna' WHERE "key" = 'thon';
UPDATE "ingredients" SET "key" = 'cod' WHERE "key" = 'cabillaud';
UPDATE "ingredients" SET "key" = 'trout' WHERE "key" = 'truite';
UPDATE "ingredients" SET "key" = 'sardine' WHERE "key" = 'sardine';
UPDATE "ingredients" SET "key" = 'anchovy' WHERE "key" = 'anchois';
UPDATE "ingredients" SET "key" = 'whiting' WHERE "key" = 'merlan';
UPDATE "ingredients" SET "key" = 'surimi' WHERE "key" = 'surimi';
UPDATE "ingredients" SET "key" = 'sea_bass' WHERE "key" = 'bar_loup_de_mer';
UPDATE "ingredients" SET "key" = 'sea_bream' WHERE "key" = 'dorade';
UPDATE "ingredients" SET "key" = 'sole' WHERE "key" = 'sole';
UPDATE "ingredients" SET "key" = 'turbot' WHERE "key" = 'turbot';
UPDATE "ingredients" SET "key" = 'hake' WHERE "key" = 'merlu';
UPDATE "ingredients" SET "key" = 'pollock' WHERE "key" = 'colin';
UPDATE "ingredients" SET "key" = 'saithe' WHERE "key" = 'lieu_noir';
UPDATE "ingredients" SET "key" = 'haddock' WHERE "key" = 'eglefin';
UPDATE "ingredients" SET "key" = 'mackerel' WHERE "key" = 'maquereau';
UPDATE "ingredients" SET "key" = 'herring' WHERE "key" = 'hareng';
UPDATE "ingredients" SET "key" = 'red_mullet' WHERE "key" = 'rouget';
UPDATE "ingredients" SET "key" = 'skate' WHERE "key" = 'raie';
UPDATE "ingredients" SET "key" = 'monkfish' WHERE "key" = 'lotte';
UPDATE "ingredients" SET "key" = 'halibut' WHERE "key" = 'fletan';
UPDATE "ingredients" SET "key" = 'swordfish' WHERE "key" = 'espadon';
UPDATE "ingredients" SET "key" = 'carp' WHERE "key" = 'carpe';
UPDATE "ingredients" SET "key" = 'pike' WHERE "key" = 'brochet';
UPDATE "ingredients" SET "key" = 'perch' WHERE "key" = 'perche';
UPDATE "ingredients" SET "key" = 'tilapia' WHERE "key" = 'tilapia';
UPDATE "ingredients" SET "key" = 'pangasius' WHERE "key" = 'panga';
UPDATE "ingredients" SET "key" = 'smoked_salmon' WHERE "key" = 'saumon_fume';
UPDATE "ingredients" SET "key" = 'dried_fish' WHERE "key" = 'poisson_seche';
UPDATE "ingredients" SET "key" = 'shrimp' WHERE "key" = 'crevettes';
UPDATE "ingredients" SET "key" = 'langoustine' WHERE "key" = 'langoustines';
UPDATE "ingredients" SET "key" = 'lobster' WHERE "key" = 'homard';
UPDATE "ingredients" SET "key" = 'crab' WHERE "key" = 'crabe';
UPDATE "ingredients" SET "key" = 'spiny_lobster' WHERE "key" = 'langouste';
UPDATE "ingredients" SET "key" = 'mussels' WHERE "key" = 'moules';
UPDATE "ingredients" SET "key" = 'oysters' WHERE "key" = 'huitres';
UPDATE "ingredients" SET "key" = 'scallops' WHERE "key" = 'saint_jacques';
UPDATE "ingredients" SET "key" = 'squid' WHERE "key" = 'calamar';
UPDATE "ingredients" SET "key" = 'octopus' WHERE "key" = 'poulpe';
UPDATE "ingredients" SET "key" = 'clams' WHERE "key" = 'palourdes';
UPDATE "ingredients" SET "key" = 'whelks' WHERE "key" = 'bulots';
UPDATE "ingredients" SET "key" = 'semolina' WHERE "key" = 'semoule';
UPDATE "ingredients" SET "key" = 'couscous' WHERE "key" = 'couscous';
UPDATE "ingredients" SET "key" = 'bulgur' WHERE "key" = 'boulgour';
UPDATE "ingredients" SET "key" = 'polenta' WHERE "key" = 'polenta';
UPDATE "ingredients" SET "key" = 'quinoa' WHERE "key" = 'quinoa';
UPDATE "ingredients" SET "key" = 'pasta' WHERE "key" = 'pates';
UPDATE "ingredients" SET "key" = 'whole_wheat_pasta' WHERE "key" = 'pates_completes';
UPDATE "ingredients" SET "key" = 'rice' WHERE "key" = 'riz';
UPDATE "ingredients" SET "key" = 'basmati_rice' WHERE "key" = 'riz_basmati';
UPDATE "ingredients" SET "key" = 'brown_rice' WHERE "key" = 'riz_complet';
UPDATE "ingredients" SET "key" = 'oats' WHERE "key" = 'flocons_d_avoine';
UPDATE "ingredients" SET "key" = 'spaghetti' WHERE "key" = 'spaghetti';
UPDATE "ingredients" SET "key" = 'penne' WHERE "key" = 'penne';
UPDATE "ingredients" SET "key" = 'tagliatelle' WHERE "key" = 'tagliatelles';
UPDATE "ingredients" SET "key" = 'lasagna_sheets' WHERE "key" = 'lasagnes_feuilles';
UPDATE "ingredients" SET "key" = 'gnocchi' WHERE "key" = 'gnocchi';
UPDATE "ingredients" SET "key" = 'arborio_rice' WHERE "key" = 'riz_arborio';
UPDATE "ingredients" SET "key" = 'rice_noodles' WHERE "key" = 'nouilles_de_riz';
UPDATE "ingredients" SET "key" = 'udon_noodles' WHERE "key" = 'nouilles_udon';
UPDATE "ingredients" SET "key" = 'soba_noodles' WHERE "key" = 'nouilles_soba';
UPDATE "ingredients" SET "key" = 'chinese_noodles' WHERE "key" = 'nouilles_chinoises';
UPDATE "ingredients" SET "key" = 'rice_vermicelli' WHERE "key" = 'vermicelles_de_riz';
UPDATE "ingredients" SET "key" = 'soy_vermicelli' WHERE "key" = 'vermicelles_de_soja';
UPDATE "ingredients" SET "key" = 'sticky_rice' WHERE "key" = 'riz_gluant';
UPDATE "ingredients" SET "key" = 'sushi_rice' WHERE "key" = 'riz_a_sushi';
UPDATE "ingredients" SET "key" = 'jasmine_rice' WHERE "key" = 'riz_jasmin';
UPDATE "ingredients" SET "key" = 'green_lentils' WHERE "key" = 'lentilles_vertes';
UPDATE "ingredients" SET "key" = 'red_lentils' WHERE "key" = 'lentilles_corail';
UPDATE "ingredients" SET "key" = 'chickpeas' WHERE "key" = 'pois_chiches';
UPDATE "ingredients" SET "key" = 'white_beans' WHERE "key" = 'haricots_blancs';
UPDATE "ingredients" SET "key" = 'kidney_beans' WHERE "key" = 'haricots_rouges';
UPDATE "ingredients" SET "key" = 'black_beans' WHERE "key" = 'haricots_noirs';
UPDATE "ingredients" SET "key" = 'split_peas' WHERE "key" = 'pois_casses';
UPDATE "ingredients" SET "key" = 'fava_beans' WHERE "key" = 'feves';
UPDATE "ingredients" SET "key" = 'edamame' WHERE "key" = 'edamame';
UPDATE "ingredients" SET "key" = 'pinto_beans' WHERE "key" = 'haricots_pinto';
UPDATE "ingredients" SET "key" = 'peanuts_shelled' WHERE "key" = 'cacahuetes';
UPDATE "ingredients" SET "key" = 'almonds' WHERE "key" = 'amandes';
UPDATE "ingredients" SET "key" = 'walnuts' WHERE "key" = 'noix';
UPDATE "ingredients" SET "key" = 'hazelnuts' WHERE "key" = 'noisettes';
UPDATE "ingredients" SET "key" = 'cashews' WHERE "key" = 'noix_de_cajou';
UPDATE "ingredients" SET "key" = 'pistachios' WHERE "key" = 'pistaches';
UPDATE "ingredients" SET "key" = 'pecans' WHERE "key" = 'noix_de_pecan';
UPDATE "ingredients" SET "key" = 'almond_powder' WHERE "key" = 'poudre_d_amande';
UPDATE "ingredients" SET "key" = 'pine_nuts' WHERE "key" = 'pignons_de_pin';
UPDATE "ingredients" SET "key" = 'sunflower_seeds' WHERE "key" = 'graines_de_tournesol';
UPDATE "ingredients" SET "key" = 'pumpkin_seeds' WHERE "key" = 'graines_de_courge';
UPDATE "ingredients" SET "key" = 'shredded_coconut' WHERE "key" = 'noix_de_coco_rapee';
UPDATE "ingredients" SET "key" = 'raisins' WHERE "key" = 'raisins_secs';
UPDATE "ingredients" SET "key" = 'prunes' WHERE "key" = 'pruneaux';
UPDATE "ingredients" SET "key" = 'dried_apricots' WHERE "key" = 'abricots_secs';
UPDATE "ingredients" SET "key" = 'sesame_seeds' WHERE "key" = 'graines_de_sesame';
UPDATE "ingredients" SET "key" = 'black_mushrooms' WHERE "key" = 'champignons_noirs';
UPDATE "ingredients" SET "key" = 'nori_seaweed' WHERE "key" = 'algue_nori';
UPDATE "ingredients" SET "key" = 'wakame_seaweed' WHERE "key" = 'algue_wakame';
UPDATE "ingredients" SET "key" = 'kombu_seaweed' WHERE "key" = 'algue_kombu';
UPDATE "ingredients" SET "key" = 'bamboo_shoots' WHERE "key" = 'pousses_de_bambou';
UPDATE "ingredients" SET "key" = 'water_chestnuts' WHERE "key" = 'chataignes_d_eau';
UPDATE "ingredients" SET "key" = 'bread' WHERE "key" = 'pain';
UPDATE "ingredients" SET "key" = 'sandwich_bread' WHERE "key" = 'pain_de_mie';
UPDATE "ingredients" SET "key" = 'whole_wheat_bread' WHERE "key" = 'pain_complet';
UPDATE "ingredients" SET "key" = 'baguette' WHERE "key" = 'baguette';
UPDATE "ingredients" SET "key" = 'rye_bread' WHERE "key" = 'pain_de_seigle';
UPDATE "ingredients" SET "key" = 'breadcrumbs' WHERE "key" = 'chapelure';
UPDATE "ingredients" SET "key" = 'burger_bun' WHERE "key" = 'pain_a_burger';
UPDATE "ingredients" SET "key" = 'brioche_bun' WHERE "key" = 'pain_brioche';
UPDATE "ingredients" SET "key" = 'hot_dog_bun' WHERE "key" = 'pain_a_hot_dog';
UPDATE "ingredients" SET "key" = 'pita_bread' WHERE "key" = 'pain_pita';
UPDATE "ingredients" SET "key" = 'bagel' WHERE "key" = 'pain_bagel';
UPDATE "ingredients" SET "key" = 'naan' WHERE "key" = 'naan';
UPDATE "ingredients" SET "key" = 'wrap_bread' WHERE "key" = 'pain_wrap';
UPDATE "ingredients" SET "key" = 'viennese_bread' WHERE "key" = 'pain_viennois';
UPDATE "ingredients" SET "key" = 'country_bread' WHERE "key" = 'pain_de_campagne';
UPDATE "ingredients" SET "key" = 'multigrain_bread' WHERE "key" = 'pain_aux_cereales';
UPDATE "ingredients" SET "key" = 'bread_roll' WHERE "key" = 'petit_pain';
UPDATE "ingredients" SET "key" = 'swedish_bread' WHERE "key" = 'pain_suedois';
UPDATE "ingredients" SET "key" = 'gluten_free_bread' WHERE "key" = 'pain_sans_gluten';
UPDATE "ingredients" SET "key" = 'rusk' WHERE "key" = 'biscotte';
UPDATE "ingredients" SET "key" = 'croutons' WHERE "key" = 'croutons';
UPDATE "ingredients" SET "key" = 'focaccia' WHERE "key" = 'focaccia';
UPDATE "ingredients" SET "key" = 'ciabatta' WHERE "key" = 'ciabatta';
UPDATE "ingredients" SET "key" = 'corn_tortilla' WHERE "key" = 'tortilla_de_mais';
UPDATE "ingredients" SET "key" = 'wheat_tortilla' WHERE "key" = 'tortilla_de_ble';
UPDATE "ingredients" SET "key" = 'puff_pastry' WHERE "key" = 'pate_feuilletee';
UPDATE "ingredients" SET "key" = 'shortcrust_pastry' WHERE "key" = 'pate_brisee';
UPDATE "ingredients" SET "key" = 'pizza_dough' WHERE "key" = 'pate_a_pizza';
UPDATE "ingredients" SET "key" = 'sweet_shortcrust_pastry' WHERE "key" = 'pate_a_tarte_sablee';
UPDATE "ingredients" SET "key" = 'milk' WHERE "key" = 'lait';
UPDATE "ingredients" SET "key" = 'butter' WHERE "key" = 'beurre';
UPDATE "ingredients" SET "key" = 'creme_fraiche' WHERE "key" = 'creme_fraiche';
UPDATE "ingredients" SET "key" = 'liquid_cream' WHERE "key" = 'creme_liquide';
UPDATE "ingredients" SET "key" = 'cheese' WHERE "key" = 'fromage';
UPDATE "ingredients" SET "key" = 'emmental' WHERE "key" = 'emmental';
UPDATE "ingredients" SET "key" = 'gruyere' WHERE "key" = 'gruyere';
UPDATE "ingredients" SET "key" = 'parmesan' WHERE "key" = 'parmesan';
UPDATE "ingredients" SET "key" = 'mozzarella' WHERE "key" = 'mozzarella';
UPDATE "ingredients" SET "key" = 'goat_cheese' WHERE "key" = 'chevre_fromage';
UPDATE "ingredients" SET "key" = 'feta' WHERE "key" = 'feta';
UPDATE "ingredients" SET "key" = 'comte' WHERE "key" = 'comte';
UPDATE "ingredients" SET "key" = 'fromage_blanc' WHERE "key" = 'fromage_blanc';
UPDATE "ingredients" SET "key" = 'mascarpone' WHERE "key" = 'mascarpone';
UPDATE "ingredients" SET "key" = 'yogurt' WHERE "key" = 'yaourt';
UPDATE "ingredients" SET "key" = 'burrata' WHERE "key" = 'burrata';
UPDATE "ingredients" SET "key" = 'ricotta' WHERE "key" = 'ricotta';
UPDATE "ingredients" SET "key" = 'pecorino' WHERE "key" = 'pecorino';
UPDATE "ingredients" SET "key" = 'gorgonzola' WHERE "key" = 'gorgonzola';
UPDATE "ingredients" SET "key" = 'cheddar' WHERE "key" = 'cheddar';
UPDATE "ingredients" SET "key" = 'egg' WHERE "key" = 'oeuf';
UPDATE "ingredients" SET "key" = 'coconut_milk' WHERE "key" = 'lait_de_coco';
UPDATE "ingredients" SET "key" = 'coconut_cream' WHERE "key" = 'creme_de_coco';
UPDATE "ingredients" SET "key" = 'almond_milk' WHERE "key" = 'lait_d_amande';
UPDATE "ingredients" SET "key" = 'oat_milk' WHERE "key" = 'lait_d_avoine';
UPDATE "ingredients" SET "key" = 'tofu' WHERE "key" = 'tofu';
UPDATE "ingredients" SET "key" = 'silken_tofu' WHERE "key" = 'tofu_soyeux';
UPDATE "ingredients" SET "key" = 'herbes_de_provence' WHERE "key" = 'herbes_de_provence';
UPDATE "ingredients" SET "key" = 'black_pepper' WHERE "key" = 'poivre_noir';
UPDATE "ingredients" SET "key" = 'paprika' WHERE "key" = 'paprika';
UPDATE "ingredients" SET "key" = 'espelette_pepper' WHERE "key" = 'piment_d_espelette';
UPDATE "ingredients" SET "key" = 'cayenne_pepper' WHERE "key" = 'piment_de_cayenne';
UPDATE "ingredients" SET "key" = 'cumin' WHERE "key" = 'cumin';
UPDATE "ingredients" SET "key" = 'curry_powder' WHERE "key" = 'curry_poudre';
UPDATE "ingredients" SET "key" = 'turmeric' WHERE "key" = 'curcuma';
UPDATE "ingredients" SET "key" = 'cinnamon' WHERE "key" = 'cannelle';
UPDATE "ingredients" SET "key" = 'nutmeg' WHERE "key" = 'muscade';
UPDATE "ingredients" SET "key" = 'saffron' WHERE "key" = 'safran';
UPDATE "ingredients" SET "key" = 'clove' WHERE "key" = 'clou_de_girofle';
UPDATE "ingredients" SET "key" = 'vanilla_bean' WHERE "key" = 'vanille_gousse';
UPDATE "ingredients" SET "key" = 'white_pepper' WHERE "key" = 'poivre_blanc';
UPDATE "ingredients" SET "key" = 'pink_pepper' WHERE "key" = 'poivre_rose';
UPDATE "ingredients" SET "key" = 'sichuan_pepper' WHERE "key" = 'poivre_du_sichuan';
UPDATE "ingredients" SET "key" = 'smoked_paprika' WHERE "key" = 'paprika_fume';
UPDATE "ingredients" SET "key" = 'bird_eye_chili' WHERE "key" = 'piment_oiseau';
UPDATE "ingredients" SET "key" = 'juniper_berries' WHERE "key" = 'baies_de_genievre';
UPDATE "ingredients" SET "key" = 'star_anise' WHERE "key" = 'anis_etoile_badiane';
UPDATE "ingredients" SET "key" = 'green_anise' WHERE "key" = 'anis_vert';
UPDATE "ingredients" SET "key" = 'fennel_seeds' WHERE "key" = 'graines_de_fenouil';
UPDATE "ingredients" SET "key" = 'sumac' WHERE "key" = 'sumac';
UPDATE "ingredients" SET "key" = 'nigella' WHERE "key" = 'nigelle';
UPDATE "ingredients" SET "key" = 'allspice' WHERE "key" = 'quatre_epices';
UPDATE "ingredients" SET "key" = 'colombo_powder' WHERE "key" = 'colombo_poudre';
UPDATE "ingredients" SET "key" = 'baharat' WHERE "key" = 'baharat';
UPDATE "ingredients" SET "key" = 'horseradish' WHERE "key" = 'raifort';
UPDATE "ingredients" SET "key" = 'herb_salt' WHERE "key" = 'sel_aux_herbes';
UPDATE "ingredients" SET "key" = 'celery_salt' WHERE "key" = 'sel_de_celeri';
UPDATE "ingredients" SET "key" = 'fleur_de_sel' WHERE "key" = 'fleur_de_sel';
UPDATE "ingredients" SET "key" = 'salt' WHERE "key" = 'sel';
UPDATE "ingredients" SET "key" = 'five_spice' WHERE "key" = 'cinq_epices';
UPDATE "ingredients" SET "key" = 'garam_masala' WHERE "key" = 'garam_masala';
UPDATE "ingredients" SET "key" = 'coriander_seeds' WHERE "key" = 'graines_de_coriandre';
UPDATE "ingredients" SET "key" = 'cardamom' WHERE "key" = 'cardamome';
UPDATE "ingredients" SET "key" = 'fenugreek' WHERE "key" = 'fenugrec';
UPDATE "ingredients" SET "key" = 'jalapeno' WHERE "key" = 'piment_jalapeno';
UPDATE "ingredients" SET "key" = 'chipotle' WHERE "key" = 'piment_chipotle';
UPDATE "ingredients" SET "key" = 'poblano_pepper' WHERE "key" = 'piment_poblano';
UPDATE "ingredients" SET "key" = 'habanero' WHERE "key" = 'piment_habanero';
UPDATE "ingredients" SET "key" = 'ras_el_hanout' WHERE "key" = 'ras_el_hanout';
UPDATE "ingredients" SET "key" = 'zaatar' WHERE "key" = 'za_atar';
UPDATE "ingredients" SET "key" = 'soy_sauce' WHERE "key" = 'sauce_soja';
UPDATE "ingredients" SET "key" = 'mustard' WHERE "key" = 'moutarde';
UPDATE "ingredients" SET "key" = 'mayonnaise' WHERE "key" = 'mayonnaise';
UPDATE "ingredients" SET "key" = 'ketchup' WHERE "key" = 'ketchup';
UPDATE "ingredients" SET "key" = 'tabasco' WHERE "key" = 'tabasco';
UPDATE "ingredients" SET "key" = 'worcestershire_sauce' WHERE "key" = 'sauce_worcestershire';
UPDATE "ingredients" SET "key" = 'fish_sauce' WHERE "key" = 'sauce_nuoc_mam';
UPDATE "ingredients" SET "key" = 'wasabi' WHERE "key" = 'wasabi';
UPDATE "ingredients" SET "key" = 'harissa' WHERE "key" = 'harissa';
UPDATE "ingredients" SET "key" = 'curry_paste' WHERE "key" = 'pate_de_curry';
UPDATE "ingredients" SET "key" = 'peanut_butter' WHERE "key" = 'beurre_de_cacahuete';
UPDATE "ingredients" SET "key" = 'dijon_mustard' WHERE "key" = 'moutarde_de_dijon';
UPDATE "ingredients" SET "key" = 'wholegrain_mustard' WHERE "key" = 'moutarde_a_l_ancienne';
UPDATE "ingredients" SET "key" = 'barbecue_sauce' WHERE "key" = 'sauce_barbecue';
UPDATE "ingredients" SET "key" = 'tartar_sauce' WHERE "key" = 'sauce_tartare';
UPDATE "ingredients" SET "key" = 'cocktail_sauce' WHERE "key" = 'sauce_cocktail';
UPDATE "ingredients" SET "key" = 'bearnaise_sauce' WHERE "key" = 'sauce_bearnaise';
UPDATE "ingredients" SET "key" = 'hollandaise_sauce' WHERE "key" = 'sauce_hollandaise';
UPDATE "ingredients" SET "key" = 'bechamel_sauce' WHERE "key" = 'sauce_bechamel';
UPDATE "ingredients" SET "key" = 'teriyaki_sauce' WHERE "key" = 'sauce_teriyaki';
UPDATE "ingredients" SET "key" = 'ponzu_sauce' WHERE "key" = 'sauce_ponzu';
UPDATE "ingredients" SET "key" = 'chimichurri' WHERE "key" = 'chimichurri';
UPDATE "ingredients" SET "key" = 'red_pesto' WHERE "key" = 'pesto_rouge_tomates_sechees';
UPDATE "ingredients" SET "key" = 'pesto' WHERE "key" = 'pesto';
UPDATE "ingredients" SET "key" = 'oyster_sauce' WHERE "key" = 'sauce_huitre';
UPDATE "ingredients" SET "key" = 'hoisin_sauce' WHERE "key" = 'sauce_hoisin';
UPDATE "ingredients" SET "key" = 'sriracha' WHERE "key" = 'sauce_sriracha';
UPDATE "ingredients" SET "key" = 'sweet_chili_sauce' WHERE "key" = 'sauce_sweet_chili';
UPDATE "ingredients" SET "key" = 'miso' WHERE "key" = 'miso';
UPDATE "ingredients" SET "key" = 'shrimp_paste' WHERE "key" = 'pate_de_crevettes';
UPDATE "ingredients" SET "key" = 'red_curry_paste' WHERE "key" = 'pate_de_curry_rouge_thai';
UPDATE "ingredients" SET "key" = 'green_curry_paste' WHERE "key" = 'pate_de_curry_vert_thai';
UPDATE "ingredients" SET "key" = 'tahini' WHERE "key" = 'tahini';
UPDATE "ingredients" SET "key" = 'olive_oil' WHERE "key" = 'huile_d_olive';
UPDATE "ingredients" SET "key" = 'sunflower_oil' WHERE "key" = 'huile_de_tournesol';
UPDATE "ingredients" SET "key" = 'rapeseed_oil' WHERE "key" = 'huile_de_colza';
UPDATE "ingredients" SET "key" = 'coconut_oil' WHERE "key" = 'huile_de_coco';
UPDATE "ingredients" SET "key" = 'sesame_oil' WHERE "key" = 'huile_de_sesame';
UPDATE "ingredients" SET "key" = 'cider_vinegar' WHERE "key" = 'vinaigre_de_cidre';
UPDATE "ingredients" SET "key" = 'white_vinegar' WHERE "key" = 'vinaigre_blanc';
UPDATE "ingredients" SET "key" = 'balsamic_vinegar' WHERE "key" = 'vinaigre_balsamique';
UPDATE "ingredients" SET "key" = 'capers' WHERE "key" = 'capres';
UPDATE "ingredients" SET "key" = 'olives' WHERE "key" = 'olives';
UPDATE "ingredients" SET "key" = 'white_wine' WHERE "key" = 'vin_blanc_cuisine';
UPDATE "ingredients" SET "key" = 'red_wine' WHERE "key" = 'vin_rouge_cuisine';
UPDATE "ingredients" SET "key" = 'red_wine_vinegar' WHERE "key" = 'vinaigre_de_vin_rouge';
UPDATE "ingredients" SET "key" = 'white_wine_vinegar' WHERE "key" = 'vinaigre_de_vin_blanc';
UPDATE "ingredients" SET "key" = 'sherry_vinegar' WHERE "key" = 'vinaigre_de_xeres';
UPDATE "ingredients" SET "key" = 'walnut_oil' WHERE "key" = 'huile_de_noix';
UPDATE "ingredients" SET "key" = 'hazelnut_oil' WHERE "key" = 'huile_de_noisette';
UPDATE "ingredients" SET "key" = 'peanut_oil' WHERE "key" = 'huile_d_arachide';
UPDATE "ingredients" SET "key" = 'chili_oil' WHERE "key" = 'huile_pimentee';
UPDATE "ingredients" SET "key" = 'rice_vinegar' WHERE "key" = 'vinaigre_de_riz';
UPDATE "ingredients" SET "key" = 'mirin' WHERE "key" = 'mirin';
UPDATE "ingredients" SET "key" = 'sake' WHERE "key" = 'sake_cuisine';
UPDATE "ingredients" SET "key" = 'lemon_juice' WHERE "key" = 'jus_de_citron';
UPDATE "ingredients" SET "key" = 'lime_juice' WHERE "key" = 'jus_de_citron_vert';
UPDATE "ingredients" SET "key" = 'orange_juice' WHERE "key" = 'jus_d_orange';
UPDATE "ingredients" SET "key" = 'apple_juice' WHERE "key" = 'jus_de_pomme';
UPDATE "ingredients" SET "key" = 'grape_juice' WHERE "key" = 'jus_de_raisin';
UPDATE "ingredients" SET "key" = 'tomato_juice' WHERE "key" = 'jus_de_tomate';
UPDATE "ingredients" SET "key" = 'cranberry_juice' WHERE "key" = 'jus_de_cranberry';
UPDATE "ingredients" SET "key" = 'coffee' WHERE "key" = 'cafe';
UPDATE "ingredients" SET "key" = 'tea' WHERE "key" = 'the';
UPDATE "ingredients" SET "key" = 'beer' WHERE "key" = 'biere_cuisine';
UPDATE "ingredients" SET "key" = 'cider' WHERE "key" = 'cidre_cuisine';
UPDATE "ingredients" SET "key" = 'champagne' WHERE "key" = 'champagne_vin_petillant_cuisine';
UPDATE "ingredients" SET "key" = 'port_wine' WHERE "key" = 'porto_cuisine';
UPDATE "ingredients" SET "key" = 'vin_jaune' WHERE "key" = 'vin_jaune_cuisine';
UPDATE "ingredients" SET "key" = 'cognac' WHERE "key" = 'cognac';
UPDATE "ingredients" SET "key" = 'rum' WHERE "key" = 'rhum';
UPDATE "ingredients" SET "key" = 'whisky' WHERE "key" = 'whisky';
UPDATE "ingredients" SET "key" = 'vodka' WHERE "key" = 'vodka';
UPDATE "ingredients" SET "key" = 'wheat_flour' WHERE "key" = 'farine_de_ble';
UPDATE "ingredients" SET "key" = 'whole_wheat_flour' WHERE "key" = 'farine_complete';
UPDATE "ingredients" SET "key" = 'corn_flour' WHERE "key" = 'farine_de_mais';
UPDATE "ingredients" SET "key" = 'buckwheat_flour' WHERE "key" = 'farine_de_sarrasin';
UPDATE "ingredients" SET "key" = 'rice_flour' WHERE "key" = 'farine_de_riz';
UPDATE "ingredients" SET "key" = 'vegetable_stock_cube' WHERE "key" = 'bouillon_cube_legumes';
UPDATE "ingredients" SET "key" = 'chicken_stock_cube' WHERE "key" = 'bouillon_cube_volaille';
UPDATE "ingredients" SET "key" = 'tomato_paste' WHERE "key" = 'concentre_de_tomate';
UPDATE "ingredients" SET "key" = 'tomato_coulis' WHERE "key" = 'coulis_de_tomate';
UPDATE "ingredients" SET "key" = 'canned_peeled_tomatoes' WHERE "key" = 'tomates_pelees_conserve';
UPDATE "ingredients" SET "key" = 'sun_dried_tomatoes' WHERE "key" = 'tomates_sechees';
UPDATE "ingredients" SET "key" = 'veal_stock' WHERE "key" = 'fond_de_veau';
UPDATE "ingredients" SET "key" = 'chicken_stock' WHERE "key" = 'fond_de_volaille';
UPDATE "ingredients" SET "key" = 'beef_stock_cube' WHERE "key" = 'bouillon_cube_boeuf';
UPDATE "ingredients" SET "key" = 'fish_stock_cube' WHERE "key" = 'bouillon_cube_poisson';
UPDATE "ingredients" SET "key" = 'vegetable_broth' WHERE "key" = 'bouillon_de_legumes';
UPDATE "ingredients" SET "key" = 'chicken_broth' WHERE "key" = 'bouillon_de_volaille';
UPDATE "ingredients" SET "key" = 'beef_broth' WHERE "key" = 'bouillon_de_boeuf';
UPDATE "ingredients" SET "key" = 'court_bouillon' WHERE "key" = 'court_bouillon';
UPDATE "ingredients" SET "key" = 'dashi' WHERE "key" = 'dashi_bouillon_japonais';
UPDATE "ingredients" SET "key" = 'shellfish_bisque' WHERE "key" = 'bisque_de_crustaces';
UPDATE "ingredients" SET "key" = 'tapioca_flour' WHERE "key" = 'farine_de_tapioca';
UPDATE "ingredients" SET "key" = 'masa_harina' WHERE "key" = 'masa_harina';
UPDATE "ingredients" SET "key" = 'water' WHERE "key" = 'eau';
UPDATE "ingredients" SET "key" = 'sparkling_water' WHERE "key" = 'eau_gazeuse';
UPDATE "ingredients" SET "key" = 'orange_blossom_water' WHERE "key" = 'eau_de_fleur_d_oranger';
UPDATE "ingredients" SET "key" = 'rose_water' WHERE "key" = 'eau_de_rose';
UPDATE "ingredients" SET "key" = 'fish_fumet' WHERE "key" = 'fumet_de_poisson';
UPDATE "ingredients" SET "key" = 'bakers_yeast' WHERE "key" = 'levure_boulangere';
UPDATE "ingredients" SET "key" = 'baking_powder' WHERE "key" = 'levure_chimique';
UPDATE "ingredients" SET "key" = 'cornstarch' WHERE "key" = 'maizena';
UPDATE "ingredients" SET "key" = 'lupin_flour' WHERE "key" = 'farine_de_lupin';
UPDATE "ingredients" SET "key" = 'gelatin' WHERE "key" = 'gelatine';
UPDATE "ingredients" SET "key" = 'baking_soda' WHERE "key" = 'bicarbonate_de_soude';
UPDATE "ingredients" SET "key" = 'potato_starch' WHERE "key" = 'fecule_de_pomme_de_terre';
UPDATE "ingredients" SET "key" = 'sugar' WHERE "key" = 'sucre';
UPDATE "ingredients" SET "key" = 'honey' WHERE "key" = 'miel';
UPDATE "ingredients" SET "key" = 'maple_syrup' WHERE "key" = 'sirop_d_erable';
UPDATE "ingredients" SET "key" = 'brown_sugar' WHERE "key" = 'sucre_roux';
UPDATE "ingredients" SET "key" = 'powdered_sugar' WHERE "key" = 'sucre_glace';
UPDATE "ingredients" SET "key" = 'demerara_sugar' WHERE "key" = 'cassonade';
UPDATE "ingredients" SET "key" = 'dark_chocolate' WHERE "key" = 'chocolat_noir';
UPDATE "ingredients" SET "key" = 'milk_chocolate' WHERE "key" = 'chocolat_au_lait';
UPDATE "ingredients" SET "key" = 'white_chocolate' WHERE "key" = 'chocolat_blanc';
UPDATE "ingredients" SET "key" = 'chocolate_chips' WHERE "key" = 'pepites_de_chocolat';
UPDATE "ingredients" SET "key" = 'cocoa_powder' WHERE "key" = 'cacao_en_poudre';
UPDATE "ingredients" SET "key" = 'vanilla_extract' WHERE "key" = 'extrait_de_vanille';
UPDATE "ingredients" SET "key" = 'palm_sugar' WHERE "key" = 'sucre_de_palme';
UPDATE "ingredients" SET "key" = 'cane_syrup' WHERE "key" = 'sirop_de_sucre_de_canne';

View file

@ -34,13 +34,17 @@ model House {
@@map("house")
}
/// `name` is `@unique` — not in the original spec doc, added so the seed
/// script (prisma/seed.ts) can `upsert` by name and stay idempotent/safe to
/// `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.
/// regime. A stable slug (e.g. `"vegetarien"`), not the display label —
/// the label itself lives in `apps/web`'s `locales/fr/translation.json`
/// under `catalog.diets.<key>` (see `reference-seed-data.ts`'s `DIETS` and
/// `utils/slugify.ts`), so it can be edited/translated without ever
/// touching this column or the rows that reference it by id.
model Diet {
id Int @id @default(autoincrement())
name String @unique
id Int @id @default(autoincrement())
key String @unique
users UserProfile[]
recipes RecipeDiet[]
@ -59,11 +63,13 @@ enum AllergenKind {
}
/// Enumeration-style table, meant to grow over time (e.g. allergy nuances).
/// `name` is `@unique` for the same reason as `Diet.name` above. `kind` is
/// also not in the original spec doc — see {@link AllergenKind}.
/// `key` is `@unique` for the same reason as `Diet.key` above — a stable
/// slug (`catalog.allergens.<key>` in `apps/web`'s locale file), not the
/// display label. `kind` is also not in the original spec doc — see
/// {@link AllergenKind}.
model Category {
id Int @id @default(autoincrement())
name String @unique
key String @unique
kind AllergenKind @default(ALLERGY)
allergies Allergy[]
@ -288,9 +294,11 @@ model RecipeDiet {
@@map("recipe_diet")
}
/// `name` is `@unique` — not in the original spec doc, added so the seed
/// script (reference-seed-data.ts) can `upsert` by name and stay
/// idempotent/safe to re-run, same reason as `Diet.name`/`Category.name`.
/// `key` is `@unique` — not in the original spec doc, added so the seed
/// script (reference-seed-data.ts) can `upsert` by key and stay
/// idempotent/safe to re-run, same reason as `Diet.key`/`Category.key`. A
/// stable slug (`catalog.ingredients.<key>` in `apps/web`'s locale file),
/// not the display label.
/// Ingredients are reference data (like Diet/Allergy): seeded, never
/// created/edited/deleted through the API.
/// Not in the original spec doc — supermarket-aisle grouping ("rayons") so
@ -424,7 +432,7 @@ enum IngredientIcon {
model Ingredient {
id Int @id @default(autoincrement())
name String @unique
key String @unique
icon IngredientIcon @default(JAR)
category IngredientCategory @default(EPICERIE_SECHE)
subcategory IngredientSubcategory @default(AUTRES)

View file

@ -0,0 +1,49 @@
/**
* One-off generator, run by hand whenever the catalog's reference data
* changes (a new ingredient/diet/allergen added to
* `db/reference-seed-data.ts`, or an English key corrected in
* `catalog-en-keys.ts`): regenerates
* `apps/web/src/locales/fr/translation.json`'s
* `catalog.{diets,allergens,ingredients}` sections (English key -> French
* label), merged in without touching the rest of the file.
*
* Doesn't touch the database a brand new diet/allergen/ingredient is
* created fresh by `seedReferenceData`'s normal create path (see
* `reference-seed-data.ts`), no backfill needed. Renaming an *existing*
* item's English key in `catalog-en-keys.ts` does need a one-off migration
* (`UPDATE ... SET key = ...`, keyed by the *old* key value) written by
* hand for that occasion see
* `prisma/migrations/20260818193000_catalog_keys_to_english/` for the shape
* one looks like.
*
* Never imported by the app itself a dev-time tool, run via
* `tsx scripts/generate-catalog-i18n.ts`.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { getEnglishKey } from "../src/db/catalog-en-keys.js";
import { ALLERGENS, DIETS, INGREDIENT_GROUPS } from "../src/db/reference-seed-data.js";
const here = fileURLToPath(new URL(".", import.meta.url));
function toKeyLabelMap(labels: string[]): Record<string, string> {
const map: Record<string, string> = {};
for (const label of labels) {
map[getEnglishKey(label)] = label;
}
return map;
}
const diets = toKeyLabelMap(DIETS);
const allergens = toKeyLabelMap(ALLERGENS.map((a) => a.name));
const ingredients = toKeyLabelMap(INGREDIENT_GROUPS.flatMap((g) => g.items.map((i) => i.name)));
console.log(
`diets: ${Object.keys(diets).length}, allergens: ${Object.keys(allergens).length}, ingredients: ${Object.keys(ingredients).length}`,
);
const localePath = `${here}../../web/src/locales/fr/translation.json`;
const locale = JSON.parse(readFileSync(localePath, "utf8"));
locale.catalog = { diets, allergens, ingredients };
writeFileSync(localePath, `${JSON.stringify(locale, null, 2)}\n`);
console.log(`wrote ${localePath}`);

View file

@ -0,0 +1,46 @@
import { getEnglishKey } from "../src/db/catalog-en-keys.js";
/**
* One-off validation, run by hand: checks that `catalog-en-keys.ts` has an
* entry for every diet/allergen/ingredient currently in
* `reference-seed-data.ts`, and that the resulting English keys are unique
* within each table. Not part of the app or the seed itself just a
* pre-flight check while building/editing the dictionary by hand.
*/
import { ALLERGENS, DIETS, INGREDIENT_GROUPS } from "../src/db/reference-seed-data.js";
function check(label: string, names: string[]) {
const keys = new Map<string, string>();
const missing: string[] = [];
const duplicates: string[] = [];
for (const name of names) {
let key: string;
try {
key = getEnglishKey(name);
} catch {
missing.push(name);
continue;
}
const existing = keys.get(key);
if (existing !== undefined && existing !== name) {
duplicates.push(`"${existing}" and "${name}" both map to "${key}"`);
}
keys.set(key, name);
}
console.log(`${label}: ${names.length} names, ${keys.size} unique keys`);
if (missing.length > 0) {
console.log(` MISSING (${missing.length}):`, missing);
}
if (duplicates.length > 0) {
console.log(` DUPLICATES (${duplicates.length}):`, duplicates);
}
}
check("Diets", DIETS);
check(
"Allergens",
ALLERGENS.map((a) => a.name),
);
check(
"Ingredients",
INGREDIENT_GROUPS.flatMap((g) => g.items.map((i) => i.name)),
);

View file

@ -0,0 +1,557 @@
/**
* English key for every catalog reference label `Diet`/`Category`
* (allergens)/`Ingredient` rows must carry a stable, storage-safe `key`
* that is itself English, independent of whatever language the *seed's*
* authoring label (`DIETS`/`ALLERGENS`/`INGREDIENT_GROUPS` in
* `reference-seed-data.ts`, currently French) happens to be in a French
* `key` would tie the identifier to the one language it's meant to be
* decoupled from (see `utils/slugify.ts`'s doc comment and `apps/web`'s
* `locales/fr/translation.json` `catalog.*` namespace, which resolves the
* *display* label from this same key).
*
* Hand-assigned (not machine-translated) an English label is chosen once
* and never changes, exactly like the key it produces (via {@link
* slugify}). Keyed by the exact French authoring label so
* `reference-seed-data.ts` and `scripts/generate-catalog-i18n.ts` can look
* a row's key up by the same string they already have in hand.
*
* `getEnglishKey` throws on a missing entry rather than falling back to
* slugifying the French label a silently-French key defeats the point,
* so a newly-added diet/allergen/ingredient must get an entry here before
* it can seed.
*/
import { slugify } from "../utils/slugify.js";
const DIET_KEYS: Record<string, string> = {
Omnivore: "omnivore",
Végétarien: "vegetarian",
Végan: "vegan",
Pescétarien: "pescatarian",
"Sans gluten": "gluten_free",
};
const ALLERGEN_KEYS: Record<string, string> = {
Gluten: "gluten",
Crustacés: "crustaceans",
Œufs: "eggs",
Poissons: "fish",
Arachides: "peanuts",
Soja: "soy",
Lait: "milk",
"Fruits à coque": "tree_nuts",
Céleri: "celery",
Moutarde: "mustard",
"Graines de sésame": "sesame_seeds",
Sulfites: "sulfites",
Lupin: "lupin",
Mollusques: "molluscs",
};
// Grouped by (category, subcategory), mirroring `INGREDIENT_GROUPS` in
// reference-seed-data.ts, purely so a translator can find/check an entry
// against its source group — this is one flat lookup table at runtime.
const INGREDIENT_KEYS: Record<string, string> = {
// --- Produits frais / Légumes ---------------------------------------
Tomate: "tomato",
Oignon: "onion",
Échalote: "shallot",
Ail: "garlic",
Carotte: "carrot",
Courgette: "zucchini",
Concombre: "cucumber",
Cornichons: "gherkins",
Poivron: "bell_pepper",
Champignon: "mushroom",
Cèpes: "porcini",
Aubergine: "eggplant",
Brocoli: "broccoli",
"Chou-fleur": "cauliflower",
"Chou blanc": "white_cabbage",
"Chou rouge": "red_cabbage",
"Chou de Bruxelles": "brussels_sprouts",
Épinard: "spinach",
Blette: "swiss_chard",
Salade: "lettuce",
Roquette: "arugula",
Cresson: "watercress",
Poireau: "leek",
Radis: "radish",
Betterave: "beetroot",
Navet: "turnip",
Panais: "parsnip",
"Haricot vert": "green_bean",
"Petit pois": "pea",
Maïs: "corn",
Artichaut: "artichoke",
Fenouil: "fennel",
Endive: "endive",
Potiron: "pumpkin",
Butternut: "butternut_squash",
Asperge: "asparagus",
Avocat: "avocado",
"Pomme de terre": "potato",
"Patate douce": "sweet_potato",
"Tomates cerises": "cherry_tomato",
"Pak-choï": "bok_choy",
"Germes de soja": "soybean_sprouts",
Shiitake: "shiitake",
Daikon: "daikon",
"Piment vert frais": "fresh_green_chili",
// --- 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",
// --- 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",
// --- Boucherie & poissonnerie / Volailles -----------------------------
Poulet: "chicken",
Dinde: "turkey",
Canard: "duck",
"Magret de canard": "duck_breast",
// --- 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",
// --- 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",
// --- É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",
// --- É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",
// --- 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",
// --- 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",
// --- 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",
"Vin blanc (cuisine)": "white_wine",
"Vin rouge (cuisine)": "red_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",
"Vinaigre de riz": "rice_vinegar",
Mirin: "mirin",
"Saké (cuisine)": "sake",
"Jus de citron": "lemon_juice",
"Jus de citron vert": "lime_juice",
"Jus d'orange": "orange_juice",
"Jus de pomme": "apple_juice",
"Jus de raisin": "grape_juice",
"Jus de tomate": "tomato_juice",
"Jus de cranberry": "cranberry_juice",
Café: "coffee",
Thé: "tea",
"Bière (cuisine)": "beer",
"Cidre (cuisine)": "cider",
"Champagne / vin pétillant (cuisine)": "champagne",
"Porto (cuisine)": "port_wine",
"Vin jaune (cuisine)": "vin_jaune",
Cognac: "cognac",
Rhum: "rum",
Whisky: "whisky",
Vodka: "vodka",
// --- Aides culinaires / Bases -------------------------------------------
"Farine de blé": "wheat_flour",
"Farine complète": "whole_wheat_flour",
"Farine de maïs": "corn_flour",
"Farine de sarrasin": "buckwheat_flour",
"Farine de riz": "rice_flour",
"Bouillon cube légumes": "vegetable_stock_cube",
"Bouillon cube volaille": "chicken_stock_cube",
"Concentré de tomate": "tomato_paste",
"Coulis de tomate": "tomato_coulis",
"Tomates pelées (conserve)": "canned_peeled_tomatoes",
"Tomates séchées": "sun_dried_tomatoes",
"Fond de veau": "veal_stock",
"Fond de volaille": "chicken_stock",
"Bouillon cube bœuf": "beef_stock_cube",
"Bouillon cube poisson": "fish_stock_cube",
"Bouillon de légumes": "vegetable_broth",
"Bouillon de volaille": "chicken_broth",
"Bouillon de bœuf": "beef_broth",
"Court-bouillon": "court_bouillon",
"Dashi (bouillon japonais)": "dashi",
"Bisque de crustacés": "shellfish_bisque",
"Farine de tapioca": "tapioca_flour",
"Masa harina": "masa_harina",
Eau: "water",
"Eau gazeuse": "sparkling_water",
"Eau de fleur d'oranger": "orange_blossom_water",
"Eau de rose": "rose_water",
"Fumet de poisson": "fish_fumet",
// --- Aides culinaires / Épaississants -------------------------------------
"Levure boulangère": "bakers_yeast",
"Levure chimique": "baking_powder",
Maïzena: "cornstarch",
"Farine de lupin": "lupin_flour",
Gélatine: "gelatin",
"Bicarbonate de soude": "baking_soda",
"Fécule de pomme de terre": "potato_starch",
// --- Aides culinaires / Sucres ---------------------------------------------
Sucre: "sugar",
Miel: "honey",
"Sirop d'érable": "maple_syrup",
"Sucre roux": "brown_sugar",
"Sucre glace": "powdered_sugar",
Cassonade: "demerara_sugar",
"Chocolat noir": "dark_chocolate",
"Chocolat au lait": "milk_chocolate",
"Chocolat blanc": "white_chocolate",
"Pépites de chocolat": "chocolate_chips",
"Cacao en poudre": "cocoa_powder",
"Extrait de vanille": "vanilla_extract",
"Sucre de palme": "palm_sugar",
"Sirop de sucre de canne": "cane_syrup",
};
const ALL_KEYS: Record<string, string> = {
...DIET_KEYS,
...ALLERGEN_KEYS,
...INGREDIENT_KEYS,
};
/**
* Resolves a seed-time French authoring label to its English `key`
* throws if it's missing an entry above rather than silently falling back
* to a French slug, since a new diet/allergen/ingredient needs a
* deliberately-chosen English key before it can seed at all.
* `slugify` still runs over the result so a stray character/casing slip in
* the table above can't produce a key that doesn't match the
* `[a-z0-9_]`-only shape every other key has.
*/
export function getEnglishKey(frenchLabel: string): string {
const english = ALL_KEYS[frenchLabel];
if (english === undefined) {
throw new Error(
`No English key registered for "${frenchLabel}" — add one to catalog-en-keys.ts`,
);
}
return slugify(english);
}

View file

@ -5,10 +5,14 @@ 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.
const DIETS = ["Omnivore", "Végétarien", "Végan", "Pescétarien", "Sans gluten"];
// 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"];
// The 14 allergens EU Regulation 1169/2011 (Annex II) requires food
// businesses to declare — a standard, defensible reference list rather than
@ -17,7 +21,7 @@ const DIETS = ["Omnivore", "Végétarien", "Végan", "Pescétarien", "Sans glute
// sensitivity) per the product decision discussed in chat: only Gluten and
// Sulfites are commonly-recognized intolerances among the 14; the rest are
// true allergens.
const ALLERGENS: Array<{ name: string; kind: AllergenKind }> = [
export const ALLERGENS: Array<{ name: string; kind: AllergenKind }> = [
{ name: "Gluten", kind: "INTOLERANCE" },
{ name: "Crustacés", kind: "ALLERGY" },
{ name: "Œufs", kind: "ALLERGY" },
@ -92,7 +96,7 @@ interface IngredientSeed {
// repeating the same values on hundreds of items; only a group's
// exceptions (a fish-based stock inside "bases", a cheese inside "produits
// laitiers"…) need a per-item override.
const INGREDIENT_GROUPS: Array<{
export const INGREDIENT_GROUPS: Array<{
category: IngredientCategory;
subcategory: IngredientSubcategory;
defaultDiets: string[];
@ -879,27 +883,38 @@ const INGREDIENTS: Array<
/**
* Populates the `Diet`/`Category`/`Allergy` reference tables. Idempotent
* (safe to call against a database that already has this data upserts by
* `name`, both `@unique`) used both by `prisma/seed.ts` (the CLI entry
* `key`, all `@unique`) used both by `prisma/seed.ts` (the CLI entry
* point, `prisma db seed`) and by `test-support/reset-db.ts` (so every
* 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`).
*/
export async function seedReferenceData(prisma: PrismaClient): Promise<void> {
for (const name of DIETS) {
await prisma.diet.upsert({ where: { name }, update: {}, create: { name } });
const key = getEnglishKey(name);
await prisma.diet.upsert({ where: { key }, update: {}, create: { key } });
}
// `Allergy` itself carries no `name` — it's the selectable instance of a
// named `Category` (see schema.prisma) — so seeding an allergen means one
// Category (upserted by name) plus exactly one Allergy row under it,
// `Allergy` itself carries no `key` — it's the selectable instance of a
// keyed `Category` (see schema.prisma) — so seeding an allergen means one
// Category (upserted by key) plus exactly one Allergy row under it,
// 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);
const category = await prisma.category.upsert({
where: { name },
where: { key },
update: { kind },
create: { name, kind },
create: { key, kind },
});
const existing = await prisma.allergy.findFirst({ where: { categoryId: category.id } });
if (!existing) {
@ -916,17 +931,18 @@ export async function seedReferenceData(prisma: PrismaClient): Promise<void> {
// 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 existingIngredients = await prisma.ingredient.findMany({
where: { name: { in: INGREDIENTS.map((i) => i.name) } },
select: { id: true, name: true, icon: true, category: true, subcategory: true },
where: { key: { in: ingredientKeys } },
select: { id: true, key: true, icon: true, category: true, subcategory: true },
});
const existingByName = new Map(existingIngredients.map((i) => [i.name, i]));
const existingByKey = new Map(existingIngredients.map((i) => [i.key, i]));
const missingIngredients = INGREDIENTS.filter((i) => !existingByName.has(i.name));
const missingIngredients = INGREDIENTS.filter((i) => !existingByKey.has(getEnglishKey(i.name)));
if (missingIngredients.length > 0) {
await prisma.ingredient.createMany({
data: missingIngredients.map(({ name, icon, category, subcategory }) => ({
name,
key: getEnglishKey(name),
icon,
category,
subcategory,
@ -935,7 +951,7 @@ export async function seedReferenceData(prisma: PrismaClient): Promise<void> {
}
const changed = INGREDIENTS.filter((i) => {
const existing = existingByName.get(i.name);
const existing = existingByKey.get(getEnglishKey(i.name));
return (
existing &&
(existing.icon !== i.icon ||
@ -944,29 +960,32 @@ export async function seedReferenceData(prisma: PrismaClient): Promise<void> {
);
});
for (const { name, icon, category, subcategory } of changed) {
await prisma.ingredient.update({ where: { name }, data: { icon, category, subcategory } });
await prisma.ingredient.update({
where: { key: getEnglishKey(name) },
data: { icon, category, subcategory },
});
}
// Re-resolve every ingredient's id (existing + just-created) and every
// allergy's id (by its category name) once, then link them in a single
// allergy's id (by its category key) once, then link them in a single
// bulk insert — same "re-derived every time, not upserted per link"
// reasoning as before for `IngredientAllergy` (it has no natural per-row
// identity to upsert against), just batched instead of looped.
const allIngredients = await prisma.ingredient.findMany({
where: { name: { in: INGREDIENTS.map((i) => i.name) } },
select: { id: true, name: true },
where: { key: { in: ingredientKeys } },
select: { id: true, key: true },
});
const ingredientIdByName = new Map(allIngredients.map((i) => [i.name, i.id]));
const ingredientIdByKey = new Map(allIngredients.map((i) => [i.key, i.id]));
const allergies = await prisma.allergy.findMany({ include: { category: true } });
const allergyIdByCategoryName = new Map(allergies.map((a) => [a.category.name, a.id]));
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 = ingredientIdByName.get(name);
const ingredientId = ingredientIdByKey.get(getEnglishKey(name));
if (ingredientId === undefined) continue;
for (const allergenName of allergenNames) {
const allergyId = allergyIdByCategoryName.get(allergenName);
const allergyId = allergyIdByCategoryKey.get(getEnglishKey(allergenName));
if (allergyId !== undefined) links.push({ ingredientId, allergyId });
}
}
@ -978,14 +997,14 @@ export async function seedReferenceData(prisma: PrismaClient): Promise<void> {
// `dietNames` (item override, falling back to its group's `defaultDiets`
// in the `INGREDIENTS` flatten step) instead of `allergenNames`.
const diets = await prisma.diet.findMany();
const dietIdByName = new Map(diets.map((d) => [d.name, d.id]));
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 = ingredientIdByName.get(name);
const ingredientId = ingredientIdByKey.get(getEnglishKey(name));
if (ingredientId === undefined) continue;
for (const dietName of dietNames) {
const dietId = dietIdByName.get(dietName);
const dietId = dietIdByKey.get(getEnglishKey(dietName));
if (dietId !== undefined) dietLinks.push({ ingredientId, dietId });
}
}

View file

@ -39,21 +39,21 @@ type IngredientWithDetails = RecipeWithDetails["ingredients"][number]["ingredien
function toIngredientView(ingredient: IngredientWithDetails): IngredientView {
return {
id: ingredient.id,
name: ingredient.name,
key: ingredient.key,
icon: ingredient.icon,
category: ingredient.category,
subcategory: ingredient.subcategory,
allergens: ingredient.allergies.map(({ allergy }) => ({
id: allergy.id,
name: allergy.category.name,
key: allergy.category.key,
kind: allergy.category.kind,
})),
diets: ingredient.diets.map(({ diet }) => ({ id: diet.id, name: diet.name })),
diets: ingredient.diets.map(({ diet }) => ({ id: diet.id, key: diet.key })),
};
}
function toDietView(diet: { id: number; name: string }): DietView {
return { id: diet.id, name: diet.name };
function toDietView(diet: { id: number; key: string }): DietView {
return { id: diet.id, key: diet.key };
}
/** Deduplicates allergens (by id) across every ingredient of a recipe, for the aggregated "contains" badge — see {@link RecipeSummaryView.allergens}. */

View file

@ -1,35 +1,43 @@
import type { AllergyView, DietView, IngredientView } from "@batch-cooking/shared";
import { prisma } from "../../db/prisma.js";
/** All reference dietary regimes, alphabetically — small, static list (see prisma/seed.ts). */
/**
* All reference dietary regimes, ordered by `key` small, static list (see
* prisma/seed.ts). `key` is a stable slug, not the display label (see
* {@link DietView}), so this is an alphabetical-by-slug order rather than a
* true French alphabetical one close enough for a 5-item list, and the
* server has no other order to offer now that the label itself only exists
* client-side (`apps/web`'s `locales/fr/translation.json`).
*/
export async function getDiets(): Promise<DietView[]> {
return prisma.diet.findMany({ orderBy: { name: "asc" } });
return prisma.diet.findMany({ orderBy: { key: "asc" } });
}
/**
* All reference allergens, alphabetically. `Allergy` carries no `name` of
* its own it's the selectable instance of a named `Category` (see
* schema.prisma) so this resolves each allergen's display name from its
* category and flattens the split away for callers.
* All reference allergens, ordered by key (see {@link getDiets} for why key,
* not label). `Allergy` carries no `key` of its own it's the selectable
* instance of a keyed `Category` (see schema.prisma) so this resolves
* each allergen's key from its category and flattens the split away for
* callers.
*/
export async function getAllergies(): Promise<AllergyView[]> {
const allergies = await prisma.allergy.findMany({
include: { category: { select: { name: true, kind: true } } },
orderBy: { category: { name: "asc" } },
include: { category: { select: { key: true, kind: true } } },
orderBy: { category: { key: "asc" } },
});
return allergies.map((allergy) => ({
id: allergy.id,
name: allergy.category.name,
key: allergy.category.key,
kind: allergy.category.kind,
}));
}
/**
* All reference ingredients, alphabetically, each resolved to its allergens
* (see `IngredientAllergy` in schema.prisma) and compatible diet regimes
* (see `IngredientDiet`) same aplattening approach as {@link getAllergies}.
* Ingredients with no linked allergen/diet come back with `allergens: []`/
* `diets: []`.
* All reference ingredients, ordered by key (see {@link getDiets} for why),
* each resolved to its allergens (see `IngredientAllergy` in schema.prisma)
* and compatible diet regimes (see `IngredientDiet`) same flattening
* approach as {@link getAllergies}. Ingredients with no linked
* allergen/diet come back with `allergens: []`/`diets: []`.
*/
export async function getIngredients(): Promise<IngredientView[]> {
const ingredients = await prisma.ingredient.findMany({
@ -37,19 +45,19 @@ export async function getIngredients(): Promise<IngredientView[]> {
allergies: { include: { allergy: { include: { category: true } } } },
diets: { include: { diet: true } },
},
orderBy: { name: "asc" },
orderBy: { key: "asc" },
});
return ingredients.map((ingredient) => ({
id: ingredient.id,
name: ingredient.name,
key: ingredient.key,
icon: ingredient.icon,
category: ingredient.category,
subcategory: ingredient.subcategory,
allergens: ingredient.allergies.map(({ allergy }) => ({
id: allergy.id,
name: allergy.category.name,
key: allergy.category.key,
kind: allergy.category.kind,
})),
diets: ingredient.diets.map(({ diet }) => ({ id: diet.id, name: diet.name })),
diets: ingredient.diets.map(({ diet }) => ({ id: diet.id, key: diet.key })),
}));
}

View file

@ -0,0 +1,26 @@
/**
* Deterministic slug for a French reference-data label used as the
* stable, storage-safe `key` for `Diet`/`Category`/`Ingredient` rows (see
* `db/reference-seed-data.ts`), decoupled from the display label so the
* label itself can live in `apps/web`'s `locales/fr/translation.json`
* (`catalog.*` namespace) instead of the database. A row's `key` is derived
* from its seed-time French name once and then never changes renaming the
* *label* later (a translation fix, a rewording) never touches the key, the
* FK-referencing rows, or any code that looks a row up by key.
*
* Handles the two French ligatures NFD decomposition doesn't touch (`œ`,
* `æ` aren't accented letters, they're distinct glyphs) explicitly, then
* strips every other accent via NFD decomposition + Unicode "Mark" removal
* (`\p{M}`, every combining diacritic NFD can produce), then collapses
* whatever isn't `[a-z0-9]` into single underscores.
*/
export function slugify(label: string): string {
return label
.toLowerCase()
.replace(/œ/g, "oe")
.replace(/æ/g, "ae")
.normalize("NFD")
.replace(/\p{M}/gu, "")
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
}

View file

@ -3,6 +3,7 @@ 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";
@ -39,7 +40,9 @@ describe("Profile", () => {
it("sets the profile's regime to a valid, seeded diet", async () => {
const agent = request.agent(app);
await agent.post("/auth/signup").send(buildSignupPayload());
const diet = await prisma.diet.findFirstOrThrow({ where: { name: "Végétarien" } });
const diet = await prisma.diet.findFirstOrThrow({
where: { key: getEnglishKey("Végétarien") },
});
const res = await agent.patch("/profile/diet").send({ dietId: diet.id });
@ -50,7 +53,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: { name: "Végan" } });
const diet = await prisma.diet.findFirstOrThrow({ where: { key: getEnglishKey("Végan") } });
await agent.patch("/profile/diet").send({ dietId: diet.id });
const res = await agent.patch("/profile/diet").send({ dietId: null });
@ -83,8 +86,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.name === "Arachides");
const gluten = allergies.find((a) => a.category.name === "Gluten");
const peanuts = allergies.find((a) => a.category.key === getEnglishKey("Arachides"));
const gluten = allergies.find((a) => a.category.key === getEnglishKey("Gluten"));
if (!peanuts || !gluten) throw new Error("expected seeded allergens missing");
const initial = await agent.get("/profile/allergies");
@ -104,8 +107,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.name === "Arachides");
const gluten = allergies.find((a) => a.category.name === "Gluten");
const peanuts = allergies.find((a) => a.category.key === getEnglishKey("Arachides"));
const gluten = allergies.find((a) => a.category.key === getEnglishKey("Gluten"));
if (!peanuts || !gluten) throw new Error("expected seeded allergens missing");
await agent.patch("/profile/allergies").send({ allergyIds: [peanuts.id] });
@ -140,8 +143,12 @@ describe("Profile", () => {
it("starts empty, then reflects a saved selection", async () => {
const agent = request.agent(app);
await agent.post("/auth/signup").send(buildSignupPayload());
const tomate = await prisma.ingredient.findFirstOrThrow({ where: { name: "Tomate" } });
const oignon = await prisma.ingredient.findFirstOrThrow({ where: { name: "Oignon" } });
const tomate = await prisma.ingredient.findFirstOrThrow({
where: { key: getEnglishKey("Tomate") },
});
const oignon = await prisma.ingredient.findFirstOrThrow({
where: { key: getEnglishKey("Oignon") },
});
const initial = await agent.get("/profile/disliked-ingredients");
expect(initial.body).to.deep.equal([]);
@ -159,8 +166,12 @@ describe("Profile", () => {
it("replaces (not merges) the previous selection", async () => {
const agent = request.agent(app);
await agent.post("/auth/signup").send(buildSignupPayload());
const tomate = await prisma.ingredient.findFirstOrThrow({ where: { name: "Tomate" } });
const oignon = await prisma.ingredient.findFirstOrThrow({ where: { name: "Oignon" } });
const tomate = await prisma.ingredient.findFirstOrThrow({
where: { key: getEnglishKey("Tomate") },
});
const oignon = await prisma.ingredient.findFirstOrThrow({
where: { key: getEnglishKey("Oignon") },
});
await agent
.patch("/profile/disliked-ingredients")

View file

@ -4,6 +4,7 @@ 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";
@ -19,9 +20,11 @@ function buildSignupPayload(): SignupInput {
};
}
/** Resolves a reference ingredient's id by its `reference-seed-data.ts` name. */
/** Resolves a reference ingredient's id by its `reference-seed-data.ts` French name (slugified to match its `key`). */
async function ingredientId(name: string): Promise<number> {
const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { name } });
const ingredient = await prisma.ingredient.findFirstOrThrow({
where: { key: getEnglishKey(name) },
});
return ingredient.id;
}
@ -163,7 +166,9 @@ describe("Recipes", () => {
const { agent } = await signup();
const tomate = await ingredientId("Tomate");
const oeuf = await ingredientId("Œuf");
const vegetarien = await prisma.diet.findFirstOrThrow({ where: { name: "Végétarien" } });
const vegetarien = await prisma.diet.findFirstOrThrow({
where: { key: getEnglishKey("Végétarien") },
});
const res = await agent.post("/recipes").send({
name: "Omelette provençale",
@ -183,8 +188,12 @@ 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: { name: string }) => a.name)).to.include("Œufs");
expect(res.body.diets.map((d: { name: string }) => d.name)).to.deep.equal(["Végétarien"]);
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"),
]);
});
it("defaults to PERSONAL visibility, and stamps the author's current household", async () => {
@ -279,7 +288,7 @@ describe("Recipes", () => {
expect(res.status).to.equal(200);
expect(res.body.name).to.equal("Salade");
expect(res.body.ingredients[0].ingredient.name).to.equal("Tomate");
expect(res.body.ingredients[0].ingredient.key).to.equal(getEnglishKey("Tomate"));
expect(res.body.isFavorite).to.equal(false);
});
@ -364,7 +373,7 @@ 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.name).to.equal("Oignon");
expect(res.body.ingredients[0].ingredient.key).to.equal(getEnglishKey("Oignon"));
expect(res.body.steps).to.have.length(2);
});

View file

@ -1,6 +1,7 @@
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";
@ -21,28 +22,29 @@ describe("Reference data", () => {
expect(res.status).to.equal(200);
expect(res.body).to.have.length(5);
expect(res.body.map((d: { name: string }) => d.name)).to.include("Végétarien");
expect(res.body[0]).to.have.keys(["id", "name"]);
expect(res.body.map((d: { key: string }) => d.key)).to.include(getEnglishKey("Végétarien"));
expect(res.body[0]).to.have.keys(["id", "key"]);
});
});
describe("GET /reference/allergies", () => {
it("returns the seeded allergens with their name resolved, no session required", async () => {
it("returns the seeded allergens with their key resolved, no session required", async () => {
const res = await request(app).get("/reference/allergies");
expect(res.status).to.equal(200);
expect(res.body).to.have.length(14);
expect(res.body.map((a: { name: string }) => a.name)).to.include("Arachides");
expect(res.body[0]).to.have.keys(["id", "name", "kind"]);
expect(res.body.map((a: { key: string }) => a.key)).to.include(getEnglishKey("Arachides"));
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 byName = (name: string) => res.body.find((a: { name: string }) => a.name === name);
expect(byName("Gluten").kind).to.equal("INTOLERANCE");
expect(byName("Sulfites").kind).to.equal("INTOLERANCE");
expect(byName("Arachides").kind).to.equal("ALLERGY");
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");
expect(res.body.filter((a: { kind: string }) => a.kind === "INTOLERANCE")).to.have.length(2);
});
});
@ -53,10 +55,10 @@ describe("Reference data", () => {
expect(res.status).to.equal(200);
expect(res.body.length).to.be.greaterThan(0);
expect(res.body.map((i: { name: string }) => i.name)).to.include("Tomate");
expect(res.body.map((i: { key: string }) => i.key)).to.include(getEnglishKey("Tomate"));
expect(res.body[0]).to.have.keys([
"id",
"name",
"key",
"icon",
"category",
"subcategory",
@ -68,9 +70,12 @@ 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 byName = (name: string) => res.body.find((i: { name: string }) => i.name === name);
expect(byName("Œuf").allergens.map((a: { name: string }) => a.name)).to.include("Œufs");
expect(byName("Tomate").allergens).to.deep.equal([]);
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([]);
});
});
});

View file

@ -32,8 +32,8 @@ describe("Onboarding wizard (regime → foyer → allergens)", () => {
cy.intercept("GET", "**/reference/diets", {
statusCode: 200,
body: [
{ id: 1, name: "Omnivore" },
{ id: 2, name: "Végétarien" },
{ id: 1, key: "omnivore" },
{ id: 2, key: "vegetarian" },
],
});
cy.intercept("PATCH", "**/profile/diet", {
@ -48,8 +48,8 @@ describe("Onboarding wizard (regime → foyer → allergens)", () => {
cy.intercept("GET", "**/reference/allergies", {
statusCode: 200,
body: [
{ id: 1, name: "Arachides", kind: "ALLERGY" },
{ id: 2, name: "Gluten", kind: "INTOLERANCE" },
{ id: 1, key: "peanuts", kind: "ALLERGY" },
{ id: 2, key: "gluten", kind: "INTOLERANCE" },
],
});
cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [1] }).as(

View file

@ -16,15 +16,15 @@ describe("Dietary preferences (/parametres/preferences) — hot saving", () => {
cy.intercept("GET", "**/reference/diets", {
statusCode: 200,
body: [
{ id: 1, name: "Omnivore" },
{ id: 2, name: "Végétarien" },
{ id: 1, key: "omnivore" },
{ id: 2, key: "vegetarian" },
],
});
cy.intercept("GET", "**/reference/allergies", {
statusCode: 200,
body: [
{ id: 1, name: "Arachides", kind: "ALLERGY" },
{ id: 2, name: "Gluten", kind: "INTOLERANCE" },
{ id: 1, key: "peanuts", kind: "ALLERGY" },
{ id: 2, key: "gluten", kind: "INTOLERANCE" },
],
});
cy.intercept("GET", "**/profile/allergies", { statusCode: 200, body: [2] });

View file

@ -0,0 +1,178 @@
// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no
// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover
// real API behavior against a real database).
const authenticatedProfile = {
id: 1,
firstName: "Alice",
lastName: "Martin",
email: "alice@example.com",
tokenVersion: 0,
houseId: 1,
dietId: null,
};
const tomato = {
id: 1,
key: "tomato",
icon: "VEGETABLE",
category: "PRODUITS_FRAIS",
subcategory: "LEGUMES",
allergens: [],
diets: [{ id: 2, key: "vegetarian" }],
};
const egg = {
id: 2,
key: "egg",
icon: "EGG",
category: "CREMERIE_FROMAGE",
subcategory: "OEUFS",
allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }],
diets: [],
};
const carrot = {
id: 3,
key: "carrot",
icon: "VEGETABLE",
category: "PRODUITS_FRAIS",
subcategory: "LEGUMES",
allergens: [],
diets: [{ id: 2, key: "vegetarian" }],
};
const diets = [
{ id: 1, key: "omnivore" },
{ id: 2, key: "vegetarian" },
];
function interceptCatalog() {
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
cy.intercept("GET", "**/reference/ingredients", {
statusCode: 200,
body: [tomato, egg, carrot],
});
cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: diets });
}
describe("Recipe form — associating ingredients", () => {
beforeEach(() => {
interceptCatalog();
});
it("adds an ingredient from the picker, fills its quantity/unit, and creates the recipe", () => {
cy.intercept("POST", "**/recipes", { statusCode: 201, body: { id: 42 } }).as("createRecipe");
cy.visit("/recettes/nouvelle");
cy.get("#recipe-name").type("Salade de tomates");
cy.get("input[placeholder='Rechercher un ingrédient…']").type("tomat");
cy.contains(".ingredient-picker__card", "Tomate").click();
// The card disappears from the picker once selected (excludeIds), and
// a row for it appears in the recipe's own ingredient list.
cy.contains(".ingredient-picker__card", "Tomate").should("not.exist");
cy.contains(".ingredient-row__name", "Tomate").should("be.visible");
cy.get(".ingredient-row .ingredient-row__quantity").type("3");
cy.get(".ingredient-row .ingredient-row__unit").type("unité");
cy.contains("button", "Ajouter une étape").click();
cy.get(".step-list-editor__item textarea").type("Couper les tomates.");
cy.contains("button", "Enregistrer").should("not.be.disabled").click();
cy.wait("@createRecipe")
.its("request.body")
.should("deep.include", {
name: "Salade de tomates",
ingredients: [{ ingredientId: 1, quantity: 3, unit: "unité" }],
});
cy.url().should("include", "/recettes/42");
});
// Regression test for the exact bug reported: `crypto.randomUUID()` (used
// to mint each ingredient/step draft's client-only React key) throws
// outside a secure context — https, or literally the hostname
// `localhost` — so a LAN IP during on-device testing or a Capacitor
// WebView's `capacitor://` origin hit a black screen with "TypeError:
// crypto.randomUUID is not a function" the instant an ingredient was
// added. Cypress's own origin is secure, so this forces the same failure
// by deleting `crypto.randomUUID` before the app boots — see
// `apps/web/src/lib/client-key.ts`, which replaced it.
it("still works when crypto.randomUUID is unavailable (insecure-context regression)", () => {
cy.visit("/recettes/nouvelle", {
onBeforeLoad(win) {
Object.defineProperty(win.crypto, "randomUUID", {
value: undefined,
configurable: true,
});
},
});
cy.get("#recipe-name").type("Recette hors contexte sécurisé");
cy.contains(".ingredient-picker__card", "Tomate").click();
cy.contains(".ingredient-picker__card", "Œuf").click();
// Both rows rendered with distinct identities — no crash, no React
// "same key" warning silently collapsing one of them.
cy.get(".ingredient-row").should("have.length", 2);
cy.contains(".ingredient-row__name", "Tomate").should("be.visible");
cy.contains(".ingredient-row__name", "Œuf").should("be.visible");
cy.contains("button", "Ajouter une étape").click();
cy.contains("button", "Ajouter une étape").click();
cy.get(".step-list-editor__item").should("have.length", 2);
});
it("excludes an already-selected ingredient from the picker, and removing it brings it back", () => {
cy.visit("/recettes/nouvelle");
cy.contains(".ingredient-picker__card", "Carotte").click();
cy.contains(".ingredient-picker__card", "Carotte").should("not.exist");
cy.contains(".ingredient-row", "Carotte")
.find("button[title='Retirer cet ingrédient']")
.click();
cy.contains(".ingredient-picker__card", "Carotte").should("be.visible");
cy.get(".ingredient-row").should("have.length", 0);
});
it("preloads an existing recipe's ingredients when editing, and lets you add another", () => {
const existingRecipe = {
id: 7,
name: "Omelette",
description: null,
picture: null,
authorId: 1,
visibility: "PERSONAL",
allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }],
diets: [],
isFavorite: false,
ingredients: [{ ingredient: egg, quantity: 3, unit: "unité" }],
steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 0 }],
};
cy.intercept("GET", "**/recipes/7", { statusCode: 200, body: existingRecipe });
cy.intercept("PATCH", "**/recipes/7", { statusCode: 200, body: { id: 7 } }).as("updateRecipe");
cy.visit("/recettes/7/modifier");
cy.contains(".ingredient-row__name", "Œuf").should("be.visible");
cy.get(".ingredient-row .ingredient-row__quantity").should("have.value", "3");
cy.contains(".ingredient-picker__card", "Tomate").click();
cy.get(".ingredient-row").should("have.length", 2);
cy.get(".ingredient-row .ingredient-row__quantity").last().type("1");
cy.get(".ingredient-row .ingredient-row__unit").last().type("unité");
cy.contains("button", "Enregistrer").click();
cy.wait("@updateRecipe")
.its("request.body.ingredients")
.should("deep.equal", [
{ ingredientId: 2, quantity: 3, unit: "unité" },
{ ingredientId: 1, quantity: 1, unit: "unité" },
]);
});
});

View file

@ -0,0 +1,241 @@
// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no
// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover
// real API behavior against a real database).
const authenticatedProfile = {
id: 1,
firstName: "Alice",
lastName: "Martin",
email: "alice@example.com",
tokenVersion: 0,
houseId: 1,
dietId: null,
};
const vegetarien = { id: 1, key: "vegetarian" };
const gluten = { id: 1, key: "gluten", kind: "INTOLERANCE" };
const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" };
const ratatouille = {
id: 1,
name: "Ratatouille",
description: null,
picture: null,
authorId: 1,
visibility: "PERSONAL",
allergens: [],
diets: [vegetarien],
isFavorite: true,
};
const omelette = {
id: 2,
name: "Omelette",
description: null,
picture: null,
authorId: 1,
visibility: "PERSONAL",
allergens: [oeufs],
diets: [],
isFavorite: false,
};
const omeletteDetail = {
...omelette,
description: "Une omelette toute simple.",
ingredients: [
{
ingredient: {
id: 10,
key: "egg",
icon: "EGG",
category: "CREMERIE_FROMAGE",
subcategory: "OEUFS",
allergens: [oeufs],
diets: [],
},
quantity: 3,
unit: "unité",
},
],
steps: [
{ id: 1, description: "Battre les œufs.", picture: null, order: 1 },
{ id: 2, description: "Cuire à la poêle.", picture: null, order: 2 },
],
};
function interceptAuth() {
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] });
}
describe("Recipe catalog", () => {
beforeEach(() => {
interceptAuth();
});
it("defaults to the Favoris tab and lists its recipes", () => {
cy.intercept("GET", /\/recipes\?/, (req) => {
expect(req.url).to.include("tab=favoris");
req.reply({ statusCode: 200, body: [ratatouille] });
}).as("listRecipes");
cy.visit("/recettes");
cy.wait("@listRecipes");
cy.contains("h1", "Recettes").should("be.visible");
cy.get(".recipe-tabs__tab.active").should("contain.text", "Favoris");
cy.contains(".recipe-table__name", "Ratatouille").should("be.visible");
// The favorited row carries the ★ fav-mark.
cy.contains(".recipe-table__name", "Ratatouille")
.find(".recipe-table__fav-mark")
.should("exist");
cy.contains(".recipe-table__name", "Ratatouille")
.parents("tr")
.find(".diet-badge")
.should("contain.text", "Végétarien");
});
it("shows the empty state when a tab has no recipes", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
cy.visit("/recettes");
cy.contains("Aucune recette pour le moment.").should("be.visible");
});
it("shows an error state when the catalog fails to load", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 500, body: { code: 5000, message: "boom" } });
cy.visit("/recettes");
cy.contains(".recipes-page__status--error", "Impossible de charger").should("be.visible");
});
it("switches tabs, re-fetching each one's own recipes", () => {
cy.intercept("GET", /\/recipes\?/, (req) => {
const tab = new URL(req.url).searchParams.get("tab");
const body = tab === "perso" ? [omelette] : [ratatouille];
req.reply({ statusCode: 200, body });
}).as("listRecipes");
cy.visit("/recettes");
cy.wait("@listRecipes");
cy.contains(".recipe-table__name", "Ratatouille").should("be.visible");
// Not asserting the specific request URL here — React StrictMode (see
// main.tsx) double-invokes mount/update effects in dev, so this can
// legitimately fire twice; the rendered result converges either way
// (same reasoning as planning-page.cy.ts's week-navigation tests).
cy.contains(".recipe-tabs__tab", "Perso").click();
cy.get(".recipe-tabs__tab.active").should("contain.text", "Perso");
cy.contains(".recipe-table__name", "Omelette").should("be.visible");
cy.contains(".recipe-table__name", "Ratatouille").should("not.exist");
// The disabled "Sources (bientôt)" placeholder never becomes active.
cy.contains(".recipe-tabs__tab", "Sources (bientôt)").should("be.disabled");
});
it("searches within the active tab, debounced", () => {
cy.intercept("GET", /\/recipes\?/, (req) => {
const search = new URL(req.url).searchParams.get("search");
req.reply({ statusCode: 200, body: search ? [omelette] : [ratatouille, omelette] });
}).as("listRecipes");
cy.visit("/recettes");
cy.wait("@listRecipes");
cy.contains(".recipe-table__name", "Ratatouille").should("be.visible");
// Same "assert the rendered result, not the request count/URL" reasoning
// as the tab-switch test above — the 300ms debounce plus StrictMode's
// double-invoked effects make the exact number/order of requests an
// implementation detail, not something worth pinning down here.
cy.get(".recipes-page__search").type("Omel");
cy.contains(".recipe-table__name", "Omelette").should("be.visible");
cy.contains(".recipe-table__name", "Ratatouille").should("not.exist");
});
it("opens a recipe's detail alongside the table when its row is selected", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [ratatouille, omelette] });
cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail }).as("getRecipe");
cy.visit("/recettes");
cy.contains(".recipe-table__name", "Omelette").click();
cy.wait("@getRecipe");
cy.url().should("include", "/recettes/2");
// The table stays mounted (master-detail, not a page navigation) —
// both rows are still visible next to the detail panel.
cy.contains(".recipe-table__name", "Ratatouille").should("be.visible");
cy.get("tr.selected .recipe-table__name").should("contain.text", "Omelette");
cy.get(".recipe-detail-panel").within(() => {
cy.contains("h2", "Omelette").should("be.visible");
cy.contains("Une omelette toute simple.").should("be.visible");
cy.contains("Battre les œufs.").should("be.visible");
cy.contains("Cuire à la poêle.").should("be.visible");
});
});
it("shows a not-found message for a selected id the API rejects", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
cy.intercept("GET", "**/recipes/999", {
statusCode: 404,
// ErrorCode.RECIPE_NOT_FOUND (packages/shared/src/errors/error-codes.ts)
// — RecipeDetailPanel only renders the "not found" message for this
// exact code, anything else falls into its generic error state.
body: { code: 4045, message: "not found" },
});
cy.visit("/recettes/999");
cy.contains(".recipe-detail-panel", "Cette recette n'existe pas.").should("be.visible");
});
it("toggles a recipe's favorite from the detail panel and reflects it in the table", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [omelette] });
cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail });
cy.intercept("POST", "**/recipes/2/favorite", { statusCode: 204 }).as("favorite");
cy.visit("/recettes/2");
cy.contains(".recipe-table__name", "Omelette")
.find(".recipe-table__fav-mark")
.should("not.exist");
cy.get(".favorite-star-button").click();
cy.wait("@favorite");
cy.get(".favorite-star-button").should("have.class", "is-favorite");
cy.contains(".recipe-table__name", "Omelette").find(".recipe-table__fav-mark").should("exist");
});
it("deletes a recipe after a two-step confirmation, then clears the selection", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [omelette] });
cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail });
cy.intercept("DELETE", "**/recipes/2", { statusCode: 204 }).as("deleteRecipe");
cy.visit("/recettes/2");
cy.contains(".recipe-detail-panel", "Omelette").should("be.visible");
cy.contains(".recipe-detail-panel__danger-button", "Supprimer").click();
cy.contains(".recipe-detail-panel__danger-button", "Confirmer la suppression").click();
cy.wait("@deleteRecipe");
cy.url().should("match", /\/recettes\/?$/);
cy.contains(".recipe-table__name", "Omelette").should("not.exist");
cy.contains("Sélectionnez une recette dans le tableau").should("be.visible");
});
it("links the new-recipe button to the recipe form", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
cy.visit("/recettes");
cy.contains(".recipes-page__new-button", "Nouvelle recette").should(
"have.attr",
"href",
"/recettes/nouvelle",
);
});
});

View file

@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.0.0",
"version": "0.2.0",
"private": true,
"type": "module",
"scripts": {
@ -16,6 +16,7 @@
"@batch-cooking/date-tools": "workspace:*",
"@batch-cooking/shared": "workspace:*",
"i18next": "^26.3.6",
"lucide-react": "^1.32.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^17.0.11",

View file

@ -12,6 +12,7 @@ import { OnboardingAllergensPage } from "./pages/onboarding/OnboardingAllergensP
import { OnboardingDietPage } from "./pages/onboarding/OnboardingDietPage";
import { OnboardingHouseholdPage } from "./pages/onboarding/OnboardingHouseholdPage";
import { AccountSettingsPage } from "./pages/settings/AccountSettingsPage";
import { CreditsPage } from "./pages/settings/CreditsPage";
import { HouseholdSettingsPage } from "./pages/settings/HouseholdSettingsPage";
import { PreferencesPage } from "./pages/settings/PreferencesPage";
import { UserPreferencesPage } from "./pages/settings/UserPreferencesPage";
@ -25,8 +26,8 @@ import { UserPreferencesPage } from "./pages/settings/UserPreferencesPage";
* itself redirects to `/login` if needed.
*
* `/parametres/*` (compte/préférences alimentaires/foyer/préférences
* utilisateur) are the settings pages, reachable from the sidebar's bottom
* "Paramètres" menu and the account menu (see `AppLayout`) nested under
* utilisateur/crédits) are the settings pages, reachable from the sidebar's
* bottom "Paramètres" menu and the account menu (see `AppLayout`) nested under
* `AppLayout` like every other authenticated section. `/foyer` is the old,
* pre-split combined page's path; it now just redirects to
* `/parametres/foyer` so an existing bookmark/link keeps working.
@ -61,6 +62,7 @@ export function App() {
<Route path="/parametres/preferences" element={<PreferencesPage />} />
<Route path="/parametres/foyer" element={<HouseholdSettingsPage />} />
<Route path="/parametres/preferences-utilisateur" element={<UserPreferencesPage />} />
<Route path="/parametres/credits" element={<CreditsPage />} />
<Route path="/foyer" element={<Navigate to="/parametres/foyer" replace />} />
</Route>
<Route

View file

@ -0,0 +1,41 @@
import type { ReactNode } from "react";
/**
* The app-wide "selectable card" checkbox see `global.scss`'s
* `label:has(> input[type="checkbox"])` rule for the actual look (hidden
* native input, a `.check-mark` that scales in, `is-selected` driving the
* tinted/bordered state). Factors out the JSX triplet (`label` hidden
* `input` `span.check-mark` label text) that used to be duplicated
* across `AllergySelect`, `DietTagSelect`, `IngredientPicker`'s display
* menu, and `UserPreferencesPage`'s theme picker (see {@link RadioOption}
* for its `type="radio"` sibling) one place to get the markup/a11y right
* instead of four.
*
* `is-selected` is applied in JS from the same `checked` boolean the caller
* already has, not derived via a CSS `:has(:checked)` chain that turned
* out unreliable across browsers (see the callers this replaces for the
* original note).
*
* `className` is for the *container* layout only (grid item, flex-wrap
* chip, stacked list) the control's own look never varies, so there's
* no `variant` prop here.
*/
export function CheckboxOption({
checked,
onChange,
children,
className,
}: {
checked: boolean;
onChange: (checked: boolean) => void;
children: ReactNode;
className?: string;
}) {
return (
<label className={[className, checked && "is-selected"].filter(Boolean).join(" ")}>
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
<span className="check-mark" aria-hidden="true" />
{children}
</label>
);
}

View file

@ -0,0 +1,38 @@
import type { ReactNode } from "react";
/**
* The `type="radio"` sibling of {@link CheckboxOption} same "selectable
* card" markup/look (see `global.scss`'s `label:has(> input[...])` rule,
* shared by both), just a native radio input under the hood so a group of
* `RadioOption`s sharing `name` behaves as mutually exclusive (see
* `UserPreferencesPage`'s theme picker, the one caller so far).
*/
export function RadioOption<T extends string>({
name,
value,
checked,
onChange,
children,
className,
}: {
name: string;
value: T;
checked: boolean;
onChange: (value: T) => void;
children: ReactNode;
className?: string;
}) {
return (
<label className={[className, checked && "is-selected"].filter(Boolean).join(" ")}>
<input
type="radio"
name={name}
value={value}
checked={checked}
onChange={() => onChange(value)}
/>
<span className="check-mark" aria-hidden="true" />
{children}
</label>
);
}

View file

@ -1,4 +1,6 @@
import type { AllergyView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import { CheckboxOption } from "../../components/ui/Checkbox";
import "./profile-forms.scss";
interface AllergySelectProps {
@ -28,6 +30,8 @@ interface AllergySelectProps {
* rationale as `DietSelect`.
*/
export function AllergySelect({ legend, allergies, value, onChange }: AllergySelectProps) {
const { t } = useTranslation();
function toggle(id: number) {
onChange(value.includes(id) ? value.filter((existing) => existing !== id) : [...value, id]);
}
@ -38,19 +42,14 @@ export function AllergySelect({ legend, allergies, value, onChange }: AllergySel
{allergies.map((allergy) => {
const checked = value.includes(allergy.id);
return (
<label
<CheckboxOption
key={allergy.id}
// `is-selected` (not a `:has(:checked)` CSS rule) drives the
// selected look — chaining `:has(...):has(:checked)` to react to
// a *state* change (rather than a DOM mutation) turned out to be
// unreliable, so this stays a plain, always-correct React class
// instead of relying on CSS to derive it.
className={`allergy-select__option${checked ? " is-selected" : ""}`}
checked={checked}
onChange={() => toggle(allergy.id)}
className="allergy-select__option"
>
<input type="checkbox" checked={checked} onChange={() => toggle(allergy.id)} />
<span className="check-mark" aria-hidden="true" />
{allergy.name}
</label>
{t(`catalog.allergens.${allergy.key}`)}
</CheckboxOption>
);
})}
</fieldset>

View file

@ -33,7 +33,7 @@ export function DietSelect({ diets, value, onChange }: DietSelectProps) {
<option value="">{t("preferences.form.dietNone")}</option>
{diets.map((diet) => (
<option key={diet.id} value={diet.id}>
{diet.name}
{t(`catalog.diets.${diet.key}`)}
</option>
))}
</select>

View file

@ -54,7 +54,7 @@ export function DislikedIngredientsField({
<span aria-hidden="true">
<IngredientTypeIcon icon={ingredient.icon} />
</span>
{ingredient.name}
{t(`catalog.ingredients.${ingredient.key}`)}
<button
type="button"
onClick={() => remove(ingredient.id)}

View file

@ -1,4 +1,5 @@
import type { AllergyView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import "./recipes.scss";
/**
@ -10,6 +11,7 @@ import "./recipes.scss";
* mount it unconditionally.
*/
export function AllergenBadges({ allergens }: { allergens: AllergyView[] }) {
const { t } = useTranslation();
if (allergens.length === 0) {
return null;
}
@ -17,7 +19,7 @@ export function AllergenBadges({ allergens }: { allergens: AllergyView[] }) {
<ul className="allergen-badges">
{allergens.map((allergen) => (
<li key={allergen.id} className="allergen-badge">
{allergen.name}
{t(`catalog.allergens.${allergen.key}`)}
</li>
))}
</ul>

View file

@ -1,4 +1,5 @@
import type { DietView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import "./recipes.scss";
/**
@ -10,6 +11,7 @@ import "./recipes.scss";
* Renders nothing for an empty list, same convention as `AllergenBadges`.
*/
export function DietBadges({ diets }: { diets: DietView[] }) {
const { t } = useTranslation();
if (diets.length === 0) {
return null;
}
@ -17,7 +19,7 @@ export function DietBadges({ diets }: { diets: DietView[] }) {
<ul className="diet-badges">
{diets.map((diet) => (
<li key={diet.id} className="diet-badge">
{diet.name}
{t(`catalog.diets.${diet.key}`)}
</li>
))}
</ul>

View file

@ -1,5 +1,6 @@
import type { DietView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import { CheckboxOption } from "../../components/ui/Checkbox";
import "./recipes.scss";
/**
@ -29,11 +30,9 @@ export function DietTagSelect({
{diets.map((diet) => {
const checked = value.includes(diet.id);
return (
<label key={diet.id} className={checked ? "is-selected" : undefined}>
<input type="checkbox" checked={checked} onChange={() => toggle(diet.id)} />
<span className="check-mark" aria-hidden="true" />
{diet.name}
</label>
<CheckboxOption key={diet.id} checked={checked} onChange={() => toggle(diet.id)}>
{t(`catalog.diets.${diet.key}`)}
</CheckboxOption>
);
})}
</fieldset>

View file

@ -52,7 +52,7 @@ export function FavoriteStarButton({
disabled={isSaving}
title={t(isFavorite ? "recipes.detail.unfavorite" : "recipes.detail.favorite")}
>
<FavoriteIcon />
<FavoriteIcon aria-hidden="true" />
</button>
);
}

View file

@ -7,6 +7,7 @@ import {
} from "@batch-cooking/shared";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { CheckboxOption } from "../../components/ui/Checkbox";
import { SettingsIcon } from "../../layouts/nav-icons";
import { AllergenBadges } from "./AllergenBadges";
import { DietBadges } from "./DietBadges";
@ -73,8 +74,12 @@ export function IngredientPicker({
if (ingredient.category !== category) return false;
if (subcategory !== ALL && ingredient.subcategory !== subcategory) return false;
}
if (normalizedQuery.length > 0 && !ingredient.name.toLowerCase().includes(normalizedQuery)) {
return false;
// Matches against the *displayed* (translated) label, not the raw slug
// key — searching "œuf" should find "Œuf" the way it always has, not
// require typing its key.
if (normalizedQuery.length > 0) {
const label = t(`catalog.ingredients.${ingredient.key}`).toLowerCase();
if (!label.includes(normalizedQuery)) return false;
}
return true;
});
@ -101,28 +106,16 @@ export function IngredientPicker({
aria-expanded={isDisplayMenuOpen}
title={t("recipes.form.displayOptions")}
>
<SettingsIcon />
<SettingsIcon aria-hidden="true" />
</button>
{isDisplayMenuOpen && (
<div className="ingredient-picker__display-menu">
<label className={showDiets ? "is-selected" : ""}>
<input
type="checkbox"
checked={showDiets}
onChange={(e) => setShowDiets(e.target.checked)}
/>
<span className="check-mark" aria-hidden="true" />
<CheckboxOption checked={showDiets} onChange={setShowDiets}>
{t("recipes.form.showDietsLabel")}
</label>
<label className={showAllergens ? "is-selected" : ""}>
<input
type="checkbox"
checked={showAllergens}
onChange={(e) => setShowAllergens(e.target.checked)}
/>
<span className="check-mark" aria-hidden="true" />
</CheckboxOption>
<CheckboxOption checked={showAllergens} onChange={setShowAllergens}>
{t("recipes.form.showAllergensLabel")}
</label>
</CheckboxOption>
</div>
)}
</div>
@ -186,7 +179,9 @@ export function IngredientPicker({
<span className="ingredient-picker__card-icon" aria-hidden="true">
<IngredientTypeIcon icon={ingredient.icon} />
</span>
<span className="ingredient-picker__card-name">{ingredient.name}</span>
<span className="ingredient-picker__card-name">
{t(`catalog.ingredients.${ingredient.key}`)}
</span>
{showAllergens && <AllergenBadges allergens={ingredient.allergens} />}
{showDiets && <DietBadges diets={ingredient.diets} />}
</button>

View file

@ -28,7 +28,7 @@ export function IngredientRow({
<span className="ingredient-row__icon" aria-hidden="true">
<IngredientTypeIcon icon={ingredient.icon} />
</span>
<span className="ingredient-row__name">{ingredient.name}</span>
<span className="ingredient-row__name">{t(`catalog.ingredients.${ingredient.key}`)}</span>
<input
type="number"
min="0"

View file

@ -97,7 +97,7 @@ export function RecipeDetailPanel({
<ul className="disliked-badges">
{dislikedIngredients.map((ingredient) => (
<li key={ingredient.id} className="disliked-badge">
🚫 {ingredient.name}
🚫 {t(`catalog.ingredients.${ingredient.key}`)}
</li>
))}
</ul>

View file

@ -1,10 +1,11 @@
import type { RecipeTab } from "@batch-cooking/shared";
import type { LucideIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { AccountIcon, FavoriteIcon, HouseholdIcon, PublicIcon } from "../../layouts/nav-icons";
import "./recipes.scss";
/** Every functional tab, in display order, with its icon — reuses `AccountIcon`/`HouseholdIcon` from the sidebar's own icon set (see nav-icons.tsx) rather than a second "person"/"house" glyph. */
const TABS: Array<{ value: RecipeTab; Icon: () => JSX.Element }> = [
const TABS: Array<{ value: RecipeTab; Icon: LucideIcon }> = [
{ value: "favoris", Icon: FavoriteIcon },
{ value: "perso", Icon: AccountIcon },
{ value: "foyer", Icon: HouseholdIcon },
@ -37,7 +38,7 @@ export function RecipeTabs({
className={`recipe-tabs__tab${value === active ? " active" : ""}`}
onClick={() => onChange(value)}
>
<Icon />
<Icon aria-hidden="true" />
{t(`recipes.tabs.${value}`)}
</button>
))}

View file

@ -1,4 +1,5 @@
import { useTranslation } from "react-i18next";
import { makeClientKey } from "../../lib/client-key";
import "./recipes.scss";
/** One in-progress preparation step in the recipe form. `key` is a client-only stable identity for React/reordering — the server derives the real `order`/`id` from array position on save (see `schemas/recipe.ts`), never from this. */
@ -24,7 +25,7 @@ export function StepListEditor({
const { t } = useTranslation();
function addStep() {
onChange([...steps, { key: crypto.randomUUID(), description: "", picture: "" }]);
onChange([...steps, { key: makeClientKey(), description: "", picture: "" }]);
}
function updateStep(key: string, patch: Partial<Pick<StepDraft, "description" | "picture">>) {

View file

@ -9,13 +9,20 @@ import type { ReactNode } from "react";
// (schema.prisma), reused across the category chips, subcategory chips and
// ingredient cards in `IngredientPicker`/`IngredientRow`. Replaces the
// earlier per-ingredient emoji (437 different characters, one per
// ingredient) with a small, hand-drawn vocabulary grouped by *what kind of
// thing* an ingredient is (a vegetable, a bottle of oil, a wedge of
// cheese…) rather than a literal picture of each one — 437 unique,
// professional icons isn't a realistic hand-drawn set, but ~20 generic
// shapes reused across them is. Same 24×24, stroke-only house style as
// `layouts/nav-icons.tsx` (kept separate from that file since these are
// ingredient-domain, not app-nav).
// ingredient) with a small vocabulary grouped by *what kind of thing* an
// ingredient is (a vegetable, a bottle of oil, a wedge of cheese…) rather
// than a literal picture of each one — 437 unique, professional icons
// isn't a realistic set to draw or license one-by-one, but ~20 generic
// shapes reused across them is.
//
// Most of these (everything using `FilledIcon` below) are solid-fill
// glyphs from foodiconpack.com's free "Common ingredient icons"/"Common
// Utensils" collections, licensed CC BY 4.0 — see the in-app credits page
// (`pages/CreditsPage.tsx`) for the required attribution. The three with
// no good match in that pack (`BreadIcon`, `DoughIcon`, `SproutIcon`) stay
// hand-drawn, using the stroke-based `Icon` wrapper that used to cover all
// 22 — both wrappers render at the same on-screen size (sized entirely via
// CSS, no width/height baked into either), so the mix reads as one set.
function Icon({ children }: { children: ReactNode }) {
return (
<svg
@ -32,111 +39,138 @@ function Icon({ children }: { children: ReactNode }) {
);
}
/** Carrot — root vegetables, leafy greens, and produce generally (`PRODUITS_FRAIS`/`LEGUMES`). */
/** Wrapper for the foodiconpack.com icons — solid-fill glyphs (2048×2048 source viewBox) rather than `Icon`'s stroke-only ones. `fill="currentColor"` on the root cascades to every child `<path>` (none set their own), so these theme/recolor exactly like the hand-drawn set. */
function FilledIcon({ children }: { children: ReactNode }) {
return (
<svg viewBox="0 0 2048 2048" fill="currentColor" aria-hidden="true">
{children}
</svg>
);
}
/** Carrot (foodiconpack.com, CC BY 4.0 — see the credits page) — root vegetables, leafy greens, and produce generally (`PRODUITS_FRAIS`/`LEGUMES`). */
export function VegetableIcon() {
return (
<Icon>
<path d="M12 3c1.5 1 2 2.5 1.3 4M15 2c1 1.2 1 2.6 0 4" />
<path d="M13.3 7c2.8 0 4.7 2.9 4.1 5.6L15.8 20a2 2 0 0 1-3.9.2L9.2 12.6C8.3 9.9 10.3 7 13.3 7z" />
</Icon>
<FilledIcon>
<path d="M 1372.78 269.269 C 1451.37 259.868 1460.99 372.996 1461.44 425.661 C 1581.16 368.032 1695.37 431.416 1620.86 576.71 C 1685.93 569.075 1808.81 603.967 1777.39 694.986 C 1758.86 748.652 1687.55 747.579 1641.06 748.465 C 1580.54 749.618 1520.86 763.068 1465.02 788.033 C 1534.06 891.041 1509.32 1016.25 1431.63 1106.97 C 1379.12 1166.91 1315.36 1215.45 1253.34 1264.87 C 1058.07 1420.5 847.167 1562.19 628.521 1682.86 C 575.245 1711.66 526.145 1740.43 468.866 1762.97 C 390.259 1795.21 302.898 1750 274.227 1672.21 C 244.804 1591.18 312.531 1477.87 352.017 1402.88 C 476.741 1166.04 615.662 937.768 779.446 725.68 C 906.883 560.659 1021.56 455.144 1235.99 558.243 C 1264.92 502.026 1277.99 486.974 1284.17 416.13 C 1287.17 381.847 1282.64 350.416 1304.56 321.09 C 1322.93 296.516 1341.19 275.069 1372.78 269.269 z M 950.76 616.439 L 1013.63 678.467 C 1029.66 694.066 1071.68 724.095 1046.31 747.333 C 1024.75 767.082 1004.77 741.305 990.319 727.133 C 964.593 701.906 939.427 675.525 913.717 650.33 C 880.748 676.906 827.077 745.279 800.804 781.139 L 854.416 832.616 C 865.235 843.052 882.868 854.945 884.017 870.127 C 885.534 890.17 863.677 905.592 846.353 893.305 C 828.849 880.245 786.026 838.832 771.219 820.954 C 714.393 891.651 650.945 992.221 601.996 1070.21 C 624.959 1095 681.374 1144.78 697.215 1170.14 C 708.227 1187.77 690.155 1203.7 673.807 1207.19 C 653.583 1201.54 592.75 1131.47 575.668 1114.3 C 559.969 1135.77 548.023 1159.96 533.882 1182.5 C 510.398 1219.92 485.868 1260.5 465.325 1299.77 C 502.429 1335.29 536.293 1374.32 572.651 1410.85 C 616.262 1454.67 564.989 1469.73 552.538 1459.88 C 545.792 1454.54 539.672 1447.84 533.725 1441.61 C 507.768 1414.38 462.092 1372.67 441.283 1344.75 C 406.237 1391.7 337.706 1542.08 320.289 1594.46 C 307.509 1635.07 326.359 1680.01 360.377 1704.01 C 404.141 1734.89 437.219 1724.27 480.998 1702.92 C 588.332 1650.35 692.162 1590.27 793.354 1526.86 C 814.319 1513.73 835.284 1498.97 857.352 1487.95 C 835.987 1470.56 796.488 1430.13 780.352 1410.17 C 763.323 1389.11 795.041 1355.92 821.643 1382.07 C 847.186 1408.16 873.398 1433.7 899.028 1459.69 C 963.823 1420.72 1057.58 1356.01 1116.07 1307.2 C 1077.11 1269.13 1037.68 1230.01 1000.71 1190.1 C 982.941 1170.92 1012.3 1138.95 1033.56 1154.94 C 1048.09 1165.87 1062.35 1182.66 1075.26 1195.74 L 1155.51 1275.86 C 1166.47 1265.89 1185.51 1254.03 1198.05 1244 L 1260.92 1194.32 C 1274.19 1183.64 1289.57 1170.23 1302.77 1160.32 C 1278.8 1138.72 1256.47 1114.14 1233.14 1092.69 C 1217.61 1078.42 1202.94 1062.56 1221.87 1044.16 C 1249.46 1017.35 1315.96 1106.48 1340.25 1126.29 C 1444.56 1048.18 1496.33 911.829 1413.1 796.434 C 1406.16 786.802 1391.42 772.44 1382.72 763.523 L 1275.05 654.549 C 1222.53 602.189 1141.05 555.834 1065.3 567.094 C 1021.92 572.869 984.684 588.342 950.76 616.439 z M 1378.48 317.89 C 1330.92 337.14 1337 380.739 1333.96 418.637 C 1328.72 484.036 1311.97 533.086 1277.38 588.051 C 1293.37 600.283 1316.84 625.274 1331.99 640.442 L 1435.43 745.774 C 1500.7 717.893 1548.44 703.336 1620.74 699.414 C 1645.59 698.066 1687.15 701.875 1708.39 694.233 C 1721.56 688.881 1730.42 684.198 1734.14 669.597 C 1725.34 623.524 1643.09 621.963 1607.48 627.006 C 1580.34 630.85 1546.06 647.219 1546.46 602.242 C 1552.37 591.19 1562.16 583.393 1568.67 573.333 C 1587.79 543.812 1600.89 501.106 1592 466.268 C 1482.33 418.046 1449.83 520.39 1414.54 505.673 C 1378.89 490.801 1434.89 402.689 1400.54 340.129 C 1396.73 333.184 1387.45 318.271 1378.48 317.89 z" />
</FilledIcon>
);
}
/** Apple — `PRODUITS_FRAIS`/`FRUITS`. */
/** Apple (foodiconpack.com, CC BY 4.0) — `PRODUITS_FRAIS`/`FRUITS`. */
export function FruitIcon() {
return (
<Icon>
<path d="M12 8c-.5-1.3-.2-3 1-4" />
<path d="M12 8c-4 0-6.5 3-6.5 7a6.5 6.5 0 0 0 13 0C18.5 11 16 8 12 8z" />
</Icon>
<FilledIcon>
<path d="M 639.796 267.013 C 781.434 262.103 956.112 338.089 1022.53 468.256 C 1028.76 480.476 1033.68 492.986 1039.98 505.103 C 1079.28 423.136 1119.59 362.725 1191.86 304.793 C 1207.29 295.22 1235.79 265.497 1254.81 269.055 C 1274.69 272.773 1277.84 292.064 1274.6 308.547 C 1266.47 318.866 1248.2 329.649 1237.12 338.285 C 1131.9 420.35 1060.45 546.672 1048.57 682.02 C 1107.04 669.285 1129.39 657.187 1183.99 637.63 C 1536.21 511.481 1760.44 792.38 1734.89 1120.39 C 1718.81 1354.66 1590.75 1616.21 1387.43 1738.88 C 1311.69 1785.07 1214.98 1792.8 1134.52 1754.06 C 1082.6 1729.07 1073.4 1712.81 1004.38 1718.28 C 917.695 1725.14 884.149 1804.95 750.621 1775.01 C 601.141 1741.49 473.114 1590.49 406.265 1456.56 C 327.693 1299.13 287.874 1104.16 324.793 930.29 C 366.48 733.959 526.268 590.689 731.532 609.409 C 820.954 617.563 903.048 662.498 993.64 682.02 C 995.341 671.368 996.474 657.971 997.837 647 C 980.828 646.119 962.537 647.498 945.443 646.853 C 794.823 641.173 687.284 529.291 639.45 392.227 C 628.619 361.191 596.29 281.598 639.796 267.013 z M 730.753 1713.44 C 832.193 1747.12 856.182 1715.04 934.669 1681.28 C 995.523 1655.1 1082.34 1658.82 1138.75 1694.17 C 1249.82 1763.75 1371.52 1705.24 1453.51 1620.95 C 1636.34 1440.7 1756.94 1086.45 1634.33 845.186 C 1571.31 719.014 1460.98 652.165 1319.22 660.997 C 1247.81 670.321 1204.55 687.291 1138.29 714.935 C 1006.74 769.809 916.081 711.143 793.279 675.575 C 667.76 638.278 533.232 676.049 451.257 780.278 C 245.567 1041.81 435.376 1604.58 730.753 1713.44 z M 1002.96 588.644 L 1004.74 588.061 C 1005.7 582.463 1002.62 570.998 1001.38 565.365 C 964.943 401.816 825.31 343.477 675.894 322.716 C 703.263 417.984 739.351 507.827 831.847 559.58 C 882.452 587.895 945.913 600.64 1002.96 588.644 z" />
<path d="M 1447.63 774.31 C 1466.99 773.829 1479.84 789.768 1492.65 802.065 C 1598.84 903.998 1609.83 1072.78 1575.18 1207.76 C 1569.82 1228.55 1564.73 1260.7 1541.05 1267.95 C 1513.84 1272.68 1511.65 1247.61 1517.21 1228.75 C 1555.27 1099.42 1556.15 950.748 1461.25 844.839 C 1446.49 828.371 1414.28 815.596 1424.71 790.193 C 1429.12 779.44 1436.68 776.036 1447.63 774.31 z" />
<path d="M 921.833 797.237 C 951.095 795.04 989.613 858.005 1079.32 816.787 C 1104.69 805.129 1153.42 774.482 1150.9 831.101 C 1144.48 842.746 1139.52 845.664 1128.52 851.943 C 974.142 937.063 833.621 814.285 921.833 797.237 z" />
<path d="M 1497.5 1305.04 C 1537.83 1303.8 1534.41 1331.11 1520.2 1360.63 C 1508.41 1385.13 1460.58 1486.18 1434.24 1491.8 C 1413.92 1495.19 1397.1 1475.37 1403.39 1458.8 C 1409.29 1443.25 1426.07 1419.47 1435.14 1405 C 1448.3 1384.01 1479.36 1312.89 1497.5 1305.04 z" />
</FilledIcon>
);
}
/** A leaf sprig — `PRODUITS_FRAIS`/`HERBES_FRAICHES`. */
/** Basil sprig (foodiconpack.com, CC BY 4.0) — `PRODUITS_FRAIS`/`HERBES_FRAICHES`. */
export function HerbIcon() {
return (
<Icon>
<path d="M12 21V7" />
<path d="M12 8c0-3 2-5 5-5 0 3-2 5-5 5z" />
<path d="M12 13c0-3-2-5-5-5 0 3 2 5 5 5z" />
<path d="M12 18c0-2.5 1.7-4 4-4 0 2.5-1.7 4-4 4z" />
</Icon>
<FilledIcon>
<path d="M 1297 375.152 C 1297.31 375.122 1297.62 375.092 1297.93 375.062 L 1299.5 374.946 C 1324.25 372.99 1330.3 393.76 1340.29 411.428 C 1445.38 597.423 1466.58 822.011 1335.29 1001.07 C 1538.83 1039.4 1693.43 1206.7 1764.63 1395.49 C 1780.62 1440.99 1802.85 1460.72 1755.17 1486.63 C 1511.01 1619.32 1256.57 1642.53 1054.51 1420.27 C 1044.41 1409.16 1035.39 1396.78 1024.69 1386.22 L 1023.41 1384.96 C 1020.62 1390.15 1017.8 1396.04 1015.36 1401.29 C 954.186 1533 852.17 1597.02 721.738 1643.36 C 635.872 1669.89 545.702 1676.59 456.49 1670 C 444.114 1669.08 424.62 1670.5 415.057 1660.86 C 399.096 1644.75 396.531 1553 396.403 1526.65 C 395.692 1380.85 439.29 1221.82 549.469 1120.14 C 556.91 1113.28 564.765 1107.03 571.903 1099.81 C 414.431 1016.69 311.984 867.702 267.408 696.354 C 260.292 669.001 292.765 660.426 312.264 650.538 C 464.952 573.114 662.94 553.712 812.003 649.983 C 839.647 667.809 858.12 688.164 882.624 708.423 C 952.609 553.039 1138.14 422.318 1297 375.152 z M 1058.31 1158.5 C 1086.64 1176.01 1110.56 1194.94 1141.8 1210.5 L 1143.45 1210.05 C 1161.11 1205.39 1177.37 1205.57 1195.39 1199.49 C 1211.62 1194.02 1222.58 1185.39 1238.1 1179.36 C 1265.26 1112.37 1353.03 1181.94 1236.12 1237.58 C 1226.69 1242.23 1215.48 1244.9 1205.92 1249.34 C 1271.27 1282.67 1347.14 1304.65 1419.69 1312.71 C 1444.43 1315.46 1471.38 1314.5 1495.71 1318.23 C 1516.07 1321.35 1518.21 1362.24 1492.81 1365.18 C 1396.9 1372.42 1308.35 1347.66 1221.61 1310.93 C 1176.05 1291.64 1092.58 1247.44 1054 1213.44 C 1054.22 1219.16 1054.24 1223.36 1054 1229.09 C 1054.21 1269.53 1049.24 1289.92 1043.83 1328.33 C 1055.08 1336.9 1066.99 1355.53 1076.09 1366.7 C 1168.79 1480.51 1310.86 1552.36 1459.54 1534.46 C 1539.73 1524.81 1652.75 1481.64 1726.47 1444.05 C 1719.96 1417.1 1708.08 1392.76 1696.41 1367.77 C 1628.45 1222.19 1504.2 1091.36 1343.67 1053.67 C 1329.31 1050.3 1305.36 1049.82 1293.46 1045.94 C 1216.88 1115.83 1148.01 1127.06 1058.31 1158.5 z M 1008.88 1104.51 C 1029.08 1016.58 1066.85 970.902 994.607 892.42 C 986.451 883.338 973.212 876.324 967.474 865.403 C 951.773 835.52 1009.39 801.066 1062.57 897.391 C 1074.35 864.076 1146.81 683.543 1177.85 667.485 C 1194.73 615.664 1245.36 655.028 1229.77 677.355 C 1162.43 773.807 1144.66 834.804 1103.59 935.529 C 1128.61 924.476 1194.49 891.272 1208.89 931.16 C 1219.69 961.081 1175.36 962.839 1160.07 967.194 C 1092.16 983.544 1071.27 1043.61 1062.57 1104.51 C 1114.58 1093.5 1194.46 1057 1237.44 1026.78 C 1282.73 994.928 1323.48 933.824 1344.58 883.971 C 1399.15 754.991 1376.26 618.983 1326.7 493.407 C 1319.33 474.74 1300.37 449.876 1294.56 430.498 L 1293.46 430.013 C 1073.91 508.331 826.989 716.516 924.55 976.739 C 949.002 1041.96 966.404 1054.78 1008.88 1104.51 z M 911.322 1220.42 C 908.583 1225.42 905.868 1230.48 903.012 1235.41 C 879.204 1305.9 892.966 1298.41 890.98 1355.46 C 869.513 1387.78 840.809 1367.92 837.943 1335.49 C 835.735 1310.5 834.461 1287.82 840.494 1263 C 772.153 1312.77 716.487 1369.47 664.741 1435.89 C 654.728 1448.75 636.703 1476.18 624.209 1484.63 C 603.237 1487.23 593.985 1482.23 588.611 1461.25 C 605.45 1395.46 769.662 1249.54 829.728 1210.5 C 804.37 1201.73 744.257 1193.55 731.185 1179.36 C 729.217 1161.72 731.122 1142.64 752.398 1142.95 C 790.952 1143.52 852.187 1158.43 882.624 1179.36 C 912.06 1159.77 938.23 1152.97 966.613 1137.46 C 933.936 1081.82 841.879 1065.99 783.499 1067 C 518.61 1094.25 425.858 1371.6 451.438 1602.06 L 453.382 1618.13 C 582.728 1636.62 748.134 1597.01 851.931 1521.4 C 953.222 1447.61 1028.59 1305.85 996.187 1179.36 C 969.744 1193.75 934.896 1205.5 911.322 1220.42 z M 624.209 1067 C 663.756 1024.24 801.711 1004.42 859.585 1024.71 C 834.882 993.777 768.557 949.403 734.582 928.067 C 689.329 896.195 626.684 867.979 574.45 850.664 C 565.801 847.797 555.964 845.051 548.231 840.425 C 529.357 829.134 535.202 795.346 557.806 793.29 C 574.762 791.747 594.243 803.066 610.101 808.112 C 686.088 836.217 805.124 901.186 863.317 957.456 C 852.613 903.863 843.249 823.055 863.317 771.526 C 803.078 672.149 665.648 629.126 555.357 635.989 C 500.936 641.624 449.889 649.695 398.311 669.264 C 372.256 679.149 347.839 692.36 322.412 703.094 C 351.371 820.526 445.376 959.627 543.493 1025.77 C 568.268 1042.47 598.946 1052.4 624.209 1067 z" />
</FilledIcon>
);
}
/** A cut of meat — `BOUCHERIE_POISSONNERIE`/`VIANDES`. */
/** Cut of beef (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`VIANDES`. */
export function MeatIcon() {
return (
<Icon>
<path d="M8 16 17 7a3 3 0 1 1 4 4l-9 9a5 5 0 0 1-8-2 5 5 0 0 1 4-4z" />
<circle cx="9" cy="15" r="1.2" fill="currentColor" stroke="none" />
</Icon>
<FilledIcon>
<path d="M 1308.14 523.992 L 1312.6 523.924 C 1454.43 522.805 1610.61 548.182 1716.75 650.075 C 1775.28 706.261 1782.93 751.789 1783.01 826.54 L 1783.04 917.353 C 1783.06 1010.55 1771.33 1066.78 1700.13 1137.95 C 1596.62 1241.42 1403.81 1296.7 1259.76 1326.93 C 1209.71 1337.43 1160.48 1346.4 1120.16 1380.19 C 1012.81 1470.17 871.679 1517.01 732.041 1517.05 C 595.68 1521.18 446.238 1484.54 349.138 1386.9 C 249.032 1286.24 267.821 1219.77 264.451 1097.37 C 263.781 988.817 261.11 910.336 340.139 821.149 C 474.113 669.955 688.1 616.603 883.588 631.743 C 957.665 637.48 921.629 639.588 980.005 611.665 C 1080 554.854 1194.6 532.022 1308.14 523.992 z M 706.25 1286.5 C 818.707 1286.65 934.051 1253.71 1024.51 1185.44 C 1077.93 1145.12 1101.9 1119.99 1171.74 1105.09 C 1337.6 1069.69 1557.82 1016.23 1684.73 894.779 C 1750.71 831.638 1750.35 747.664 1684.27 684.643 C 1595.11 603.34 1458.42 569.51 1340.93 571.393 L 1333.37 571.545 C 1221.14 574.763 1097.45 597.84 999.775 655.417 C 940.411 690.411 951.287 685.266 883.111 678.262 C 713.082 660.793 509.707 714.149 388.559 839.132 C 285.627 945.322 291.099 1078.2 397.649 1178.69 C 480.66 1255.97 594.96 1285.65 706.25 1286.5 z M 311.865 1150.04 C 311.618 1193.12 310.214 1233.21 327.929 1274 C 360.387 1349.74 426.019 1399.76 500.238 1429.77 C 669.049 1498.04 907.387 1480.12 1056.81 1368.31 C 1144.06 1303.01 1155.66 1298.02 1263.78 1277.08 C 1411.68 1248.44 1661.21 1169.28 1720.81 1024.04 C 1727.44 1007.91 1741.85 923.985 1734.47 909.711 C 1629.45 1029.38 1445.12 1090.38 1294.98 1126.72 C 1243.16 1139.26 1192.05 1145.3 1142.31 1163.55 C 1106.31 1176.9 1076.19 1206.59 1044.46 1229.82 C 846.531 1374.72 495.37 1379.86 331.246 1177.56 C 324.617 1169.39 318.964 1158.23 311.865 1150.04 z" />
<path d="M 1315.3 625.51 C 1403.29 621.006 1500.96 638.275 1580.27 677.164 C 1633.56 703.294 1709.65 758.76 1674.46 826.622 C 1617.4 936.633 1349.51 1014.12 1219.34 1042.33 C 1150.83 1057.18 1089.02 1071.24 1031.01 1112.69 C 1007.54 1129.45 986.187 1148.63 962.088 1164.65 C 832.042 1251.11 632.613 1256.92 496.247 1184.82 C 391.696 1129.54 330.391 1025.89 391.816 914.601 C 406.591 887.832 433.393 895.208 439.338 922.263 C 439.253 922.61 439.167 922.957 439.082 923.304 L 438.609 925.025 C 431.523 951.486 410.012 974.111 413.43 1009.19 C 415.396 1029.38 424.15 1059.52 439.338 1072.9 C 444.902 1083.93 450.491 1091.78 459.648 1100.35 C 584.804 1217.49 818.215 1211.08 952.793 1113.46 C 1069.9 1022 1106.29 1021.56 1252.02 986.806 C 1361.4 960.723 1533.36 906.124 1616.42 824.398 C 1680.03 768.986 1552.92 713.717 1512.33 700.09 C 1363.35 650.077 1194.11 669.799 1053.38 735.651 C 1022.03 750.322 997.224 723.169 1021.34 697.751 C 1072.9 656.981 1251.49 628.454 1315.3 625.51 z" />
<path d="M 646.825 886.362 C 702.457 879.407 786.447 922.225 794.75 982.778 C 801.648 1033.08 763.457 1065.59 718.244 1072.9 C 567.004 1081.28 502.772 901.463 646.825 886.362 z M 718.244 1024.83 C 793.647 1002.49 716.16 931.349 646.825 932.999 C 561.064 956.042 655.25 1036.09 718.244 1024.83 z" />
<path d="M 802.826 729.574 C 828.698 729.076 924.149 731.655 942.262 748.115 C 956.777 761.306 943.306 787.771 925.012 787.846 C 907.327 787.919 888.961 782.426 871.344 780.184 C 846.828 777.066 821.805 776.935 797.139 776.997 C 760.02 779.065 724.895 784.441 688.234 789.836 C 663.605 793.461 645.396 771.717 661.968 753.577 C 679.575 734.304 774.344 730.066 802.826 729.574 z" />
</FilledIcon>
);
}
/** Drumstick — `BOUCHERIE_POISSONNERIE`/`VOLAILLES`. */
/** Chicken (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`VOLAILLES`. */
export function PoultryIcon() {
return (
<Icon>
<path d="M13 4c2.5 0 4.5 2 4.5 4.5 0 2.8-2.3 4.8-4 6.4l-3.2 3.2a2.3 2.3 0 0 1-3.3-3.3l3.2-3.2C11.7 10 13.7 7.7 13 4z" />
<path d="M6.5 21 5 22.5" />
<path d="M9 18.5 7.5 20" />
</Icon>
<FilledIcon>
<path d="M 675.126 266.323 C 967.235 260.048 1214.44 528.773 1251.89 805.204 C 1266.26 911.248 1251.46 943.654 1312.46 1036.29 C 1335.55 1072.85 1332.23 1115.27 1333 1156.17 C 1341.12 1168.54 1356.96 1182.77 1368.02 1193.66 L 1523.48 1347.01 C 1571.93 1308.81 1626.77 1294.25 1686.33 1320.03 C 1837.19 1385.35 1796.26 1620.31 1605.98 1612.96 C 1607.1 1734 1500.17 1813.09 1387.71 1760.71 C 1313.41 1722.49 1280.75 1625.77 1323.94 1552.68 C 1330.42 1541.72 1340.23 1529.7 1347.93 1519.26 C 1328.45 1504.48 1304.03 1476.31 1285.74 1458.28 C 1242.55 1415.72 1196.92 1372.39 1155.75 1327.99 C 1115.44 1335.98 1066.84 1330.84 1034.25 1304.74 C 952.169 1239.01 919.31 1249.46 819.432 1240.34 C 552.64 1215.97 290.736 994.396 273.345 717.722 C 258.595 483.062 440.327 279.156 675.126 266.323 z M 1012.42 1226.73 C 1064.37 1255.02 1057.57 1277.45 1119.99 1283.74 L 1119.73 1282.45 C 1117.6 1272.41 1113.71 1263.24 1111.83 1252.97 C 1096.94 1171.59 1162.38 1099.72 1244.97 1108.95 C 1257.47 1110.35 1269.74 1114.56 1282.14 1115.55 C 1281.25 1066.26 1263.39 1056.6 1241.35 1018.29 C 1209.13 962.295 1212.42 888.346 1204.72 825.685 C 1172.36 562.105 946.595 307.481 667.216 315.994 C 336.567 353.769 220.698 717.712 423.74 970.752 C 524.402 1096.2 678.735 1180.6 839.46 1192.03 C 911.572 1197.16 943.937 1193.67 1012.42 1226.73 z M 1555.01 1604.22 C 1549.48 1592.78 1542.83 1579.52 1548.04 1566.93 C 1561.96 1533.24 1596 1559.93 1616.77 1561.54 C 1731.75 1570.5 1773.06 1412.29 1665.9 1366.04 C 1579.53 1328.72 1563.03 1415.3 1503.62 1392.76 C 1483.85 1385.26 1452.8 1348.4 1437.09 1332.91 L 1318.07 1213.59 C 1293.75 1189.22 1266.91 1153.69 1227.77 1159.34 C 1167.96 1167.95 1138.15 1231.74 1174.36 1275.3 C 1192.34 1296.95 1216.41 1318.31 1236.62 1338.38 L 1352.4 1454.75 C 1401.76 1503.76 1423.84 1509.57 1371.55 1571.75 C 1356.84 1590.46 1352.85 1621.38 1357.31 1643.95 C 1383.05 1774.38 1573.03 1740.94 1555.01 1604.22 z" />
<path d="M 455.532 531 C 499.531 529.749 482.303 565.435 472.293 588.075 C 368.207 819.036 626.45 1051.94 846.232 1066.11 C 854.945 1066.67 864.015 1066.09 872.296 1067.15 C 898.816 1070.54 905.178 1109.95 875.701 1115.55 C 815.373 1120.97 737.3 1099.73 683.214 1075.56 C 484.697 986.826 328.482 768.729 435.173 551.515 C 440.697 540.27 443.403 535.311 455.532 531 z" />
<path d="M 595.867 416.13 C 613.538 413.493 629.489 429.31 625.29 446.941 C 621.69 462.056 580.796 475.159 563.709 484.618 C 542.722 487.614 527.439 464.903 536.564 448.282 C 543.497 435.655 582.641 420.266 595.867 416.13 z" />
</FilledIcon>
);
}
/** Fish — `BOUCHERIE_POISSONNERIE`/`POISSONS`. */
/** Salmon (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`POISSONS`. */
export function FishIcon() {
return (
<Icon>
<path d="M3 12c3-4 8-6 13-4 2.5 1 5 3 5 4s-2.5 3-5 4c-5 2-10 0-13-4z" />
<path d="M16 9v6" />
<circle cx="6.5" cy="11" r=".6" fill="currentColor" stroke="none" />
</Icon>
<FilledIcon>
<path d="M 848.8 611.959 C 956.068 608.708 1044.51 631.421 1131.39 698.914 C 1148.25 712.014 1165.22 725.562 1177.7 742.966 C 1227.5 742.17 1299.29 739.642 1347.16 745.629 C 1475.32 761.658 1584.27 822.795 1683.11 902.874 C 1728.9 939.97 1819.43 997.284 1769.04 1067.17 C 1695.69 1166.69 1489.7 1273.94 1375.76 1298.94 C 1360.87 1302.21 1346.37 1303.87 1331.48 1306.84 C 1329.74 1307.19 1324.42 1329.42 1322.71 1331.44 C 1312.15 1353.61 1273.12 1434.41 1240.09 1416.29 C 1205.22 1397.16 1161.41 1373.48 1131.54 1347.44 C 1077.87 1401.88 959.556 1430.72 883.367 1434.04 C 864.642 1434.85 841.26 1438.14 830.499 1420.07 C 824.192 1403.88 839.343 1383.72 845.775 1368.4 C 859.93 1334.68 868.734 1300.99 871.477 1264.51 C 811.517 1248.31 719.051 1202.21 660.649 1176.38 C 635.966 1165.47 610.267 1149.44 584.486 1142.76 C 569.097 1138.77 527.699 1150.22 516.836 1159.32 C 477.658 1192.15 410.834 1284.93 366.594 1293.68 C 328.759 1301.16 292.698 1283.67 276.582 1248.61 C 257.157 1207.52 278.639 1177.56 298.038 1142.08 L 333.333 1078.66 C 358.335 1034.54 356.085 1028.74 329.607 985.464 C 313.639 959.826 300.153 929.214 283.839 904.045 C 234.677 828.199 326.659 715.631 421.756 816.857 L 467.491 866.032 C 528.722 932.931 541.448 935.655 629.953 894.25 C 675.649 872.872 818.201 795.24 861.408 788.49 C 869.971 739.897 865.6 713.87 843.414 669.403 C 836.835 652.967 807.955 621.582 848.8 611.959 z M 1035.23 1260.01 C 1084.67 1266.26 1121.21 1264.44 1170.4 1264.52 C 1268.29 1264.66 1372.07 1266.46 1462.35 1221.55 C 1318.94 1111.49 1314.37 931.696 1452.56 819.114 C 1376.92 794.541 1343.35 788.251 1265.28 788.502 L 1181.4 788.532 C 1154.8 788.512 1108.16 785.885 1084.93 788.49 C 912.171 783.433 754.681 894.896 602.061 958.678 C 542.216 981.383 542.976 1064.44 597.361 1093.98 C 632.316 1112.96 667.984 1128.68 704.288 1144.7 C 784.236 1179.98 949.211 1257.44 1035.23 1260.01 z M 1513.21 1199.7 C 1538.49 1186.5 1565.23 1169.12 1588.88 1157.14 L 1590.18 1156 C 1637 1115.8 1692.77 1091.87 1732.84 1037.59 C 1761.99 998.107 1583.28 886.745 1555.05 868.355 C 1541.64 859.615 1517.43 849.614 1506.72 840.812 C 1354.84 928.932 1366.55 1109.04 1513.21 1199.7 z M 538.028 1100.01 C 508.305 1065.26 497.667 1001.63 528.261 965.1 C 463.125 943.727 445.497 907.33 398.237 859.615 C 383.478 844.714 370.882 820.78 347.424 822.164 C 278.723 836.198 344.977 918.628 362.722 950.128 C 411.207 1029.13 405.5 1044.14 363.451 1120.08 L 331.581 1177.83 C 318.823 1201.08 302.259 1225.54 331.962 1244.78 C 361.159 1263.68 384.283 1227.56 401.021 1210.56 C 422.042 1188.15 474.084 1130.68 497.432 1116.97 C 510.269 1109.44 524.88 1106.52 538.028 1100.01 z M 1207.23 1311.42 L 1162.01 1311.42 C 1184.76 1333.38 1215.11 1349.18 1241.53 1366.8 C 1243.02 1366.96 1246.95 1367.54 1248.19 1366.8 C 1261.56 1345.86 1269.05 1333.98 1280.47 1311.42 L 1207.23 1311.42 z M 1111.06 742.966 C 1058.95 692.943 961.972 655.509 888.733 656.971 C 908.887 698.018 911.838 724.895 910.908 769.027 C 958.966 755.726 1031.03 738.7 1080.72 742.966 C 1094.34 741.284 1097.55 741.422 1111.06 742.966 z M 917.364 1281.01 C 910.429 1325.55 901.194 1346.02 888.733 1386.83 C 940.753 1385.27 990.679 1371.4 1038.67 1352.01 C 1060.75 1342.49 1086.55 1329.1 1103.09 1311.42 C 1030.94 1309.91 986.962 1301.35 917.364 1281.01 z" />
<path d="M 1138.71 911.138 C 1141.01 911.319 1146.16 911.143 1148.69 911.138 C 1179.13 922.018 1172.09 950.887 1143.5 958.005 C 1080.14 973.783 1025.18 1007.98 985.657 1060.08 C 976.182 1072.57 973.205 1084.48 959.841 1094.55 C 936.953 1100.24 920.042 1077.29 934.135 1054.15 C 980.888 977.368 1054.04 933.806 1138.71 911.138 z" />
<path d="M 935.933 915.073 C 953.045 914.272 967.591 929.458 961.074 946.565 C 955.463 961.292 877.195 1000.11 851.021 1070.08 C 846.765 1081.41 842.067 1089.7 830.499 1094.55 C 792.917 1099.31 802.165 1062.07 811.947 1040.99 C 832.235 997.279 888.251 932.996 935.933 915.073 z" />
<path d="M 1562.6 950.18 C 1573.55 948.189 1584.28 954.59 1587.73 965.166 C 1589.08 969.304 1589.19 973.747 1588.04 977.944 C 1585.68 986.54 1578.42 992.9 1569.59 994.105 C 1563.05 994.998 1556.45 992.937 1551.57 988.479 C 1546.7 984.021 1544.06 977.628 1544.36 971.03 C 1544.85 960.681 1552.41 952.035 1562.6 950.18 z" />
</FilledIcon>
);
}
/** Shrimp — `BOUCHERIE_POISSONNERIE`/`CRUSTACES_FRUITS_DE_MER`. */
/** Shrimp (foodiconpack.com, CC BY 4.0) — `BOUCHERIE_POISSONNERIE`/`CRUSTACES_FRUITS_DE_MER`. */
export function ShellfishIcon() {
return (
<Icon>
<path d="M6 6c6-2 12 1 13 7-4 2-9 2-12-1C5 10 4.5 8 6 6z" />
<path d="M6 6 4 4M7 8 5 7M8 11l-2.2.6" />
</Icon>
<FilledIcon>
<path d="M 406.938 329.975 C 420.456 328.426 461.606 329.929 478.111 329.968 L 1131.51 329.551 C 1470.33 330.801 1773.34 636.729 1768.97 977.539 C 1764.28 1343.03 1477.67 1613.49 1117.55 1611 C 1016.13 1682.55 903.221 1736.61 783.079 1768.46 C 758.473 1774.04 730.58 1783.31 705.357 1777.76 C 660.154 1767.83 630.173 1723.58 639.98 1678 C 647.173 1644.57 671.241 1628.08 695.497 1606.99 L 753.833 1556.35 C 766.873 1545.02 790.931 1521.56 803.533 1513.61 C 790.311 1499.24 771.674 1488.17 755.927 1475.94 C 715.955 1444.91 649.476 1381.54 635.493 1332.75 C 627.423 1290.39 652.422 1253.62 692.825 1240.15 C 753.277 1220.01 936.175 1304.57 988.999 1331.68 C 1020.6 1347.9 1050.88 1366.76 1082.58 1383.33 C 1098.43 1377.29 1127.57 1377.77 1146.56 1371.73 C 1207.35 1352.41 1259.45 1319.26 1291.51 1263.48 C 1337.45 1180.16 1370.08 1123.42 1342.98 1021.19 C 1316.94 922.937 1222.75 846.004 1121.11 839.379 C 1097.59 837.846 1033.24 836.114 1012.94 842.367 C 953.966 860.532 924.33 939.719 967.505 988.121 C 993.163 1016.89 1013.51 1022.95 1040.23 1043.51 C 1064.01 1062.28 1038.26 1106.8 1000.82 1081.01 C 904.072 1014.36 858.029 948.67 929.502 838.946 C 864.678 837.299 799.62 883.674 812.342 953.152 C 821.604 1003.73 876.964 1022.7 892.321 1055.77 L 892.93 1057.12 C 883.291 1126.99 828.577 1062.01 807.666 1042.42 C 746.966 985.54 742.644 897.667 803.533 838.946 C 770.437 837.973 735.885 840.112 702.975 838.728 C 512.664 830.723 353.129 724.246 298.564 537.94 C 282.773 484.022 265.073 438.935 302.954 387.995 C 327.141 355.47 367.174 335.163 406.938 329.975 z M 359.088 401.252 C 381.815 416.537 394.861 433.427 415.235 451.468 C 467.804 498.019 526.934 511.39 594.352 518.344 C 592.133 535.017 589.861 552.495 589.477 569.33 C 513.374 562.715 444.814 542.9 385.02 493.222 C 368.174 479.227 346.168 454.633 330.488 443.841 C 330.307 514.756 386.704 628.592 434.824 676.604 C 507.857 749.474 621.768 788.396 723.801 788.689 L 843.532 788.441 C 860.28 788.413 882.053 787.742 898.5 788.49 L 1038.36 788.472 C 1140.07 788.443 1215.01 788.544 1299.22 861.535 C 1441.2 811.164 1518.19 711.434 1544.44 565.358 C 1402.87 437.853 1295.28 380.584 1095.5 380.613 L 693.374 380.636 L 433.545 380.606 C 406.526 380.464 381.311 384.734 359.088 401.252 z M 1338.86 903.254 C 1379.94 947.312 1405.12 1027.41 1402.49 1086.62 C 1431.25 1098.02 1443.96 1104.03 1475.01 1111.46 C 1567.78 1128.49 1642.63 1113.9 1713 1048.13 C 1737.19 927.145 1693.35 769.641 1627.82 668.707 C 1620.68 657.712 1597.84 623.903 1589.87 616.435 C 1585.79 627.183 1582.78 638.577 1579.17 649.494 C 1537.83 774.365 1453.88 850.262 1338.86 903.254 z M 1237.15 1388.06 C 1188.44 1418.7 1089.96 1431.25 1072.26 1448.24 C 1033.97 1485 1054.23 1546.57 1105.82 1555.71 C 1127.29 1559.52 1143.48 1559.29 1164.89 1558.99 C 1217.37 1553.15 1303.79 1539.24 1351.4 1513.61 C 1305.52 1486.7 1268.61 1456.41 1246.45 1406.36 C 1244.91 1402.89 1239.04 1389.72 1237.15 1388.06 z M 1696.63 1125.81 C 1619.77 1182.53 1484.66 1181.57 1398.81 1140.65 C 1380.05 1217.41 1341.24 1299.85 1284.26 1355.35 C 1290.07 1379.84 1300.31 1401.23 1315.82 1421.15 C 1344.11 1455.26 1377.21 1471.4 1419.23 1481.63 C 1518.71 1443.09 1623.53 1309.89 1665.65 1216.82 C 1671.57 1203.75 1698.88 1137.41 1696.63 1125.81 z M 713.193 1287.13 C 701.618 1289.96 695.911 1294.19 689.674 1304.42 C 663.742 1346.97 811.935 1456.07 841.285 1472.47 C 900.389 1505.48 885.289 1511.25 835.595 1553.03 C 815.59 1569.86 702.246 1665.68 692.995 1681.95 C 684.317 1697.21 693.107 1716.72 707.541 1724.7 C 725.346 1734.55 814.134 1707.12 840.752 1696.69 C 887.26 1680.39 1018.53 1623.86 1054.87 1588.48 C 982.295 1545.74 989.462 1476.4 1031.52 1412.55 C 939.345 1365.12 862.757 1323.81 759.293 1296.88 C 748.377 1294.04 723.137 1286.46 713.193 1287.13 z" />
<path d="M 663.454 505.915 C 691.733 504.193 716.17 525.462 718.366 553.708 C 720.562 581.954 699.707 606.745 671.502 609.415 C 652.83 611.183 634.657 602.742 623.963 587.335 C 613.268 571.929 611.715 551.952 619.9 535.077 C 628.084 518.202 644.734 507.055 663.454 505.915 z" />
</FilledIcon>
);
}
/** Wheat ear — 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 (`EPICERIE_SECHE`/`FECULENTS`, and flours under `AIDES_CULINAIRES`/`BASES`). */
export function GrainIcon() {
return (
<Icon>
<path d="M12 22V6" />
<path d="M12 7c-1-1-2.5-1-3.5 0 1 1 2.5 1 3.5 0zM12 7c1-1 2.5-1 3.5 0-1 1-2.5 1-3.5 0z" />
<path d="M12 11c-1-1-2.5-1-3.5 0 1 1 2.5 1 3.5 0zM12 11c1-1 2.5-1 3.5 0-1 1-2.5 1-3.5 0z" />
<path d="M12 15c-1-1-2.5-1-3.5 0 1 1 2.5 1 3.5 0zM12 15c1-1 2.5-1 3.5 0-1 1-2.5 1-3.5 0z" />
<path d="M12 6c0-1.5.8-2.8 2-3.5" />
</Icon>
<FilledIcon>
<path d="M 910.895 546.004 C 963.142 539.715 976.976 568.025 1015.6 572.175 C 1067.55 579.738 1143.11 497.126 1227.25 585.607 C 1265.7 626.042 1281.37 619.731 1333.45 613.476 C 1383.16 607.852 1437.33 637.737 1461.72 680.486 C 1492.16 733.835 1478.5 737.073 1547.2 751.088 C 1597.77 761.402 1642.54 804.035 1653.56 854.79 C 1665.08 907.857 1662.91 902.985 1711.69 916.532 C 1799.22 942.499 1808.65 1051.5 1730.7 1105.25 C 1727.55 1124.49 1717.68 1147.67 1709.51 1165.97 C 1652.44 1293.6 1556 1408.56 1434.94 1479.6 C 1399.88 1500.17 1350.75 1495.6 1311.33 1495.59 L 1166.25 1495.54 L 723.987 1495.58 C 638.136 1495.6 617.325 1495.43 547.632 1438.36 C 439.637 1349.93 360.588 1238.54 320.186 1105.25 C 312.672 1099 303.794 1093.43 296.379 1086.6 C 244.517 1038.86 257.881 948.348 324.422 920.61 C 391.508 892.645 373.956 917.684 398.634 843.682 C 412.775 801.278 451.966 762.448 495.675 751.506 C 559.266 735.587 546.838 742.839 577.749 688.391 C 606.277 638.141 663.732 605.766 721.87 613.943 C 767.387 620.345 778.161 626.969 813.557 592.239 C 844.736 562.253 867.315 547.981 910.895 546.004 z M 1123.75 593.348 C 1084.78 598.665 1072.65 620.665 1024.21 620.51 C 969.014 620.334 957.472 587.74 904.098 595.381 C 848.119 603.395 838.626 672.347 751.279 667.428 C 704.28 664.781 671.206 644.723 631.072 697.809 C 616.411 717.201 609.97 741.06 592.558 758.55 C 559.137 801.379 512.288 783.678 474.207 817.233 C 408.119 875.466 479.926 921.396 341.041 966.749 C 318.781 974.018 305.366 1019.28 319.921 1037.81 C 341.458 1075.3 386.538 1066.19 423.368 1066.19 L 899.75 1066.17 L 1656.66 1066.27 C 1667.48 1066.37 1677.69 1066.49 1688.01 1066.01 C 1737.09 1063.73 1767.97 972.945 1671.43 957.275 C 1606.38 946.716 1612.76 858.359 1586.19 827.757 C 1544.59 779.835 1485.94 815.998 1438.22 741.598 C 1430.31 729.264 1425.74 714.695 1418.47 702.944 C 1361 610.666 1286.49 715.282 1206.1 632.781 C 1182.1 608.147 1161.13 589.594 1123.75 593.348 z M 1549.74 1321.46 C 1604.47 1258.73 1646.54 1193.93 1677.04 1115.97 C 1648.29 1115.12 1615.43 1115.97 1586.23 1115.97 L 422.623 1115.92 C 411.331 1115.9 386.023 1114.98 376.085 1115.97 C 407.412 1231.51 523.469 1368.3 622.564 1429.74 C 649.154 1446.22 669.529 1445.25 699.498 1445.35 L 1328.1 1445.41 C 1341.98 1445.41 1382.15 1446.27 1393.43 1442.45 C 1444.74 1425.09 1514.43 1360.06 1549.74 1321.46 z" />
<path d="M 1141.2 687.994 C 1152.75 687.403 1193.43 686.078 1203.08 691.804 C 1220.91 702.389 1217.43 733.715 1193.16 737.983 C 1183.34 738.44 1167.74 737.105 1157.57 738.156 C 1108.99 743.18 1107.03 692.946 1141.2 687.994 z" />
<path d="M 560.473 908.045 C 619.234 904.862 574.313 970.708 540.825 986.982 C 518.475 987.906 510.767 975.204 511.027 954.689 C 517.878 941.655 546.665 912.691 560.473 908.045 z" />
<path d="M 779.781 947.003 C 793.571 946.901 828.187 944.959 838.316 951.744 C 856.542 963.953 848.523 992.185 826.199 996.97 C 815.153 997.062 780.364 998.789 771.828 994.339 C 750.301 983.115 754.338 953.94 779.781 947.003 z" />
<path d="M 1056.75 865.234 C 1113.77 869.736 1066.37 938.797 1032.46 943.013 C 974.711 946.264 1028.33 872.554 1056.75 865.234 z" />
<path d="M 936.348 737.983 C 958.3 731.257 1033.13 789.941 976.649 809.248 C 951.821 813.134 884.764 753.056 936.348 737.983 z" />
<path d="M 1231.76 918.185 C 1252.8 919.574 1319.83 980.518 1263.2 996.97 C 1239.04 996.092 1175.04 929.033 1231.76 918.185 z" />
<path d="M 1342.56 781.027 C 1369.08 780.782 1429.65 832.584 1379.48 858.001 C 1356.36 858.94 1292.24 799.262 1342.56 781.027 z" />
<path d="M 1472.67 943.013 C 1485.27 942.888 1509.89 941.296 1520.38 945.509 C 1544.12 955.043 1537.31 985.279 1514.98 993.49 C 1502.43 993.656 1468.83 995.776 1460.6 989.535 C 1440.72 974.456 1448.11 947.945 1472.67 943.013 z" />
<path d="M 709.837 784.339 C 734.276 782.429 793.224 845.928 739.437 858.001 C 711.809 858.665 655.635 792.466 709.837 784.339 z" />
<path d="M 1503.8 1239.21 C 1533.06 1239.99 1537.22 1267.62 1517.1 1287.65 C 1436.03 1368.35 1341.5 1414.67 1226.01 1414.01 C 1220.84 1414.36 1216.6 1414.17 1211.46 1414.01 C 1195.42 1398.13 1192.26 1372.96 1219.01 1365.73 C 1229.64 1362.85 1247.87 1365.62 1259.95 1364.16 C 1337.23 1354.87 1410.36 1318.8 1467.44 1266.26 C 1478.54 1256.05 1488.75 1242.33 1503.8 1239.21 z" />
</FilledIcon>
);
}
/** Bean pod — `EPICERIE_SECHE`/`LEGUMINEUSES`. */
/** Chickpeas (foodiconpack.com, CC BY 4.0) — `EPICERIE_SECHE`/`LEGUMINEUSES`. */
export function LegumeIcon() {
return (
<Icon>
<path d="M5 13c0-5 3.5-9 9-9 1 3-1 5-1 5s3 0 5 3c0 5-4 9-9 9-3 0-4-4-4-8z" />
<circle cx="9.5" cy="14.5" r="1" fill="currentColor" stroke="none" />
<circle cx="13" cy="11" r="1" fill="currentColor" stroke="none" />
</Icon>
<FilledIcon>
<path d="M 1013.51 267.432 C 1054.43 264.207 1100.24 278.034 1122.05 315.518 C 1180.56 423.993 1241.02 436.027 1342.11 494.876 C 1471.74 570.336 1530.28 681.13 1486.64 829.74 C 1545.75 839.209 1566.11 892.267 1599.8 933.697 C 1671.83 1022.28 1715.25 1055.05 1758.15 1175.38 C 1841.93 1412.28 1678.03 1672.39 1450.72 1750.57 C 1308.73 1799.41 1173.53 1789.15 1039.67 1724.49 C 1033.19 1721.15 1031.84 1719.92 1026.34 1715.26 C 991.374 1733.25 959.676 1746.61 921.88 1758.18 C 494.809 1888.38 94.9953 1429.32 345.529 1050.14 C 361.138 1026.52 380.493 1006.19 399.199 985.2 L 459.204 916.706 C 496.056 874.184 500.503 852.333 555.689 829.74 C 553.61 813.771 548.9 797.541 547.223 780.596 C 535.851 665.686 596.471 557.081 693.829 497.779 C 775.149 450.088 855.239 428.656 910.864 340.514 C 940.561 293.459 948.947 274.699 1013.51 267.432 z M 603.727 818.271 C 656.652 822.916 692.575 834.894 725.311 879.589 C 742 902.376 784.999 948.72 789.685 973.144 C 786.727 981.272 784.008 987.91 775.434 991.692 C 729.185 1012.41 703.046 876.839 618.793 869.227 C 570.664 864.878 545.066 886.976 517.381 922.752 C 464.085 991.627 394.366 1049.45 357.354 1129.84 C 193.102 1485.16 574.214 1833.93 933.689 1702.98 C 946.781 1698.22 964.505 1691.1 976.179 1684.07 C 952.103 1667.53 938.458 1655.36 917.845 1634.5 C 744.671 1462.32 720.803 1208.92 873.734 1012.23 C 892.823 987.684 916.515 966.264 935.633 941.851 C 957.672 913.706 977.482 885.57 1001.01 858.542 C 1017.98 839.054 1039.72 835.474 1054.3 822.633 C 1082.47 752.252 1063.19 669.654 1042.05 599.577 C 1035.1 576.532 1012.05 548.352 1026.34 525.437 C 1041.77 515.724 1060.34 514.993 1068.52 533.407 C 1106.84 619.641 1132.29 725.908 1108.23 818.271 C 1202.19 821.085 1218.89 888.402 1272.32 942.071 C 1315.86 881.786 1339.44 833.735 1419.84 820.857 L 1436.6 818.271 C 1456.63 760.852 1462.07 712.001 1433.76 653.521 C 1376.68 535.596 1237.13 516.899 1150.84 435.265 C 1134.31 419.621 1120.85 404.766 1107.08 386.506 C 1081.94 353.152 1071.33 305.846 1013.51 316.999 C 969.085 322.72 966.599 349.547 944.081 380.567 C 880.308 468.417 812.995 487.203 728.093 534.855 C 623.605 594.958 577.059 702.123 603.727 818.271 z M 1528.15 1658.14 C 1627.22 1594.53 1696.94 1496.13 1723.35 1380.9 C 1762.22 1211.25 1665.02 1075.29 1557.48 958.821 C 1542.9 943.033 1532.25 922.398 1518.81 905.496 C 1486.7 865.13 1425.17 857.059 1382.59 885.343 C 1336.85 915.728 1307.92 1010.81 1265.44 1008.94 C 1253.38 1002.37 1245.86 990.451 1237.49 979.5 L 1198.79 928.664 C 1167.97 887.694 1148.14 864.117 1089.75 868.031 C 1026.85 877.015 1005.09 937.006 967.48 979.741 C 947.131 1002.86 924.883 1024.48 906.383 1049.2 C 617.866 1445.08 1108.89 1920.89 1528.15 1658.14 z" />
<path d="M 803.837 570.263 C 816.631 568.791 828.258 576.677 831.432 589.333 C 842.771 634.54 728.622 623.76 711.544 730.095 C 708.383 749.772 710.529 763.493 688.265 769.926 C 612.6 773.1 694.982 592.905 803.837 570.263 z" />
<path d="M 541.29 1433.12 C 592.324 1430.63 559.382 1475.46 570.723 1502.01 C 578.27 1519.67 633.086 1547.81 584.185 1569.25 C 532.537 1578.43 488.747 1458.63 541.29 1433.12 z" />
<path d="M 497.481 1073.17 C 542.167 1068.57 514.288 1121.55 524.535 1142.28 C 531.879 1158.98 584.927 1184.58 537.784 1205.85 C 479.931 1210.95 445.843 1093.14 497.481 1073.17 z" />
<path d="M 460.572 1258.23 C 478.638 1255.82 486.678 1260.75 492.994 1277.94 C 476.761 1349.19 512.767 1321.04 523.439 1358.64 C 521.051 1370.57 519.267 1378.45 507.165 1383.62 C 448.938 1399.11 412.727 1273.75 460.572 1258.23 z" />
<path d="M 600.018 1258.23 C 618.259 1255.06 625.732 1263.03 633.327 1277.94 C 624.777 1322.36 625.753 1319.44 660.777 1346.4 C 669.325 1363.88 664.223 1374.38 647.895 1383.62 C 590.88 1393.42 551.674 1281.29 600.018 1258.23 z" />
<path d="M 682.829 1466.11 C 722.586 1464.19 702.352 1506.18 715.124 1525.8 C 731.787 1551.4 754.706 1540.44 750.813 1578.22 C 745.417 1586.64 742.894 1589.39 733.455 1592.05 C 674.449 1594.11 630.88 1478.9 682.829 1466.11 z" />
<path d="M 1521.8 1333.05 C 1604.54 1329.65 1516.57 1582.4 1323.12 1597.61 C 1283.49 1602.06 1282.02 1552.74 1320.79 1550.14 C 1411.56 1544.06 1487.33 1464.11 1502.59 1376.73 C 1505.99 1357.39 1500.88 1341.19 1521.8 1333.05 z" />
</FilledIcon>
);
}
/** Acorn — nuts, seeds, dried fruit (`EPICERIE_SECHE`/`GRAINES_FRUITS_SECS`). */
/** Almonds (foodiconpack.com, CC BY 4.0) — nuts, seeds, dried fruit (`EPICERIE_SECHE`/`GRAINES_FRUITS_SECS`). */
export function NutSeedIcon() {
return (
<Icon>
<path d="M8 9c0-3 1.8-5 4-5s4 2 4 5" />
<path d="M8 9c-1.3.5-2 1.7-2 3 0 4 2.7 8 6 8s6-4 6-8c0-1.3-.7-2.5-2-3z" />
</Icon>
<FilledIcon>
<path d="M 519.112 267.004 C 702.645 256.911 911.605 357.081 1054.42 464.673 C 1072.21 478.076 1088.34 493.554 1106.8 506.701 C 1245.69 449.076 1395.16 403.644 1547.04 409.112 C 1629.93 412.095 1660.51 492.578 1689.44 554.52 C 1857.55 927.686 1771.06 1348.1 1451.5 1602.11 C 1339.67 1689.88 1222.33 1748.42 1080.95 1770.26 C 771.484 1818.07 513.153 1603.38 513.066 1285.81 C 502.419 1269.82 489.949 1256.71 477.939 1241.8 C 284.456 1001.56 225.698 709.39 317.313 414.961 C 353.481 298.726 401.491 280.595 519.112 267.004 z M 808.738 1686.51 C 979.643 1758.72 1155.35 1723.52 1311.77 1634.91 C 1683.71 1424.22 1814.95 995.958 1657.65 609.48 C 1628.44 537.714 1608.99 455.036 1518.77 459.076 C 1470.96 458.583 1411.28 469.23 1364.38 478.775 C 987.814 554.845 568.571 857.845 563.56 1275.76 C 561.466 1450.38 646.303 1612.43 808.738 1686.51 z M 519.112 1210.8 C 522.564 1130.73 555.869 1031.88 594.69 962.196 C 703.455 761.458 852.304 636.013 1051.95 530.327 C 935.6 416.368 695.525 316.867 538.275 318.468 L 535.514 318.51 C 448.008 322.098 397.228 334.307 369.794 418.571 C 308.175 607.84 308.507 825.828 393.368 1008.1 C 422.163 1069.95 470.683 1160.88 519.112 1210.8 z" />
<path d="M 1598.68 637.027 C 1634.96 636.008 1631.43 677.59 1633.38 703.782 C 1660.05 1061.69 1409.99 1438.78 1084.43 1578.07 C 1064.07 1586.81 1041.58 1592.74 1021.7 1602.11 C 990.429 1606.96 975.681 1570.64 1001.28 1554.63 C 1009.08 1549.75 1029.21 1545.21 1039.32 1541.41 C 1365.76 1418.7 1617.24 1037.19 1580.86 690.388 C 1578.23 665.352 1572.45 648.646 1598.68 637.027 z" />
<path d="M 1389.55 513.084 C 1420.88 510.153 1436.47 548.477 1389.63 565.042 C 1089.32 671.257 809.481 970.365 747.434 1282.07 C 742.975 1304.47 747.472 1327.49 723.119 1337.73 C 681.185 1342.02 692.713 1296.79 697.737 1271.86 C 761.679 954.621 1070.79 601.146 1389.55 513.084 z" />
<path d="M 1494.44 587.89 C 1524.5 579.666 1540.77 618.733 1515.32 632.093 C 1372.83 706.936 1263.57 829.757 1171.91 959.7 C 1139.08 1006.24 1014.2 1175.99 970.965 1198.24 C 947.407 1203.31 937.48 1187.92 938.726 1166.33 C 943.263 1158.9 948.465 1153.71 954.576 1147.64 C 1044.16 1059.82 1109.56 953.698 1185.75 855.011 C 1236.48 789.295 1408.64 609.858 1494.44 587.89 z" />
<path d="M 1401 947.194 C 1418.44 945.106 1439.07 954.077 1432.88 975.625 C 1421.27 1015.98 1145.22 1271.37 1107.5 1304.52 C 1042.37 1361.77 937.778 1453.82 860.358 1494.78 C 831.809 1503.03 814.986 1466.14 838.867 1449.69 C 1033.05 1315.88 1206.91 1153.58 1366.06 980.106 C 1377.42 967.723 1385.63 955.132 1401 947.194 z" />
<path d="M 527.238 530.327 C 549.916 527.673 555.705 542.414 564.717 559.516 C 588.26 604.191 625.411 639.87 670.146 662.994 C 694.395 675.528 698.057 700.841 670.807 714.082 C 626.871 720.144 456.047 564.148 527.238 530.327 z" />
<path d="M 660.624 459.076 C 692.344 451.902 788.132 507.422 813.942 524.957 C 838.38 541.56 870.027 569.964 826.773 587.89 C 807.313 588.78 792.835 572.687 776.913 563.127 C 746.27 544.726 715.103 527.143 681.523 514.651 C 670.352 510.495 655.348 506.303 645.654 499.785 C 635.539 480.478 638.726 465.073 660.624 459.076 z" />
<path d="M 482.823 714.082 C 516.153 713.195 527.206 774.263 543.132 805.241 C 552.817 824.079 600.887 864.414 554.102 881.663 C 513.988 888.85 477.292 790.388 467.37 760.79 C 460.711 740.926 458.572 720.487 482.823 714.082 z" />
</FilledIcon>
);
}
@ -161,34 +195,46 @@ export function DoughIcon() {
);
}
/** A glass of milk — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (non-cheese items). */
/** Milk carton (foodiconpack.com, CC BY 4.0) — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (non-cheese items). */
export function MilkIcon() {
return (
<Icon>
<path d="M9 2h6l1 4-1 2v11a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2V8L8 6z" />
<path d="M8 12h8" />
</Icon>
<FilledIcon>
<path d="M 779.86 268.928 C 786.444 267.776 808.108 268.835 816.425 268.869 L 1239.47 268.892 C 1251.42 268.887 1268.83 268.222 1280.22 270.224 C 1339.49 280.641 1344.2 346.945 1336.34 394.552 C 1333.06 414.436 1314.38 431.172 1297.92 441.343 C 1343.66 501.777 1394.45 593.378 1433.99 659.263 C 1437.18 664.566 1436.96 682.486 1436.96 688.894 L 1436.9 1658.43 C 1436.9 1715.13 1419.86 1772.69 1350.61 1776.96 C 1324.54 1778.56 1292.75 1777 1265.75 1776.99 L 753.887 1777.04 C 732.737 1777.04 709.252 1778.28 688.335 1775.73 C 599.133 1764.87 608.939 1673.48 609.023 1609.46 L 608.924 696.324 C 608.915 673.385 605.675 665.681 618.805 645.763 L 726.465 482.971 C 733.856 471.575 743.878 455.001 752.327 444.99 C 733.226 431.892 711.239 414.788 710.597 389.124 C 709.27 336.038 711.774 275.09 779.86 268.928 z M 655.997 1095.86 C 657.228 1141.69 655.977 1193.52 655.975 1239.96 L 656.084 1623.69 C 655.355 1718.28 650.722 1730.19 740.845 1730.1 L 816.5 1730 L 1290.37 1730.04 C 1386.14 1730.08 1389.84 1729.99 1389.77 1623.23 L 1389.71 1148.13 C 1326.18 1144.56 1312.21 1136.26 1263.63 1103.33 C 1228.78 1079.71 1200.2 1069.82 1158.5 1074.03 C 1070.35 1066.06 1015.65 1198.45 855.662 1123.37 C 838.033 1115.1 822.796 1101.08 805.94 1091.4 C 757.478 1065.7 701.947 1067.96 655.767 1096.35 L 655.997 1095.86 z M 655.997 690.535 C 655.415 763.136 655.026 841.111 655.997 913.692 C 746.649 887.151 780.349 903.836 853.481 945.995 C 930.868 991.394 983.241 907.17 1087.85 963.051 C 1113.47 976.738 1106.78 1005.09 1077.6 1006.99 C 1066.09 1006.09 1056.19 1001.04 1045.4 997.401 C 1009.64 985.352 979.087 995.291 943.904 1003.48 C 839.891 1027.7 809.781 942.719 713.57 950.335 C 693.938 951.889 673.206 956.794 655.997 966.626 C 656.723 992.751 655.768 1015.71 655.997 1041.3 C 694.992 1023.56 752.692 1022.39 793.757 1034.87 C 869.483 1061.19 899.87 1121.11 996.655 1089.42 C 1022.81 1081.16 1043.67 1061.28 1067.84 1048.91 C 1135 1012.98 1229.18 1018.3 1290.63 1064.07 C 1328.35 1092.16 1342.12 1096.6 1389.71 1101.24 L 1389.71 690.535 L 831.485 690.535 L 655.997 690.535 z M 811.491 314.999 C 747.852 317.269 756.708 337.816 757.996 390.049 L 767.34 398.009 C 780.479 399.085 802.178 398.047 816.382 398.026 L 1238.5 398.009 C 1261.58 398.307 1274.96 401.836 1292.06 385.935 C 1293.53 346.238 1300.37 314.504 1250.44 314.994 L 811.491 314.999 z M 1247.5 642.987 L 1370.52 642.987 C 1343.23 602.272 1316.23 557.822 1290.1 516.091 L 1268.91 482.018 C 1260.74 469.147 1253.32 459.132 1247.5 444.99 L 805.895 444.99 C 768.179 506.706 711.428 581.218 679.918 642.987 C 742.577 643.629 806.436 642.983 869.179 642.982 L 1247.5 642.987 z" />
<path d="M 1292.06 1165.05 C 1304.47 1164.51 1314.69 1166.59 1319.57 1179.63 C 1322.97 1188.7 1321.03 1350.47 1321.04 1369.31 L 1321.05 1461.22 C 1321.05 1481.43 1329.33 1524.23 1304.49 1531.79 C 1260.85 1538.42 1274.43 1475.3 1274.42 1445 L 1274.43 1237.1 C 1274.44 1210.88 1265.14 1174.39 1292.06 1165.05 z" />
<path d="M 1292.06 1574.06 C 1335.71 1570.9 1327.92 1660.46 1304.49 1669.63 C 1268.65 1672.77 1273.3 1645.42 1274.39 1620.96 C 1275.27 1601.16 1267.84 1583.13 1292.06 1574.06 z" />
</FilledIcon>
);
}
/** A wedge of cheese with holes — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (cheese items). */
/** Wedge of cheddar (foodiconpack.com, CC BY 4.0) — `CREMERIE_FROMAGE`/`PRODUITS_LAITIERS` (cheese items). */
export function CheeseIcon() {
return (
<Icon>
<path d="M3 18 20 8l1 3-3 9H4z" />
<circle cx="10" cy="15" r="1" fill="currentColor" stroke="none" />
<circle cx="15" cy="13" r="1" fill="currentColor" stroke="none" />
<circle cx="14" cy="17" r="1" fill="currentColor" stroke="none" />
</Icon>
<FilledIcon>
<path d="M 499.059 422.998 C 555.378 418.116 619.709 441.465 674.623 455.762 L 996.114 542.998 C 1031.04 552.641 1041.28 552.872 1077.38 553.146 L 1616.68 553.023 C 1682.85 553.02 1770.16 548.867 1778.65 640.355 C 1780.01 655.048 1779.18 672.498 1779.17 687.571 L 1779.16 1476.37 C 1779.25 1597.67 1748.78 1621.04 1630.31 1620.91 L 1465.34 1620.9 C 1443.94 1620.93 1414.19 1625.68 1399.87 1607.61 C 1398.05 1604.38 1394.76 1596.66 1393.72 1593.03 C 1377.2 1535.41 1297.2 1530.02 1271.36 1582.6 C 1248.88 1628.34 1248.64 1620.97 1198.19 1620.66 L 427.301 1620.9 C 358.787 1620.91 283.221 1625.78 266.731 1535.13 C 264.224 1521.36 265.662 1500.67 265.734 1486.47 L 265.757 1263.78 C 265.759 1237.69 263.842 1189.05 268.742 1163.04 C 271.121 1150.41 300.728 1146.42 313.047 1140.68 C 369.139 1114.52 366.57 1036.24 306.189 1016.72 C 262.308 1002.37 265.669 999.106 265.716 951.744 L 265.77 737.682 C 265.716 702.461 264.337 660.984 270.354 626.626 C 291.571 505.489 383.535 437.517 499.059 422.998 z M 324.231 960.286 C 424.782 991.538 442.508 1139.67 345.468 1188.98 C 338.454 1192.54 331.35 1195.88 324.231 1199.2 L 324.133 1487.36 C 324.051 1514.94 321.811 1559.19 360.901 1561.89 C 390.466 1563.93 421.893 1562.96 451.923 1562.94 L 1215.09 1562.94 C 1251.18 1483.5 1357.74 1460.52 1418.55 1523.85 C 1430.27 1536.56 1437.26 1547.46 1445.09 1562.94 C 1494.56 1564.28 1550.54 1562.91 1600.43 1562.92 L 1652.1 1563.01 C 1695.43 1563.09 1720.53 1558.29 1720.97 1507.06 L 1721.02 796.34 L 700.446 523.084 C 646.538 508.564 558.008 475.4 503.232 480.643 C 403.945 488.725 329.744 562.227 324.905 662.706 C 323.854 684.536 324.21 706.469 324.206 728.326 L 324.253 884.955 C 324.264 905.156 325.564 942.061 324.231 960.286 z M 1721.02 735.631 L 1721.29 682.195 C 1722.41 598.797 1694.33 611.308 1624.05 611.291 L 1259.29 610.969 C 1258.13 610.958 1256.06 611.184 1254.84 611.306 C 1292.78 627.458 1364.28 641.475 1405.34 652.014 L 1665.84 720.616 C 1672.26 722.287 1717.93 735.862 1721.02 735.631 z" />
<path d="M 734.395 1158.49 C 787.928 1151.1 837.236 1188.72 844.242 1242.31 C 851.249 1295.89 813.273 1344.93 759.641 1351.55 C 706.549 1358.11 658.114 1320.6 651.178 1267.55 C 644.241 1214.51 681.403 1165.81 734.395 1158.49 z M 752.487 1294.89 C 774.162 1292.22 789.602 1272.53 787.02 1250.85 C 784.437 1229.16 764.805 1213.65 743.111 1216.15 C 721.302 1218.67 705.69 1238.43 708.286 1260.23 C 710.882 1282.03 730.698 1297.56 752.487 1294.89 z" />
<path d="M 1483.12 874.242 C 1536.37 868.906 1583.87 907.74 1589.22 960.991 C 1594.58 1014.24 1555.76 1061.76 1502.51 1067.13 C 1449.24 1072.51 1401.69 1033.66 1396.34 980.387 C 1390.98 927.109 1429.84 879.58 1483.12 874.242 z M 1500.64 1007.39 C 1521.24 1002.81 1534.16 982.322 1529.42 961.758 C 1524.69 941.194 1504.1 928.432 1483.57 933.333 C 1463.27 938.18 1450.68 958.491 1455.37 978.826 C 1460.06 999.162 1480.27 1011.91 1500.64 1007.39 z" />
<path d="M 990.465 825.063 C 1043.7 818.146 1092.42 855.837 1099.09 909.108 C 1105.76 962.379 1067.84 1010.92 1014.54 1017.34 C 961.589 1023.72 913.435 986.109 906.808 933.185 C 900.181 880.262 937.573 831.934 990.465 825.063 z M 1008.88 959.34 C 1029.88 955.91 1044.22 936.238 1041.06 915.196 C 1037.91 894.153 1018.42 879.555 997.342 882.436 C 983.457 884.334 971.668 893.555 966.482 906.574 C 961.297 919.593 963.517 934.394 972.294 945.319 C 981.071 956.243 995.047 961.6 1008.88 959.34 z" />
<path d="M 1531.75 1315.16 C 1567.83 1310.92 1581.26 1372.96 1537.23 1379.82 C 1496.94 1381.97 1490.57 1325.05 1531.75 1315.16 z" />
<path d="M 1113.52 1341.48 C 1124.59 1339.75 1135.73 1344.09 1142.71 1352.86 C 1149.68 1361.63 1151.41 1373.46 1147.23 1383.86 C 1143.05 1394.25 1133.62 1401.6 1122.51 1403.11 C 1105.59 1405.4 1089.98 1393.64 1087.52 1376.74 C 1085.05 1359.85 1096.65 1344.12 1113.52 1341.48 z" />
<path d="M 1308.82 1150.12 C 1325.59 1147.1 1341.64 1158.21 1344.71 1174.97 C 1347.78 1191.73 1336.72 1207.82 1319.97 1210.95 C 1303.14 1214.1 1286.96 1202.97 1283.87 1186.13 C 1280.79 1169.29 1291.97 1153.15 1308.82 1150.12 z" />
<path d="M 786.604 693.812 C 803.652 692.684 818.375 705.618 819.451 722.67 C 820.528 739.722 807.549 754.405 790.494 755.43 C 773.512 756.451 758.905 743.54 757.833 726.56 C 756.761 709.581 769.628 694.935 786.604 693.812 z" />
<path d="M 501.378 1363.47 C 517.816 1361.46 532.869 1372.89 535.346 1389.26 C 537.823 1405.64 526.824 1421.01 510.528 1423.95 C 499.565 1425.93 488.393 1421.8 481.36 1413.16 C 474.328 1404.52 472.546 1392.74 476.709 1382.41 C 480.871 1372.07 490.321 1364.82 501.378 1363.47 z" />
<path d="M 502.61 674.93 C 513.474 673.325 524.359 677.705 531.083 686.388 C 537.807 695.071 539.323 706.706 535.05 716.823 C 530.777 726.939 521.379 733.964 510.467 735.198 C 493.988 737.061 479.058 725.37 476.914 708.926 C 474.77 692.481 486.204 677.353 502.61 674.93 z" />
<path d="M 640.602 933.6 C 650.874 930.436 662.055 933.036 669.876 940.408 C 677.698 947.781 680.954 958.788 678.402 969.229 C 675.85 979.67 667.884 987.935 657.543 990.868 C 641.802 995.333 625.399 986.3 620.758 970.61 C 616.117 954.92 624.965 938.417 640.602 933.6 z" />
</FilledIcon>
);
}
/** An egg — `CREMERIE_FROMAGE`/`OEUFS`. */
/** Eggs (foodiconpack.com, CC BY 4.0) — `CREMERIE_FROMAGE`/`OEUFS`. */
export function EggIcon() {
return (
<Icon>
<path d="M12 3C8 8 6 12.5 6 15.5a6 6 0 0 0 12 0C18 12.5 16 8 12 3z" />
</Icon>
<FilledIcon>
<path d="M 672.028 452.989 C 802.231 431.822 916.044 572.595 976.073 668.56 C 982.025 662.6 990.897 647.221 996.637 639.262 C 1056.03 556.903 1134.48 467.569 1240.98 454.261 C 1440.34 428.671 1578.12 667.305 1635.4 824.202 C 1650.71 866.151 1669.24 919.792 1674.32 963.863 C 1796.44 1050.34 1809.04 1195.17 1745.42 1324.69 C 1688.22 1441.14 1595.52 1527.32 1473.73 1571.05 C 1298.56 1633.57 1065.66 1573.42 958.295 1417.43 C 945.299 1429.42 925.899 1441.05 910.854 1450.18 C 632.974 1618.83 279.351 1421.44 265.261 1103.37 C 257.624 930.964 357.165 692.936 470.568 565.306 C 519.686 510.026 595.86 455.977 672.028 452.989 z M 662.243 1464 C 757.01 1464.06 858.31 1447.38 929.301 1378.54 C 814.231 1173.44 836.302 990.945 923.922 774.62 C 931.423 756.102 942.19 737.296 948.796 718.683 C 900.647 639.142 870.484 578.847 783.776 525.855 C 762.865 513.075 703.956 491.421 679.695 500.115 C 592.129 496.732 497.313 595.998 452.999 664.544 C 376.729 782.52 321.573 926.983 311.732 1067.93 C 300.626 1267.53 463.183 1450.54 662.243 1464 z M 1557.87 1477.91 C 1576.89 1460.83 1595.24 1447.76 1613.92 1429.68 C 1709.13 1337.16 1790.75 1153.1 1687.79 1036.91 C 1631.4 973.276 1537.02 953.186 1461.36 993.787 C 1448.89 1000.48 1438.13 1009.84 1426.48 1017.79 C 1369.1 1056.81 1294.02 1057.84 1233.37 1025.8 C 1180.17 997.693 1161.91 968.725 1087.78 972.993 C 807.16 1011.04 898.602 1337.03 1073.46 1465.96 C 1200.69 1561.72 1376.3 1576.66 1517.03 1502.16 C 1526.94 1496.91 1548.72 1480.65 1557.87 1477.91 z M 906.453 1017.56 C 979.391 933.074 1088.63 900.45 1194.17 947.19 C 1205.36 952.146 1219.05 961.719 1229.41 968.383 C 1288.55 1006.6 1355.65 1018.41 1413.05 970.324 C 1468.19 924.129 1552.4 915.263 1619.91 938.773 C 1597.48 779.347 1431.48 482.377 1253.19 500.115 C 1048.63 534.486 925.124 838.141 906.453 1017.56 z" />
<path d="M 1641.18 1164.04 C 1655.96 1163.87 1667.24 1172.92 1666.31 1187.92 C 1655.64 1360.64 1490.45 1471.56 1326.72 1477.91 C 1313.35 1477.35 1308.32 1475.65 1301.24 1464 C 1298.93 1437.3 1309.46 1432.84 1334.84 1431.93 C 1467.26 1427.15 1593.28 1334.34 1618.18 1200.99 C 1619.88 1191.88 1620.69 1182.45 1623.87 1173.73 C 1630.25 1167.66 1632.69 1166.33 1641.18 1164.04 z" />
<path d="M 1571.63 1070.83 C 1584.72 1067.48 1598.05 1075.41 1601.34 1088.51 C 1604.64 1101.62 1596.65 1114.91 1583.52 1118.14 C 1570.49 1121.36 1557.3 1113.43 1554.03 1100.41 C 1550.75 1087.39 1558.62 1074.17 1571.63 1070.83 z" />
<path d="M 1141.5 1043.64 C 1154.47 1043.06 1165.52 1052.98 1166.35 1065.94 C 1167.17 1078.91 1157.46 1090.14 1144.52 1091.21 C 1136.66 1091.85 1128.99 1088.57 1124.04 1082.45 C 1119.08 1076.32 1117.47 1068.14 1119.74 1060.59 C 1122.65 1050.89 1131.38 1044.1 1141.5 1043.64 z" />
<path d="M 1036.59 1114.73 C 1049.08 1110.47 1062.65 1117.2 1066.8 1129.73 C 1070.95 1142.26 1064.08 1155.77 1051.52 1159.8 C 1039.12 1163.77 1025.83 1157.02 1021.73 1144.66 C 1017.64 1132.3 1024.27 1118.95 1036.59 1114.73 z" />
</FilledIcon>
);
}
@ -203,67 +249,69 @@ export function SproutIcon() {
);
}
/** A shaker — dried spices/herbs (`CONDIMENTS_EPICES`/`EPICES`). */
/** Cinnamon sticks (foodiconpack.com, CC BY 4.0) — dried spices/herbs (`CONDIMENTS_EPICES`/`EPICES`). */
export function SpiceIcon() {
return (
<Icon>
<rect x="8" y="8" width="8" height="13" rx="3" />
<path d="M10 8V5a2 2 0 0 1 4 0v3" />
<path d="M11 12h.01M13 12h.01M10.5 15h.01M13.5 15h.01M12 18h.01" />
</Icon>
<FilledIcon>
<path d="M 1299.23 499.315 C 1359.06 491.799 1390.3 536.11 1425.43 572.134 C 1496.5 553.162 1542.93 603.387 1572.87 661.811 C 1661.4 617.869 1693.59 665.685 1739.02 734.286 C 1766.76 776.167 1793.52 800.671 1780.45 855.484 C 1769.46 901.584 1737.1 918.753 1700.9 943.085 L 956.229 1453.57 C 881.032 1505.41 824.394 1564.64 727.781 1541.31 C 622.715 1509.59 598.438 1427.43 620.532 1331.46 C 593.107 1328.96 556.54 1312.76 537.915 1293.61 C 371.504 1425.39 178.623 1225.58 306.96 1081.41 C 326.186 1059.81 363.923 1042.27 388.969 1028.76 L 1109.88 640.263 C 1131.52 628.662 1150.34 620.99 1170.36 606.2 C 1223.61 566.87 1210.47 510.761 1299.23 499.315 z M 698.223 1153.03 C 685.564 1124.01 644.122 1103.62 652.014 1083.32 C 656.117 1072.77 665.7 1067.69 676.173 1065.41 L 677.523 1065.87 C 730.347 1084.36 771.371 1175.97 760.119 1229.24 L 760.916 1230.44 C 774.747 1226.74 829.275 1193.14 844.192 1184.62 L 1535.71 800.412 C 1597.34 766.704 1560.33 730.548 1538.73 696.859 C 1516.84 662.716 1510.46 619.145 1455.71 615.368 C 1443.29 616.688 1414.94 634.577 1402.39 641.277 L 879.346 923.682 C 841.372 943.965 556.595 1088.5 539.86 1109.98 C 489.374 1177.33 562.598 1300.61 654.807 1286.88 C 719.275 1277.29 729.816 1200 698.223 1153.03 z M 1598.52 704.214 C 1629.22 745.717 1610.85 806.574 1570.27 832.144 C 1528.59 858.413 1480.95 881.992 1438 906.347 L 748.377 1294.08 C 726.513 1306.49 700.23 1324.6 676.173 1331.46 C 642.753 1368.48 656.169 1436.47 691.565 1468.81 C 756.913 1528.51 865.349 1504.56 850.662 1403.24 C 846.258 1385.45 842.637 1374.38 832.194 1358.52 C 820.284 1340.43 782.796 1327.27 797.906 1301.05 C 804.787 1289.11 820.472 1287.3 830.907 1294.95 C 880.916 1329.97 899.477 1376.66 898.999 1435.05 C 919.996 1424.24 936.911 1410.37 956.218 1397.18 L 1666.61 909.295 C 1783.99 829.202 1730.19 819.511 1676.19 724.443 C 1665.66 705.918 1654.98 693.562 1631.02 693.416 C 1617.19 695.209 1610.81 697.532 1598.52 704.214 z M 424.954 1289.68 C 456.981 1288.58 481.153 1279.67 505.982 1258.74 C 482.024 1207.75 464.9 1174.25 485.217 1114.52 C 501.004 1068.11 539.721 1052.14 579.79 1030.9 L 1208.36 692.006 C 1262.31 662.286 1332.68 630.795 1383.21 598.302 C 1360.55 570.383 1339.51 542.641 1299.23 546.102 C 1275.97 551.717 1257.21 564.624 1246.66 586.131 C 1218.85 642.791 1177.24 657.382 1124.55 685.304 L 459.432 1043.62 C 436.067 1056.26 356.059 1096.31 341.245 1113.44 C 276.781 1187.98 331.053 1287.67 424.954 1289.68 z" />
<path d="M 1360.08 777.002 C 1380.21 776.904 1390.76 790.877 1384.27 811.255 C 1381.62 819.58 1275.59 878.124 1250.15 886.907 C 1226.82 891.473 1218 863.959 1226.13 851.7 C 1233.68 840.3 1344.34 781.861 1360.08 777.002 z" />
<path d="M 589.734 1153.03 C 603.824 1152.09 645.581 1173.62 661.273 1178.77 C 683.217 1185.98 681.408 1216.89 657.535 1224.03 C 632.942 1225.26 519.247 1177.53 589.734 1153.03 z" />
<path d="M 723.057 1360.97 C 731.948 1360.68 812.223 1392.94 814.451 1405.67 C 817.702 1424.25 811.966 1434.23 794.522 1438.58 C 774.923 1438.59 648.922 1386.08 723.057 1360.97 z" />
<path d="M 379.771 1163.13 C 388.374 1162.08 439.518 1180.94 450.159 1186.2 C 472.912 1198.35 467.313 1225.91 442.076 1230.67 C 427.597 1229.29 389.88 1214.43 374.813 1208.91 C 351.499 1200.37 351.948 1171.93 379.771 1163.13 z" />
</FilledIcon>
);
}
/** A condiment jar — 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 (`CONDIMENTS_EPICES`/`SAUCES` and the `EPICERIE_SECHE`/`AUTRES` catch-all). */
export function JarIcon() {
return (
<Icon>
<rect x="6" y="9" width="12" height="12" rx="2" />
<path d="M9 9V6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" />
<path d="M6 13h12" />
</Icon>
<FilledIcon>
<path d="M 441.421 842.864 C 447.786 842.313 464.211 842.793 471.395 842.807 L 1019.7 842.764 C 1032.89 842.741 1048.9 841.881 1061.84 843.353 C 1130.81 851.198 1163.98 918.556 1130.28 977.676 C 1193.73 1004.99 1195.17 1074.2 1153.32 1125.1 C 1169.13 1142.38 1192.59 1189.25 1202.41 1211.85 C 1250.22 1321.88 1252.68 1457.38 1205.46 1568.25 C 1179.01 1630.35 1126.32 1716.02 1060.36 1743.03 C 1018.13 1757.99 939.663 1751.84 893.846 1751.84 L 558.891 1751.86 C 485.108 1751.86 456.534 1757.33 399.168 1702.94 C 252.041 1563.46 226.559 1333.79 330.44 1161.63 C 336.605 1151.42 346.62 1133.29 354.405 1125.1 C 309.176 1074.05 316.393 1016.01 373.247 977.676 C 341.618 912.507 363.601 854.3 441.421 842.864 z M 675.49 1147.95 C 674.654 1202.31 686.841 1260.99 645.515 1301.98 C 605.095 1342.08 544.747 1339.55 505.417 1299.64 C 465.481 1259.11 483.176 1198.65 479.076 1147.95 L 402.038 1147.95 C 273.352 1299.41 297.324 1545.93 446.301 1676.73 C 482.42 1708.44 508.476 1701.08 553.305 1701.08 L 904.022 1701.09 C 937.651 1701.09 987.565 1702.82 1019.49 1700.23 C 1092.21 1694.33 1158.11 1565.46 1173.88 1504.48 C 1208.54 1389.77 1185.08 1240.15 1104.25 1147.95 L 1006.64 1147.91 C 955.106 1147.75 964.099 1136.4 950.944 1168.76 C 917.416 1251.22 805.109 1248.39 785.535 1147.95 C 750.82 1147.92 709.692 1146.79 675.49 1147.95 z M 830.367 1101.07 L 837.774 1107.07 C 837.983 1134.23 832.163 1176.53 873.289 1174.99 L 874.247 1174.95 C 927.062 1157.23 886.505 1109.61 920.058 1097.59 C 945.687 1088.41 1065.03 1106.22 1103.86 1094.32 C 1140.07 1083.22 1145.85 1020.86 1080.75 1017.52 C 1055.39 1016.22 1029.65 1017.07 1004.25 1017.07 L 448.493 1017.07 C 427.043 1018.23 402.017 1013.88 386.352 1031.96 C 371.738 1048.83 372.852 1073.39 390.862 1087.43 C 419.378 1109.66 500.067 1085.09 523.258 1103.42 C 535.236 1112.88 531.224 1139.24 531.155 1152.37 L 530.839 1214.29 C 530.615 1307.53 614.177 1284.58 621.411 1240.4 C 623.374 1228.41 622.701 1213.42 622.655 1201.04 L 622.434 1142.74 C 622.287 1095.93 627.961 1096.54 672.476 1096.79 L 719.123 1097 C 742.002 1097.03 812.594 1092.57 830.367 1101.07 z M 1064.76 965.18 C 1108.65 949.333 1095.92 898.437 1057.26 895.318 C 1035.28 893.545 1006.38 894.91 983.609 894.932 L 441.421 894.985 C 396.1 901.206 401.684 964.819 454.169 964.945 L 990.398 965.186 C 1007.59 965.192 1049.73 966.614 1064.76 965.18 z" />
<path d="M 1289.39 292.367 C 1326.12 288.265 1352.75 308.707 1374.41 335.781 C 1418.32 306.712 1471.33 336.401 1495.2 378.493 C 1544.75 348.864 1592.39 377.915 1621.27 421.001 C 1671.12 420.78 1704.66 417.497 1743.36 455.769 C 1792.33 504.191 1793.6 591.975 1743.75 640.152 C 1705.04 677.57 1670.58 674.943 1621.27 674.832 C 1596.59 737.221 1552.79 745.834 1495.2 729.12 C 1472.47 770.532 1441.21 787.563 1394.57 778.379 C 1360.92 771.752 1380.82 770.173 1357.74 794.106 C 1310.02 844.096 1227.55 821.602 1211.46 754.37 C 1206.62 734.136 1208.65 704.247 1208.65 682.677 L 1208.7 581.596 L 1035.75 581.596 L 377.727 581.595 C 350.11 581.589 322.276 581.811 294.694 581.208 C 260.937 580.471 261.039 531.119 291.666 530.646 C 324.929 529.931 358.287 530.245 391.571 530.245 L 1208.7 530.263 C 1209.51 520.792 1208.8 501.437 1208.77 491.101 L 1208.73 381.027 C 1208.97 333.971 1241.73 296.636 1289.39 292.367 z M 1298.86 770.005 C 1313 767.514 1329.13 756.138 1329.26 740.59 L 1329.03 442.72 C 1329.03 411.9 1342.02 337.471 1292.72 343.188 C 1246.23 352.56 1259.98 412.832 1259.97 452.092 L 1259.96 688.959 C 1259.96 719.956 1252.41 770.674 1298.86 770.005 z M 1379.63 708.828 C 1390.61 719.865 1399.84 729.244 1416.52 729.12 C 1456.74 728.388 1450.53 683.254 1450.87 655.181 L 1451.07 469.27 C 1451.05 439.366 1460.98 378.539 1416.52 378.493 C 1372.3 382.068 1379.61 426.387 1379.61 459.837 L 1379.6 654.043 C 1379.58 669.976 1378.68 693.565 1379.63 708.828 z M 1541.6 687.466 C 1578 681.587 1574.03 642.844 1574.04 615.382 L 1574.03 489.826 C 1574.01 470.817 1572.28 404.792 1534.72 421.001 C 1506.93 420.198 1502.06 439.052 1501.89 462.447 L 1501.79 631.805 C 1501.87 659.021 1504.66 689.717 1541.6 687.466 z M 1624.49 624.418 C 1656.15 624.69 1683.84 627.86 1707.91 603.745 C 1736.48 570.656 1737.04 517.576 1703.82 486.797 C 1682.99 467.498 1651.28 471.716 1624.49 471.965 L 1624.45 572.781 C 1624.45 588.207 1623.7 609.679 1624.49 624.418 z" />
</FilledIcon>
);
}
/** A bottle — oils, vinegars (`CONDIMENTS_EPICES`/`ASSAISONNEMENTS`, the pourable subset). */
/** Oil bottle (foodiconpack.com, CC BY 4.0) — oils, vinegars (`CONDIMENTS_EPICES`/`ASSAISONNEMENTS`, the pourable subset). */
export function BottleIcon() {
return (
<Icon>
<path d="M10 2h4v3.5l1.5 2V20a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V7.5L10 5.5z" />
<path d="M9.5 11h5" />
</Icon>
<FilledIcon>
<path d="M 1113 267.774 C 1118.93 267.838 1127.18 268.144 1132.79 267.575 C 1212.81 259.459 1291.26 334.177 1283.92 411.9 C 1320.46 426.872 1328.11 431.256 1348.99 466.537 C 1460.44 475.628 1555.19 437.956 1647.13 535.703 C 1705.58 597.853 1723.55 687.084 1698.19 767.766 C 1677.99 832.062 1578.77 923.958 1525.17 973.224 C 1578.32 1063.57 1591.03 1155.94 1591.05 1257.58 L 1591.36 1579.29 C 1591.7 1675.52 1530.2 1758.25 1434.53 1774.01 C 1404.53 1778.95 1376.38 1777.9 1346.23 1777.89 L 868.5 1777.85 C 817.993 1777.93 770.594 1783.03 726.147 1753.63 C 714.384 1745.84 702.133 1731.64 691.185 1725.99 C 670.808 1725.81 650.472 1726.52 630.147 1724.67 C 462.136 1709.37 366.732 1559.53 343.149 1404.74 C 325.158 1286.65 348.66 1166.97 389.895 1055.99 C 399.753 1029.46 408.922 999.898 429.075 979.14 C 503.845 902.125 585.964 984.301 642.107 1039.44 C 671.069 971.711 701.229 920.107 761.04 872.291 C 835.931 812.42 889.919 772.757 892.018 668.347 C 848.185 630.588 849.467 598.554 850.18 545.65 C 851.157 473.261 847.573 441.474 922.991 411.9 C 916.203 374.993 935.26 330.694 960.986 304.454 C 1004.3 260.277 1056.4 267.649 1113 267.774 z M 943.863 676.997 C 935.593 780.389 891.969 841.391 811.421 898.559 C 738.881 950.044 717.919 999.669 683.124 1075.69 C 718.508 1107.43 763.536 1151.02 793.91 1187.54 C 965.064 1029.92 1243.58 1024.81 1407.43 1196.22 C 1496.92 1289.84 1465.7 1413.13 1376.97 1494.79 C 1265.66 1597.23 1103.91 1616.32 962.658 1573.91 C 943.497 1567.53 925.733 1559.93 907.115 1552.11 C 874.749 1630.34 827.889 1676.83 750.715 1710.27 C 773.015 1721.48 788.554 1726.23 813.901 1726.13 L 1278.76 1725.99 C 1319.97 1726 1361.16 1726.13 1402.31 1725.57 C 1470.93 1724.62 1532.2 1669.6 1537.89 1600.7 C 1539.51 1581.09 1539.03 1561.64 1539.04 1541.91 L 1539.02 1298 C 1539.02 1153 1528.08 1021.46 1412.67 917.064 C 1373.18 881.342 1338.53 868.866 1305.8 822.534 C 1269.26 770.804 1262.58 738.027 1257.7 676.997 C 1240.38 680.459 1195.72 676.996 1176.1 676.986 L 1065.74 677.04 C 1051.68 677.053 1024.61 677.931 1011.73 676.997 C 993.467 677.85 961.651 678.239 943.863 676.997 z M 1495.04 929.233 C 1498.73 925.689 1502.55 921.909 1506.35 918.51 C 1522.16 905.383 1542.38 881.856 1557.55 867.044 C 1607.92 817.833 1649.8 786.169 1656.89 710.328 C 1661.24 663.759 1647.67 616.675 1618.03 580.681 C 1568.2 520.175 1514.1 518.813 1445.06 518.785 L 1351.01 518.841 L 1351.01 586.996 L 1446.99 586.952 C 1489.39 586.937 1526.58 582.18 1559.47 614.942 C 1606.7 661.976 1598.63 732.85 1552.52 778.096 L 1448.79 878.929 C 1461.81 889.697 1485.88 914.78 1495.04 929.233 z M 936.782 463.972 C 887.714 471.621 902.402 557.987 904.235 597.116 C 906.068 633.523 962.311 625.555 985.507 625.53 L 1203.16 625.533 C 1223.95 625.552 1246.82 626.359 1267.25 625.51 C 1292.35 624.448 1300.31 610.245 1300.03 586.659 C 1298.91 492.084 1326.13 463.961 1221.92 463.944 L 937.416 463.9 C 937.204 463.924 936.993 463.948 936.782 463.972 z M 1406.94 846.536 C 1423.38 832.363 1438.29 815.198 1454.25 800.985 C 1480.96 774.681 1538.13 728.203 1539.12 694.413 C 1540.91 632.943 1480.07 640.114 1440.08 640.261 L 1348.99 640.346 L 1337.81 640.346 C 1329.89 654.324 1323.13 658.656 1310.42 668.347 C 1308.51 755.891 1341.97 798.976 1406.94 846.536 z M 1154.01 411.9 C 1179.4 411.398 1205.04 411.981 1230.48 411.9 C 1234.21 366.538 1199.22 322.81 1152.61 321.161 C 1124.22 320.156 1079 318.458 1051.13 320.788 C 996.889 323.351 974.266 363.057 975.002 411.9 C 992.225 412.919 1016.43 411.918 1034.46 411.904 L 1154.01 411.9 z M 1077.65 1119.02 C 1006.5 1126.09 919.413 1149.46 863.243 1196.96 C 938.217 1212.28 999.725 1254.37 1070.65 1280.46 C 1108.58 1294.42 1137.83 1297.54 1176.69 1302 C 1259.65 1302.52 1312.94 1289.81 1387.05 1253.24 C 1329.02 1166.36 1182.39 1112.07 1077.65 1119.02 z M 840.44 1243.81 C 845.432 1252.5 853.131 1260.87 859.13 1268.88 C 897.565 1320.2 923.184 1382.53 922.872 1447.31 C 922.791 1464.28 921.612 1482.96 922.991 1499.54 C 934.619 1509.64 955.708 1517.09 970.31 1521.94 C 1105.09 1566.65 1256.66 1544.09 1358.35 1440.01 C 1397.17 1394.75 1410.91 1360.52 1410.01 1302 L 1408.42 1302.37 C 1395.32 1305.59 1377.94 1314.66 1365.06 1320.03 C 1294.67 1349.36 1212.93 1361.15 1137.19 1351.3 C 1041.82 1338.9 943.039 1272.16 840.44 1243.81 z M 495.507 999.144 C 453.53 1008.01 440.935 1065.13 428.748 1100.97 C 371.204 1269.09 360.088 1532 533.268 1638.59 C 598.933 1679 675.355 1687.18 746.437 1655.94 C 859.966 1603.37 901.891 1465.68 850.084 1355.36 C 800.354 1249.47 644.644 1104.07 549.506 1027.25 C 535.226 1015.89 514.258 997.224 495.507 999.144 z" />
<path d="M 490.113 1171.08 C 526.074 1169.48 523.674 1192.77 517.597 1218.79 C 501.423 1288.06 501.968 1347.56 517.363 1416.46 C 514 1429.79 511.1 1433.52 498.914 1440.01 C 429.934 1456.78 461.2 1243.09 466.534 1210.88 C 469.864 1190.77 469.321 1178.34 490.113 1171.08 z" />
<path d="M 1067.99 1178.67 C 1084.54 1178.69 1113.69 1177.22 1129 1179.34 C 1158.36 1183.39 1158.55 1230.38 1120.86 1230.63 C 1104.98 1230.58 1058.92 1234.3 1047.71 1223.62 C 1028.23 1205.06 1038.37 1179.18 1067.99 1178.67 z" />
</FilledIcon>
);
}
/** A glass — 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 (`CONDIMENTS_EPICES`/`ASSAISONNEMENTS`'s drinkable subset). */
export function DrinkIcon() {
return (
<Icon>
<path d="M6 3h12l-1.5 16.5A2 2 0 0 1 14.5 21h-5a2 2 0 0 1-2-1.5z" />
<path d="M7 9h10" />
</Icon>
<FilledIcon>
<path d="M 1267.05 274.485 C 1325.51 271.562 1335.62 290.356 1378.17 311.904 C 1386.06 307.975 1394.21 301.843 1401.71 297.203 C 1487.29 244.219 1616.96 297.102 1629.14 392.932 C 1734.88 398.312 1783.1 466.039 1783.5 563.848 L 1783.53 722.652 C 1783.49 830.259 1784.98 944.69 1727.69 1040.56 C 1659.77 1154.23 1656 1272.82 1655.97 1400.89 L 1656.05 1650.68 C 1656.02 1758.75 1621.13 1773.1 1521.07 1773.05 L 1259.5 1772.99 C 1231.98 1773.01 1203.87 1773.38 1176.4 1772.76 C 1160.67 1772.41 1144.87 1763.27 1128.16 1763.17 L 721.75 1763.02 L 588.607 1763 C 566.914 1763 544.536 1763.73 522.85 1763 C 456.776 1760.77 396.557 1710.05 383.737 1645.07 C 379.527 1623.73 380.955 1598.24 380.976 1576.49 L 381.024 1300.03 C 381.034 1273.38 380.023 1242.55 381.015 1216.37 C 333.984 1195.75 300.304 1175.9 277.68 1126.87 C 243.634 1053.08 280.583 957.367 355.016 924.877 C 355.565 907.942 353.79 884.25 356.109 868.341 C 372.867 753.37 467.596 697.043 578.822 710.005 C 632.246 644.377 685.672 601.154 778.393 611.55 C 850.674 619.653 890.27 657.13 932.09 710.005 L 964.049 710.005 C 963.638 624.745 947.76 498.792 1007.33 438.183 C 1037.36 407.629 1072.52 393.572 1114.67 392.932 C 1139.14 323.206 1192.38 279.094 1267.05 274.485 z M 1730 694.012 L 1016.32 694.012 C 1012.45 844.295 1013.09 909.673 1082.42 1040.58 C 1136.35 1152.04 1146.02 1252.12 1146.03 1374.77 L 1145.97 1611.06 C 1146.01 1682.73 1128.6 1719.82 1210.62 1719.9 L 1529.39 1719.92 C 1568.14 1719.97 1601.9 1723.64 1602.95 1675.39 C 1603.31 1647.44 1603.01 1619.46 1603.02 1591.5 L 1602.99 1414.19 C 1603.01 1277.52 1605.79 1170.54 1664.47 1044.8 C 1672.02 1028.62 1682.56 1012.72 1690.2 996.562 C 1732.11 907.991 1734.92 792.128 1730 694.012 z M 1064.3 1143.88 C 1060.97 1106.23 1018.67 1037.53 1001.98 1000.28 C 966.107 920.223 962.451 850.873 964.049 765.017 C 954.263 765.07 944.392 765.287 934.535 765.178 C 887.998 764.665 900.936 757.388 876.025 725.296 C 849.552 689.761 785.191 651.167 740.498 664.397 C 636.802 675.565 623.761 764.277 597.178 766.129 C 584.972 767.073 571.518 762.206 558.876 760.856 C 472.166 751.595 401.323 821.727 408.888 908 C 410.726 966.867 422.178 951.207 373.4 976.923 C 250.053 1041.95 354.688 1216.46 466.223 1151.84 C 499.004 1133 512.803 1095.67 562.276 1135.27 C 592.735 1159.65 588.097 1198.25 588.077 1232.88 L 587.986 1314.65 C 587.942 1343.37 583.657 1394.57 632.277 1379.55 C 658.53 1361.57 654.122 1349.31 654.111 1319.44 L 654.033 1252.24 C 654.004 1207.85 650.169 1176.52 691.026 1145.51 C 737.98 1109.87 775.436 1148.14 817.218 1149.31 C 841.254 1149.99 852.643 1149.72 875.92 1141.99 C 926.399 1125.21 906.257 1119.18 955.5 1138.03 C 992.889 1152.34 1026.34 1156.21 1064.3 1143.88 z M 433.999 1216.37 L 433.999 1550.43 C 475.685 1551.17 519.63 1550.43 561.541 1550.43 L 921.821 1550.38 C 938.878 1550.35 956.176 1550.16 973.027 1550.52 C 997.119 1551.04 1008.11 1578.93 993.12 1595.17 C 981.426 1607.83 945.098 1603.81 928.188 1603.81 L 565.302 1603.82 C 522.605 1603.82 476.507 1602.95 433.999 1603.84 C 434.202 1669.45 473.124 1710.64 541.776 1710.03 L 765.4 1709.63 L 1054.87 1709.55 C 1066.79 1709.42 1084.27 1708.4 1095.35 1709.63 C 1091.23 1685.26 1093.01 1631.26 1093.02 1603.84 C 1080.62 1604.81 1070.37 1606.45 1059.82 1598.09 C 1034.03 1577.67 1055.71 1549.13 1093.02 1550.43 L 1093.05 1401.69 C 1093.08 1325.3 1094.42 1270.1 1078.85 1193.74 C 1027.34 1219.45 971.168 1195.86 922.099 1183.84 C 909.765 1185.27 893.686 1193.02 880.867 1196.36 C 828.579 1209.97 789.396 1203.07 740.498 1183.84 C 713.843 1188.12 707.598 1201.37 706.941 1227 C 705.854 1269.36 711.046 1322.31 705.54 1363.54 C 697.442 1424.18 618.55 1456.12 569.879 1419.16 C 528.58 1389.91 533.915 1354.11 533.976 1309.77 L 534.185 1227.98 C 534.411 1208.95 539.307 1188.09 526.31 1172.61 C 495.427 1200.14 474.835 1210.36 433.999 1216.37 z M 1267.05 327.463 C 1149.56 339.214 1179.51 443.167 1135.59 443.939 C 996.152 443.732 1022.17 546.05 1016.32 641.005 C 1071.8 641.819 1129.63 641.006 1185.28 641.004 L 1520.25 641.005 L 1730 641.005 C 1726.75 542.326 1749.52 446.998 1610.37 443.425 C 1581.03 442.672 1585.35 410.202 1575.44 391.464 C 1549.22 336.931 1486.59 311.569 1431.89 340.646 C 1419.5 346.18 1392.44 368.293 1379.03 368.873 C 1353.16 369.99 1325.15 326.369 1267.05 327.463 z" />
<path d="M 856.545 913.549 C 866.417 911.644 876.552 915.188 883.085 922.83 C 889.618 930.472 891.543 941.034 888.127 950.49 C 884.71 959.946 876.478 966.838 866.569 968.539 C 851.467 971.131 837.092 961.08 834.344 946.004 C 831.596 930.929 841.499 916.452 856.545 913.549 z" />
<path d="M 728.404 789.654 C 737.003 786.898 746.405 788.493 753.615 793.928 C 760.826 799.364 764.946 807.964 764.664 816.989 C 764.296 828.745 756.569 838.996 745.369 842.586 C 730.752 847.272 715.103 839.221 710.419 824.603 C 705.734 809.985 713.786 794.338 728.404 789.654 z" />
<path d="M 625.937 963.277 C 640.819 961.847 654.064 972.7 655.588 987.572 C 657.111 1002.44 646.341 1015.76 631.478 1017.37 C 616.484 1019 603.027 1008.12 601.49 993.113 C 599.953 978.109 610.923 964.72 625.937 963.277 z" />
</FilledIcon>
);
}
/** A stockpot — broths, stocks, water bases (`AIDES_CULINAIRES`/`BASES`'s liquid-base subset). */
/** Stockpot (foodiconpack.com, CC BY 4.0) — broths, stocks, water bases (`AIDES_CULINAIRES`/`BASES`'s liquid-base subset). */
export function StockPotIcon() {
return (
<Icon>
<rect x="4" y="10" width="16" height="10" rx="2" />
<path d="M2 12h2M20 12h2" />
<path d="M9 10V7a3 3 0 0 1 6 0v3" />
</Icon>
<FilledIcon>
<path d="M 956.689 498.026 L 1074.61 497.958 C 1137.01 497.691 1174.39 532.451 1174.48 597.104 C 1265.87 599.475 1358.96 595.285 1450.52 597.066 C 1551.82 599.036 1640.84 682.526 1638.99 785.253 C 1622.4 813.785 1611.98 785.183 1613 828.678 C 1639.21 825.311 1672.61 823.696 1698.78 818.712 C 1763.14 806.46 1805.62 869.267 1772.1 924.572 C 1750.8 959.702 1653.92 968.017 1613 976.35 L 1613.05 1323.99 C 1613.06 1352.99 1614.47 1381.48 1607.44 1409.65 C 1580.36 1518.21 1497.47 1548.07 1399.95 1547.95 L 1089.99 1547.91 L 663.723 1547.96 C 564.944 1547.94 471.975 1528.47 441.558 1414.71 C 432.646 1381.38 435.528 1339.45 435.545 1304.36 L 435.707 1017.76 C 435.822 956.555 438.134 978.73 368.191 964.482 C 334.482 957.614 301.664 960.737 279.03 929.523 C 256.702 898.732 261.782 855.965 292.641 833.135 C 320.337 812.91 350.476 820.964 381.769 826.296 L 435.591 835.521 C 435.902 826.211 435.618 815.518 435.591 806.105 C 421.651 801.018 412.723 797.094 412.559 780.488 C 410.831 699.26 472.524 621.061 551.615 603.033 C 592.371 593.743 648.136 597.11 691.291 597.11 L 875.146 597.104 C 871.062 542.426 901.02 503.078 956.689 498.026 z M 478.871 806.105 C 477.554 845.634 478.872 892.42 478.876 932.718 L 478.841 1313.78 C 478.773 1369.65 473.787 1415.83 516.462 1459.99 C 546.181 1490.75 580.38 1505.29 623.081 1504.86 L 1419.04 1505.01 C 1452.51 1505.2 1469.64 1502.53 1499.23 1486.78 C 1571.64 1439.94 1569.41 1384.54 1569.36 1306.19 L 1569.36 929.821 C 1569.37 891.098 1570.64 844.093 1569.34 806.105 L 528.372 806.105 C 512.279 806.198 494.852 806.61 478.871 806.105 z M 1264.5 762.998 L 1594.19 762.998 C 1586.34 721.917 1562.92 688.244 1528.97 664.169 C 1487.57 634.821 1441.84 640.706 1393.89 640.809 L 788.11 640.848 L 665.057 640.812 C 633.374 640.813 587.735 638.189 558.195 646.073 C 508.836 659.248 463.194 707.895 457.828 759.35 C 457.629 761.31 457.844 761.459 459.043 762.615 C 489.947 763.762 525.361 763.005 556.59 763.009 L 1264.5 762.998 z M 325.946 863.171 C 316.949 867.888 313.801 870.39 309.051 879.906 C 307.696 930.337 378.413 921.02 421.834 930.35 C 426.15 930.259 431.368 929.994 435.591 930.35 C 434.898 913.263 435.666 896.899 435.591 879.906 C 417.924 875.099 342.207 861.758 325.946 863.171 z M 956.689 541.052 C 922.748 546.685 916.547 566.328 917.645 597.104 L 1093.5 597.104 L 1130.73 597.104 C 1129.13 534.666 1094.11 540.855 1046.78 541.012 L 956.689 541.052 z M 1613 873.877 L 1613 930.35 C 1622.19 929.498 1629.94 928.246 1638.99 926.739 C 1666.38 921.093 1695.84 916.564 1722.96 910.501 C 1748.41 904.812 1745.27 855.434 1711.78 863.171 C 1684.18 864.363 1640.88 870.069 1613 873.877 z" />
</FilledIcon>
);
}
/** A sugar cube — `AIDES_CULINAIRES`/`SUCRES`. */
/** Sugar (foodiconpack.com, CC BY 4.0) — `AIDES_CULINAIRES`/`SUCRES`. */
export function SugarIcon() {
return (
<Icon>
<path d="M4 8 12 4l8 4-8 4z" />
<path d="M4 8v8l8 4 8-4V8" />
<path d="M12 12v8" />
</Icon>
<FilledIcon>
<path d="M 563.343 266.546 C 579.356 265.999 598.229 266.52 614.537 266.523 L 1165.31 266.51 C 1214.01 266.5 1267.02 255.617 1296.26 302.682 C 1323.29 346.167 1275.54 419.411 1264.41 467.576 C 1285.74 508.561 1315.63 552.485 1339.39 592.89 C 1380.58 662.943 1373.02 694.734 1373.02 771.075 L 1373.04 1068.44 C 1373.05 1091.57 1373.19 1114.08 1372.67 1137.74 C 1373.67 1157.45 1347.53 1169.18 1333.48 1156.58 C 1319.4 1143.95 1323.15 1103.91 1323.13 1086.51 L 1323.23 998.001 C 1323.35 921.011 1321.79 841.992 1323.04 765.249 C 1302.66 765.841 1263.43 767.077 1243.62 763.124 C 1209.14 756.243 1194.07 747.655 1156.63 747.651 C 1097.23 747.646 1050.15 776.567 981.876 761.345 C 948.843 753.98 929.738 747.792 895.257 747.199 C 833.498 746.136 788.155 775.837 714.942 762.372 C 621.546 745.195 631.078 743.658 537.012 759.443 C 519.46 762.388 489.969 762.19 471.997 762.221 L 471.995 1371.67 C 472.002 1396.15 472.97 1424.92 471.997 1448.84 L 597.5 1448.84 L 936.881 1448.84 C 951.994 1448.71 966.891 1448.41 981.989 1448.91 C 1013.24 1449.97 1014.95 1496.54 985.493 1498.58 C 968.334 1499.76 948.038 1498.99 930.633 1498.99 L 471.997 1498.96 C 471.9 1615.07 488.067 1658.29 618.58 1658.04 L 920.519 1658.05 C 941.436 1658 962.351 1657.71 983.263 1657.98 C 1023.72 1658.51 1019.41 1701.53 1001.19 1705 C 973.159 1710.33 929.732 1708.06 902.015 1708.05 L 632.237 1708.04 C 610.876 1708.04 589.378 1708.56 568.066 1707.31 C 490.732 1702.79 424.454 1638.6 422.594 1560.23 C 422.045 1537.12 422.175 1514.18 422.199 1491.06 L 422.163 798.425 C 422.067 611.542 423.495 654.442 516.951 491.098 C 522.459 481.473 529.099 472.824 533.608 462.528 C 524.397 441.605 516.805 417.28 507.32 397.071 C 479.406 337.599 491.407 270.834 563.343 266.546 z M 581.2 444.248 L 858.635 444.207 C 877.707 444.202 918.616 439.037 917.922 470.882 C 917.289 499.919 877.231 495.793 856.563 495.719 L 575.303 495.627 C 555.982 521.64 542.019 551.259 524.773 578.28 C 494.345 625.954 472.748 652.065 471.997 711.023 C 508.075 714.232 541.943 706.338 576.962 699.622 C 653.495 684.942 727.737 724.071 802.658 710.056 C 841.73 700.588 902.783 691.423 942.696 700.048 C 970.712 706.101 991.978 712.195 1020.99 713.991 C 1085.47 714.159 1144.93 686.96 1210.17 702.913 C 1261.33 714.301 1273.21 710.564 1323.04 713.991 C 1322.45 696.464 1322.69 677.961 1317.12 661.13 C 1311.61 644.465 1231.69 507.757 1221.07 497.37 C 1197.05 488.286 1118.19 504.122 1091.8 492.457 C 1072.38 483.875 1074.24 452.977 1094.37 445.85 C 1105.49 441.914 1200.19 444.253 1218.49 444.248 C 1224.39 414.175 1250.62 365.264 1255.09 334.51 C 1240.34 308.577 1197.61 318.459 1168.14 318.51 L 615.35 318.531 C 600.872 318.528 580.593 317.824 566.686 318.553 C 513.479 321.249 572.707 415.367 581.2 444.248 z" />
<path d="M 979.84 444.248 C 1053.88 437.921 1046.87 490.04 1011.25 495.627 C 929.626 498.405 950.269 448.02 979.84 444.248 z" />
<path d="M 1396.75 1217.47 C 1443.62 1216.15 1464.72 1245.77 1494.53 1275.78 C 1565.63 1347.33 1623.96 1384.38 1535.89 1472 L 1539.17 1471.74 C 1583.19 1468.48 1626.03 1501.91 1626.71 1547.84 C 1627.54 1603.5 1637.63 1714.43 1605.34 1752.92 C 1575.25 1788.59 1526.31 1777.12 1485.51 1778.93 C 1441.76 1778.97 1394.42 1787.64 1359.68 1755.43 C 1348.72 1763.2 1342.39 1767.96 1330.25 1773.29 C 1298.34 1783.83 1255.57 1778.81 1222.29 1778.9 C 1181.48 1779.02 1140.16 1786.51 1108.24 1755.67 C 1072.31 1720.97 1081.98 1654.29 1082 1607.15 C 1082.01 1583.57 1079.27 1553.57 1085.21 1530.78 C 1105.39 1453.44 1204.35 1472.15 1264.41 1472 C 1175.55 1397.29 1237.85 1339.22 1296.83 1283.61 C 1333.08 1249.42 1344.73 1222.99 1396.75 1217.47 z M 1247.12 1728.54 C 1321.35 1728.84 1337.7 1737.1 1336.56 1662.79 C 1336.02 1627.15 1337.61 1591.34 1335.69 1555.74 C 1333.59 1516.93 1302.77 1521.22 1275.51 1521.35 L 1218.49 1521.43 C 1198.62 1521.35 1133.22 1514.22 1132.35 1546.35 C 1131.11 1592.23 1132.44 1638.8 1132.04 1684.78 C 1131.18 1732.14 1158.14 1728.7 1194.19 1728.63 L 1247.12 1728.54 z M 1273.02 1405.46 C 1290.13 1431.7 1353.26 1490.83 1377.71 1512.41 C 1385.03 1518.87 1394.84 1519.46 1404.08 1519.51 C 1424.76 1511.3 1510.19 1429.67 1526.99 1405.46 L 1526.91 1403.58 C 1525.97 1379.71 1527.17 1378.58 1508.46 1360.21 C 1486.32 1338.47 1425.85 1265.45 1396.75 1267.2 C 1373.17 1272.79 1305.16 1345.42 1286.86 1364.36 C 1272.58 1379.15 1271.92 1385.74 1273.02 1405.46 z M 1385.12 1569.55 L 1385.1 1660 C 1385.17 1745.39 1402.36 1728.92 1477.75 1728.54 C 1544.14 1728.7 1578.04 1744.59 1578.07 1671.14 L 1578.06 1591.77 C 1577.93 1508.63 1563.04 1518.16 1483.56 1519.51 C 1457.92 1546.93 1437.27 1567.87 1396.85 1569.25 L 1385.12 1569.55 z" />
</FilledIcon>
);
}

View file

@ -237,6 +237,19 @@
}
}
// App version a quiet diagnostic footnote below the account menu, not
// an interactive element (hence `aria-hidden` on the `<p>` in
// AppLayout.tsx). Hidden whenever space is at a premium the icon-only
// rail and the mobile horizontal bar (see the `.collapsed` block and the
// `@media (max-width: 640px)` block below).
&__version {
margin: var(--space-xs) 0 0;
padding: 0 var(--space-sm);
font-size: var(--font-size-xs);
color: var(--color-text-muted);
text-align: center;
}
// --- Collapsed (icon-only rail) state -----------------------------------
// A single class toggle on the root element every nested rule below
// just hides labels/chevrons and re-centers icons via CSS, no child
@ -282,6 +295,10 @@
display: none;
}
.app-sidebar__version {
display: none;
}
// The popover would otherwise shrink to the icon rail's own width,
// squashing "Mon compte"/"Se déconnecter" give it a normal,
// comfortable width instead, still anchored to the rail's left edge.
@ -342,6 +359,11 @@
display: none;
}
// No room for this on a horizontal bar either.
&__version {
display: none;
}
&__nav {
flex: 1 1 auto;
min-width: 0;

View file

@ -6,6 +6,7 @@ import "./AppLayout.scss";
import {
AccountIcon,
ChevronLeftIcon,
CreditsIcon,
DietPreferencesIcon,
HouseholdIcon,
PlanningIcon,
@ -38,6 +39,7 @@ const SETTINGS_ITEMS = [
{ to: "/parametres/preferences", key: "preferences", Icon: DietPreferencesIcon },
{ to: "/parametres/foyer", key: "household", Icon: HouseholdIcon },
{ to: "/parametres/preferences-utilisateur", key: "userPreferences", Icon: UserPreferencesIcon },
{ to: "/parametres/credits", key: "credits", Icon: CreditsIcon },
] as const;
/**
@ -85,7 +87,7 @@ export function AppLayout() {
onClick={toggleCollapsed}
title={t(isCollapsed ? "layout.sidebar.expand" : "layout.sidebar.collapse")}
>
<ChevronLeftIcon />
<ChevronLeftIcon aria-hidden="true" />
</button>
</div>
@ -102,7 +104,7 @@ export function AppLayout() {
className={({ isActive }) => (isActive ? "active" : undefined)}
title={t(`layout.nav.${key}`)}
>
<Icon />
<Icon aria-hidden="true" />
<span className="label">{t(`layout.nav.${key}`)}</span>
</NavLink>
))}
@ -110,6 +112,9 @@ export function AppLayout() {
<SettingsMenu />
<AccountMenu />
<p className="app-sidebar__version" aria-hidden="true">
v{__APP_VERSION__}
</p>
</aside>
<main className="app-content">
@ -146,7 +151,7 @@ function SettingsMenu() {
title={t("layout.settings.toggle")}
>
<span className="app-sidebar__settings-toggle-left">
<SettingsIcon />
<SettingsIcon aria-hidden="true" />
<span className="label">{t("layout.settings.toggle")}</span>
</span>
<span className="chevron" aria-hidden="true">
@ -163,7 +168,7 @@ function SettingsMenu() {
className={({ isActive }) => (isActive ? "active" : undefined)}
title={t(`layout.settings.nav.${key}`)}
>
<Icon />
<Icon aria-hidden="true" />
<span className="label">{t(`layout.settings.nav.${key}`)}</span>
</NavLink>
))}

View file

@ -1,130 +1,28 @@
import type { ReactNode } from "react";
// Small, hand-drawn line-icon set for the sidebar nav (24×24 viewBox,
// matches the reviewed mockup — see the plan/PR description) rather than
// pulling in an icon library for a handful of glyphs. Sized entirely via
// CSS (`.app-sidebar__nav svg` etc., see AppLayout.scss) — no width/height
// attribute here, so the same markup works at any size the caller picks.
// `aria-hidden` on every icon: each one is always paired with visible text
// (the nav label, or a `title` tooltip when collapsed) that already
// conveys the meaning — the icon itself is decorative.
function Icon({ children }: { children: ReactNode }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
{children}
</svg>
);
}
export function PlanningIcon() {
return (
<Icon>
<rect x="3" y="4" width="18" height="18" rx="2" />
<path d="M16 2v4M8 2v4M3 10h18" />
</Icon>
);
}
export function RecipesIcon() {
return (
<Icon>
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
</Icon>
);
}
export function ShoppingListIcon() {
return (
<Icon>
<circle cx="9" cy="21" r="1" />
<circle cx="20" cy="21" r="1" />
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
</Icon>
);
}
export function SettingsIcon() {
return (
<Icon>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</Icon>
);
}
export function AccountIcon() {
return (
<Icon>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</Icon>
);
}
export function DietPreferencesIcon() {
return (
<Icon>
<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z" />
<path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12" />
</Icon>
);
}
export function HouseholdIcon() {
return (
<Icon>
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<path d="M9 22V12h6v10" />
</Icon>
);
}
export function UserPreferencesIcon() {
return (
<Icon>
<circle cx="13.5" cy="6.5" r=".5" />
<circle cx="17.5" cy="10.5" r=".5" />
<circle cx="8.5" cy="7.5" r=".5" />
<circle cx="6.5" cy="12.5" r=".5" />
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.9 0 1.5-.7 1.5-1.5 0-.4-.2-.8-.4-1.1-.2-.3-.4-.6-.4-1 0-.8.7-1.5 1.5-1.5H16c3.3 0 6-2.7 6-6 0-4.4-4-8-10-8z" />
</Icon>
);
}
export function ChevronLeftIcon() {
return (
<Icon>
<path d="M15 18l-6-6 6-6" />
</Icon>
);
}
/** Favorites — the recipe catalog's "Favoris" tab (`RecipeTabs`) and the recipe detail panel's favorite toggle (`FavoriteStarButton`). */
export function FavoriteIcon() {
return (
<Icon>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</Icon>
);
}
/** Public recipes — the recipe catalog's "Publique" tab (`RecipeTabs`). */
export function PublicIcon() {
return (
<Icon>
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20" />
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</Icon>
);
}
// Sidebar/nav icon set — thin, named re-exports of lucide-react glyphs
// rather than importing `lucide-react` directly in every consumer. Keeps a
// single place documenting "which glyph stands for which app concept"
// (the mapping itself, decided once, isn't obvious from a bare `Settings`
// or `Home` import) and keeps consumer diffs small if a glyph ever needs
// to change. Previously a hand-drawn custom SVG set (24×24, stroke-based,
// no fill) — lucide-react uses the same stroke-based "line icon" language
// (default `strokeWidth={2}`, round caps/joins) so the switch is visually
// a no-op, just swaps who draws the paths.
//
// Every one of these is always paired with visible text (the nav label, or
// a `title` tooltip when the sidebar is collapsed) — purely decorative, so
// every consumer passes `aria-hidden="true"` itself (unlike the old custom
// `Icon` wrapper, lucide doesn't set this by default).
export {
Calendar as PlanningIcon,
BookOpen as RecipesIcon,
ShoppingCart as ShoppingListIcon,
Settings as SettingsIcon,
User as AccountIcon,
Leaf as DietPreferencesIcon,
Home as HouseholdIcon,
Palette as UserPreferencesIcon,
Info as CreditsIcon,
ChevronLeft as ChevronLeftIcon,
Star as FavoriteIcon,
Globe as PublicIcon,
} from "lucide-react";

View file

@ -0,0 +1,24 @@
/**
* Generates a client-only, ephemeral identity for a draft list row (a
* recipe's ingredient lines/steps while editing see `RecipeFormPage`,
* `StepListEditor`) used purely as a React `key`/local identity to keep
* each row's own input state stable across reorders, never sent to the
* server or persisted anywhere.
*
* `crypto.randomUUID()` looks like the obvious choice, but the Web Crypto
* API only defines it in a "secure context" https, or the literal
* hostname `localhost` and throws `TypeError: crypto.randomUUID is not a
* function` everywhere else: a LAN IP while testing on a real device, a
* Capacitor WebView's `capacitor://` origin (this app's eventual mobile
* wrapper see `AllergySelect`'s doc comment for the same "mobile via
* Capacitor" context), plain `http://` on a real domain before TLS is set
* up. None of that matters for a value that only ever needs to be *unique
* within this one render session*, not cryptographically random, so this
* never touches `crypto` at all.
*/
let counter = 0;
export function makeClientKey(): string {
counter += 1;
return `${counter}-${Math.random().toString(36).slice(2)}`;
}

View file

@ -78,7 +78,8 @@
"account": "Compte",
"preferences": "Préférences alimentaires",
"household": "Foyer",
"userPreferences": "Préférences utilisateur"
"userPreferences": "Préférences utilisateur",
"credits": "Crédits"
}
},
"accountMenu": {
@ -260,6 +261,14 @@
"SYSTEM": "Système"
}
},
"credits": {
"title": "Crédits",
"icons": {
"title": "Icônes d'ingrédients",
"description": "La plupart des icônes d'ingrédients de l'application proviennent des collections gratuites « Common ingredient icons » et « Common Utensils » de Food Icon Pack, sous licence Creative Commons Attribution 4.0. Elles ont été recolorées pour s'adapter aux thèmes clair et sombre de l'application.",
"licenseLabel": "Licence CC BY 4.0"
}
},
"household": {
"title": "Foyer",
"form": {
@ -288,5 +297,469 @@
"confirmButton": "Confirmer la suppression",
"cancelButton": "Annuler"
}
},
"catalog": {
"diets": {
"omnivore": "Omnivore",
"vegetarian": "Végétarien",
"vegan": "Végan",
"pescatarian": "Pescétarien",
"gluten_free": "Sans gluten"
},
"allergens": {
"gluten": "Gluten",
"crustaceans": "Crustacés",
"eggs": "Œufs",
"fish": "Poissons",
"peanuts": "Arachides",
"soy": "Soja",
"milk": "Lait",
"tree_nuts": "Fruits à coque",
"celery": "Céleri",
"mustard": "Moutarde",
"sesame_seeds": "Graines de sésame",
"sulfites": "Sulfites",
"lupin": "Lupin",
"molluscs": "Mollusques"
},
"ingredients": {
"tomato": "Tomate",
"onion": "Oignon",
"shallot": "Échalote",
"garlic": "Ail",
"carrot": "Carotte",
"zucchini": "Courgette",
"cucumber": "Concombre",
"gherkins": "Cornichons",
"bell_pepper": "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",
"spinach": "Épinard",
"swiss_chard": "Blette",
"lettuce": "Salade",
"arugula": "Roquette",
"watercress": "Cresson",
"leek": "Poireau",
"celery": "Céleri",
"radish": "Radis",
"beetroot": "Betterave",
"turnip": "Navet",
"parsnip": "Panais",
"green_bean": "Haricot vert",
"pea": "Petit pois",
"corn": "Maïs",
"artichoke": "Artichaut",
"fennel": "Fenouil",
"endive": "Endive",
"pumpkin": "Potiron",
"butternut_squash": "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",
"shiitake": "Shiitake",
"daikon": "Daikon",
"fresh_green_chili": "Piment vert frais",
"lemon": "Citron",
"lime": "Citron vert",
"apple": "Pomme",
"pear": "Poire",
"banana": "Banane",
"orange": "Orange",
"clementine": "Clémentine",
"grapefruit": "Pamplemousse",
"strawberry": "Fraise",
"raspberry": "Framboise",
"blueberry": "Myrtille",
"blackberry": "Mûre",
"cherry": "Cerise",
"apricot": "Abricot",
"peach": "Pêche",
"plum": "Prune",
"grape": "Raisin",
"melon": "Melon",
"watermelon": "Pastèque",
"pineapple": "Ananas",
"mango": "Mangue",
"kiwi": "Kiwi",
"fig": "Figue",
"date": "Datte",
"lychee": "Litchi",
"pomegranate": "Grenade",
"rhubarb": "Rhubarbe",
"quince": "Coing",
"basil": "Basilic",
"parsley": "Persil",
"thyme": "Thym",
"rosemary": "Romarin",
"bay_leaf": "Laurier",
"chives": "Ciboulette",
"fresh_cilantro": "Coriandre fraîche",
"mint": "Menthe",
"oregano": "Origan",
"dill": "Aneth",
"tarragon": "Estragon",
"savory": "Sarriette",
"marjoram": "Marjolaine",
"sage": "Sauge",
"chervil": "Cerfeuil",
"ginger": "Gingembre",
"lemongrass": "Citronnelle",
"kaffir_lime": "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",
"lamb": "Agneau",
"leg_of_lamb": "Gigot d'agneau",
"bacon_lardons": "Lardons",
"bacon": "Bacon",
"ham": "Jambon blanc",
"cured_ham": "Jambon cru",
"sausage": "Saucisse",
"chorizo": "Chorizo",
"merguez": "Merguez",
"prosciutto": "Prosciutto",
"pancetta": "Pancetta",
"mortadella": "Mortadelle",
"salami": "Salami",
"chicken": "Poulet",
"turkey": "Dinde",
"duck": "Canard",
"duck_breast": "Magret de canard",
"salmon": "Saumon",
"tuna": "Thon",
"cod": "Cabillaud",
"trout": "Truite",
"sardine": "Sardine",
"anchovy": "Anchois",
"whiting": "Merlan",
"surimi": "Surimi",
"sea_bass": "Bar (loup de mer)",
"sea_bream": "Dorade",
"sole": "Sole",
"turbot": "Turbot",
"hake": "Merlu",
"pollock": "Colin",
"saithe": "Lieu noir",
"haddock": "Églefin",
"mackerel": "Maquereau",
"herring": "Hareng",
"red_mullet": "Rouget",
"skate": "Raie",
"monkfish": "Lotte",
"halibut": "Flétan",
"swordfish": "Espadon",
"carp": "Carpe",
"pike": "Brochet",
"perch": "Perche",
"tilapia": "Tilapia",
"pangasius": "Panga",
"smoked_salmon": "Saumon fumé",
"dried_fish": "Poisson séché",
"shrimp": "Crevettes",
"langoustine": "Langoustines",
"lobster": "Homard",
"crab": "Crabe",
"spiny_lobster": "Langouste",
"mussels": "Moules",
"oysters": "Huîtres",
"scallops": "Saint-Jacques",
"squid": "Calamar",
"octopus": "Poulpe",
"clams": "Palourdes",
"whelks": "Bulots",
"semolina": "Semoule",
"couscous": "Couscous",
"bulgur": "Boulgour",
"polenta": "Polenta",
"quinoa": "Quinoa",
"pasta": "Pâtes",
"whole_wheat_pasta": "Pâtes complètes",
"rice": "Riz",
"basmati_rice": "Riz basmati",
"brown_rice": "Riz complet",
"oats": "Flocons d'avoine",
"spaghetti": "Spaghetti",
"penne": "Penne",
"tagliatelle": "Tagliatelles",
"lasagna_sheets": "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",
"chickpeas": "Pois chiches",
"white_beans": "Haricots blancs",
"kidney_beans": "Haricots rouges",
"black_beans": "Haricots noirs",
"split_peas": "Pois cassés",
"fava_beans": "Fèves",
"edamame": "Edamame",
"pinto_beans": "Haricots pinto",
"peanuts_shelled": "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",
"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",
"bread": "Pain",
"sandwich_bread": "Pain de mie",
"whole_wheat_bread": "Pain complet",
"baguette": "Baguette",
"rye_bread": "Pain de seigle",
"breadcrumbs": "Chapelure",
"burger_bun": "Pain à burger",
"brioche_bun": "Pain brioché",
"hot_dog_bun": "Pain à hot-dog",
"pita_bread": "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",
"rusk": "Biscotte",
"croutons": "Croûtons",
"focaccia": "Focaccia",
"ciabatta": "Ciabatta",
"corn_tortilla": "Tortilla de maïs",
"wheat_tortilla": "Tortilla de blé",
"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",
"milk": "Lait",
"butter": "Beurre",
"creme_fraiche": "Crème fraîche",
"liquid_cream": "Crème liquide",
"cheese": "Fromage",
"emmental": "Emmental",
"gruyere": "Gruyère",
"parmesan": "Parmesan",
"mozzarella": "Mozzarella",
"goat_cheese": "Chèvre (fromage)",
"feta": "Feta",
"comte": "Comté",
"fromage_blanc": "Fromage blanc",
"mascarpone": "Mascarpone",
"yogurt": "Yaourt",
"burrata": "Burrata",
"ricotta": "Ricotta",
"pecorino": "Pecorino",
"gorgonzola": "Gorgonzola",
"cheddar": "Cheddar",
"egg": "Œuf",
"coconut_milk": "Lait de coco",
"coconut_cream": "Crème de coco",
"almond_milk": "Lait d'amande",
"oat_milk": "Lait d'avoine",
"tofu": "Tofu",
"silken_tofu": "Tofu soyeux",
"herbes_de_provence": "Herbes de Provence",
"black_pepper": "Poivre noir",
"paprika": "Paprika",
"espelette_pepper": "Piment d'Espelette",
"cayenne_pepper": "Piment de Cayenne",
"cumin": "Cumin",
"curry_powder": "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",
"sumac": "Sumac",
"nigella": "Nigelle",
"allspice": "Quatre épices",
"colombo_powder": "Colombo (poudre)",
"baharat": "Baharat",
"horseradish": "Raifort",
"herb_salt": "Sel aux herbes",
"celery_salt": "Sel de céleri",
"fleur_de_sel": "Fleur de sel",
"salt": "Sel",
"five_spice": "Cinq épices",
"garam_masala": "Garam masala",
"coriander_seeds": "Graines de coriandre",
"cardamom": "Cardamome",
"fenugreek": "Fenugrec",
"jalapeno": "Piment jalapeño",
"chipotle": "Piment chipotle",
"poblano_pepper": "Piment poblano",
"habanero": "Piment habanero",
"ras_el_hanout": "Ras el hanout",
"zaatar": "Za'atar",
"soy_sauce": "Sauce soja",
"mustard": "Moutarde",
"mayonnaise": "Mayonnaise",
"ketchup": "Ketchup",
"tabasco": "Tabasco",
"worcestershire_sauce": "Sauce Worcestershire",
"fish_sauce": "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",
"chimichurri": "Chimichurri",
"red_pesto": "Pesto rouge (tomates séchées)",
"pesto": "Pesto",
"oyster_sauce": "Sauce huître",
"hoisin_sauce": "Sauce hoisin",
"sriracha": "Sauce sriracha",
"sweet_chili_sauce": "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ï)",
"tahini": "Tahini",
"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",
"capers": "Câpres",
"olives": "Olives",
"white_wine": "Vin blanc (cuisine)",
"red_wine": "Vin rouge (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",
"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",
"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)",
"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",
"dashi": "Dashi (bouillon japonais)",
"shellfish_bisque": "Bisque de crustacés",
"tapioca_flour": "Farine de tapioca",
"masa_harina": "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",
"cornstarch": "Maïzena",
"lupin_flour": "Farine de lupin",
"gelatin": "Gélatine",
"baking_soda": "Bicarbonate de soude",
"potato_starch": "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"
}
}
}

View file

@ -15,6 +15,7 @@ import { IngredientPicker } from "../features/recipes/IngredientPicker";
import { IngredientRow } from "../features/recipes/IngredientRow";
import { type StepDraft, StepListEditor } from "../features/recipes/StepListEditor";
import "../features/recipes/recipes.scss";
import { makeClientKey } from "../lib/client-key";
import { errorMessageService } from "../services/error-message.service";
/** In display order — mirrors `RecipeVisibility` (schema.prisma/shared types). */
@ -82,7 +83,7 @@ export function RecipeFormPage() {
setDietIds(recipe.diets.map((diet) => diet.id));
setIngredientLines(
recipe.ingredients.map((line) => ({
key: crypto.randomUUID(),
key: makeClientKey(),
ingredient: line.ingredient,
quantity: String(line.quantity),
unit: line.unit,
@ -90,7 +91,7 @@ export function RecipeFormPage() {
);
setSteps(
recipe.steps.map((step) => ({
key: crypto.randomUUID(),
key: makeClientKey(),
description: step.description,
picture: step.picture ?? "",
})),
@ -110,7 +111,7 @@ export function RecipeFormPage() {
function addIngredient(ingredient: IngredientView) {
setIngredientLines((lines) => [
...lines,
{ key: crypto.randomUUID(), ingredient, quantity: "", unit: "" },
{ key: makeClientKey(), ingredient, quantity: "", unit: "" },
]);
}

View file

@ -31,7 +31,7 @@ export function RecipesPage() {
const { id } = useParams<{ id: string }>();
const selectedId = id !== undefined ? Number(id) : null;
const [activeTab, setActiveTab] = useState<RecipeTab>("publique");
const [activeTab, setActiveTab] = useState<RecipeTab>("favoris");
const [search, setSearch] = useState("");
const [debouncedSearch, setDebouncedSearch] = useState("");
const [listState, setListState] = useState<RecipeListState>({ status: "loading" });

View file

@ -0,0 +1,37 @@
import { useTranslation } from "react-i18next";
import "./settings-pages.scss";
/**
* Third-party attribution routed at `/parametres/credits`. Exists solely
* to satisfy the CC BY 4.0 requirement on the ingredient icon set (see
* `features/recipes/ingredient-icons.tsx`'s `FilledIcon`-based components):
* most of it comes from foodiconpack.com's free "Common ingredient icons"
* and "Common Utensils" collections, which require crediting the source,
* linking the license, and noting that the icons were adapted (recolored
* to `currentColor` for theming, original size/style attributes dropped
* see the generation note in `ingredient-icons.tsx`). Nothing else in the
* app currently needs a credits entry grow this page if that changes.
*/
export function CreditsPage() {
const { t } = useTranslation();
return (
<div className="settings-page">
<h1>{t("credits.title")}</h1>
<div className="settings-page__section">
<h2 className="settings-page__section-title">{t("credits.icons.title")}</h2>
<p className="settings-page__hint">{t("credits.icons.description")}</p>
<p className="settings-page__hint">
<a href="https://foodiconpack.com" target="_blank" rel="noreferrer">
foodiconpack.com
</a>
{" — "}
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer">
{t("credits.icons.licenseLabel")}
</a>
</p>
</div>
</div>
);
}

View file

@ -2,6 +2,7 @@ import { ErrorCode, THEME_PREFERENCES, type ThemePreference } from "@batch-cooki
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ApiError } from "../../api/client";
import { RadioOption } from "../../components/ui/Radio";
import { useTheme } from "../../features/theme/ThemeContext";
import { errorMessageService } from "../../services/error-message.service";
import "./settings-pages.scss";
@ -46,23 +47,16 @@ export function UserPreferencesPage() {
{THEME_PREFERENCES.map((option) => {
const checked = theme === option;
return (
<label
<RadioOption
key={option}
// `is-selected` (not a `:has(:checked)` CSS rule) drives the
// selected look — see AllergySelect.tsx for why this stays
// in JS rather than pure CSS.
className={`theme-select__option${checked ? " is-selected" : ""}`}
name="theme"
value={option}
checked={checked}
onChange={handleChange}
className="theme-select__option"
>
<input
type="radio"
name="theme"
value={option}
checked={checked}
onChange={() => handleChange(option)}
/>
<span className="check-mark" aria-hidden="true" />
{t(`userPreferences.theme.${option}`)}
</label>
</RadioOption>
);
})}
</fieldset>

View file

@ -8,7 +8,15 @@
// household/regime/allergies page was split in three.
// =============================================================================
// Centered column, not pinned to the content area's left edge — on a wide
// desktop viewport that left plenty of unused space beside these forms.
// Wide enough to give grids (the allergy checkboxes, the ingredient
// picker's category chips) real room, still well short of `.app-content`'s
// full width so line lengths/inputs don't stretch edge-to-edge.
.settings-page {
max-width: 56rem;
margin: 0 auto;
&__status {
color: var(--color-text-muted);
font-size: var(--font-size-md);
@ -21,9 +29,10 @@
// Each setting lives in its own card, same surface treatment used
// elsewhere (see .planning-table in HomePage.scss) reads as a distinct,
// self-contained unit rather than one long form.
// self-contained unit rather than one long form. Width comes from
// `.settings-page` above, not its own cap, now that it's the one
// establishing the centered column every section sits in.
.settings-page__section {
max-width: 32rem;
margin-top: var(--space-lg);
padding: var(--space-lg);
background: var(--color-surface);
@ -127,15 +136,21 @@
// visually distinct, consistent "danger" treatment wherever they appear.
.settings-page__danger-zone {
margin-top: var(--space-lg);
border-color: var(--color-error);
border: 1.5px solid var(--color-error);
background: color-mix(in srgb, var(--color-error) 8%, var(--color-surface));
}
.settings-page__danger-button {
// `button.` prefix (not just `.settings-page__danger-button`) to outrank
// the generic `.settings-page button` rule above a class selector alone
// has lower specificity than "class + element", so without it the generic
// gray fill/border was winning and this never actually rendered red (same
// fix as `button.settings-page__link-button` below, same reason).
button.settings-page__danger-button {
color: #fff;
background: var(--color-error);
border-color: var(--color-error);
&:hover {
&:hover:not(:disabled) {
opacity: 0.9;
}
}

View file

@ -60,15 +60,16 @@ h1 {
// `display: none`) and the whole label row it lives in becomes the
// interactive surface instead: a flat bordered box that fills in with a
// tinted background + primary border once selected, with a checkmark
// fading in on the trailing edge.
// fading in on the leading edge.
//
// The base (unselected) look below is detected structurally with `:has()`
// safe, since "does this label contain a checkbox/radio" never changes
// after mount. The *selected* look is instead driven by the `is-selected`
// class each caller (AllergySelect.tsx, UserPreferencesPage.tsx) toggles
// in JS from the same boolean it already passes to `checked` chaining a
// second `:has(:checked)` to react to that live state turned out to be
// unreliable across browsers, so this only needs one always-true `:has()`.
// class {@link CheckboxOption}/{@link RadioOption} (components/ui/) toggle
// in JS from the same boolean their caller already passes to `checked`
// chaining a second `:has(:checked)` to react to that live state turned
// out to be unreliable across browsers, so this only needs one
// always-true `:has()`.
//
// Every checkbox/radio in the app goes through this one place (the allergy
// grid, the theme picker, anywhere future) rather than each feature styling
@ -126,15 +127,15 @@ input[type="radio"] {
opacity: 0;
}
// The checkmark a real element (see AllergySelect.tsx /
// UserPreferencesPage.tsx) shown via the same `is-selected` class as the
// label's own look above, not a separate CSS-only trigger. Scaled in from
// nothing so toggling has a bit of motion. Same mark for both checkbox and
// radio: one consistent "selected" language app-wide rather than a
// checkmark here and a dot there.
// The checkmark a real element (see components/ui/Checkbox.tsx /
// Radio.tsx) shown via the same `is-selected` class as the label's own
// look above, not a separate CSS-only trigger. Scaled in from nothing so
// toggling has a bit of motion. Same mark for both checkbox and radio: one
// consistent "selected" language app-wide rather than a checkmark here and
// a dot there. Sits first in the row (before the label text, per DOM
// order) a classic "control on the left" layout rather than trailing.
.check-mark {
flex: none;
margin-left: auto;
width: 0.9rem;
height: 0.9rem;
background: var(--color-primary);

5
apps/web/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/// <reference types="vite/client" />
// Injected by `define` in vite.config.ts, sourced from package.json's
// version field — see AppLayout.tsx for where it's rendered.
declare const __APP_VERSION__: string;

View file

@ -1,6 +1,11 @@
import { readFileSync } from "node:fs";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// Read once at config-eval time — cheaper than a plugin hook, and this file
// only ever runs in Node (dev server / build), never bundled into the app.
const pkg = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf-8"));
export default defineConfig({
plugins: [react()],
css: {
@ -13,4 +18,10 @@ export default defineConfig({
},
},
},
// Exposes package.json's version as a compile-time constant — see
// src/vite-env.d.ts for the matching ambient declaration, and
// AppLayout.tsx for where it's rendered.
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
},
});

View file

@ -1,10 +1,17 @@
/**
* 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).
*/
export interface DietView {
id: number;
name: string;
key: string;
}
/**
@ -18,17 +25,19 @@ export type AllergenKind = "ALLERGY" | "INTOLERANCE";
/**
* A selectable allergen, as returned by `GET /reference/allergies`.
*
* `name` is resolved server-side from the parent `Category` the `Allergy`
* table itself carries no name of its own (see `schema.prisma`), so this
* flattens that split away: callers just get `{id, name}` and never need to
* know a `Category` exists underneath. `kind` groups allergens into two
* `key` is resolved server-side from the parent `Category` the `Allergy`
* 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.
*/
export interface AllergyView {
id: number;
name: string;
key: string;
kind: AllergenKind;
}
@ -164,10 +173,13 @@ export type IngredientIcon = (typeof INGREDIENT_ICONS)[number];
* 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}\`)`.
*/
export interface IngredientView {
id: number;
name: string;
key: string;
icon: IngredientIcon;
category: IngredientCategory;
subcategory: IngredientSubcategory;

View file

@ -99,6 +99,9 @@ importers:
i18next:
specifier: ^26.3.6
version: 26.3.6(typescript@5.9.3)
lucide-react:
specifier: ^1.32.0
version: 1.32.0(react@18.3.1)
react:
specifier: ^18.3.1
version: 18.3.1
@ -2098,6 +2101,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, tarball: https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz}
lucide-react@1.32.0:
resolution: {integrity: sha512-txX56hMFnRxPi1f9/nH69YN8uvAO6a7Y1KSWKjCDAtdD9+soEgmWuCt6iRm1pkxUZo2+YntSdsE1L6bIuKoY8Q==, tarball: https://registry.npmjs.org/lucide-react/-/lucide-react-1.32.0.tgz}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
luxon@3.7.2:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==, tarball: https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz}
engines: {node: '>=12'}
@ -4764,6 +4772,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-react@1.32.0(react@18.3.1):
dependencies:
react: 18.3.1
luxon@3.7.2: {}
make-dir@3.1.0: