Quand le catalogue seede ne couvre pas un ingredient, l'utilisateur pouvait etre bloque (creation manuelle) ou perdre silencieusement la ligne (import). Une ligne de recette accepte desormais `placeholderName` (texte libre) au lieu de `ingredientId` : l'API cree une ligne `Ingredient` `isPlaceholder` (cle `placeholder:<uuid>`, `displayName`, `createdById`) dans la transaction de la recette, et emet `ingredient.placeholder_created`. Ces lignes sont exclues de `GET /reference/ingredients` et de `ingredient-matcher`. Front : bouton "Ajouter << ... >>" dans l'etat vide de `IngredientPicker` (formulaire + import), badge "a completer" sur la ligne, helper `ingredientLabel` applique partout ou un libelle d'ingredient est rendu. Admin : `/admin/catalog/*` (+ page `apps/admin-web`) liste les placeholders regroupes par nom normalise, "marquer traite" (`reviewedAt`) et purge des orphelins. La promotion en vraie entree catalogue reste manuelle. Migration `ingredient_placeholder` ecrite a la main (Postgres indisponible). Suites Mocha DB-backed ecrites, non executees en session ; test pur `normalizePlaceholderName` + Cypress admin-web/web verts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
26 lines
1.2 KiB
TypeScript
26 lines
1.2 KiB
TypeScript
import type { IngredientView } from "@batch-cooking/shared";
|
|
|
|
/**
|
|
* The display name for an ingredient line.
|
|
*
|
|
* A real catalog ingredient has no name in the database — its French label
|
|
* lives in i18n under `catalog.ingredients.<key>` — so it's resolved
|
|
* through `t`. A **placeholder** (`IngredientView.isPlaceholder`, the
|
|
* free text a user typed when the catalog fell short — see
|
|
* `Ingredient.isPlaceholder` in schema.prisma) has its name stored on the
|
|
* row as `displayName` and no i18n key at all, so it's shown verbatim.
|
|
*
|
|
* Every place that used to inline `t(\`catalog.ingredients.${ingredient.key}\`)`
|
|
* goes through this instead, so a placeholder never renders as the raw,
|
|
* missing translation key `catalog.ingredients.placeholder:<uuid>`.
|
|
*
|
|
* `t` is passed in (rather than calling `useTranslation` here) so this
|
|
* stays a plain function usable from non-component code and unit-testable
|
|
* without mounting i18next — same split as `shopping-list.ts`.
|
|
*/
|
|
export function ingredientLabel(
|
|
ingredient: Pick<IngredientView, "key" | "displayName">,
|
|
t: (key: string) => string,
|
|
): string {
|
|
return ingredient.displayName ?? t(`catalog.ingredients.${ingredient.key}`);
|
|
}
|