batchCooking/apps/api/scripts/generate-catalog-i18n.ts
Nicolas 1d9bb6d112 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>
2026-08-18 19:44:30 +02:00

99 lines
4.2 KiB
TypeScript

/**
* 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`): derives every row's slug `key` from its
* French name (see `slugify.ts`), fails loudly on any collision, and
* regenerates `apps/web/src/locales/fr/translation.json`'s
* `catalog.{diets,allergens,ingredients}` sections (key -> French label),
* merged in without touching the rest of the file.
*
* Also (re-)writes `backfill.sql` alongside itself — the `UPDATE ... SET
* key = ...` statements a migration adding a brand new item needs to carry
* forward, in case that ever happens again; the one for this refactor's own
* migration (`prisma/migrations/20260818190000_catalog_labels_to_keys/`)
* was generated once and copied in by hand, and `backfill.sql` itself is
* gitignored scratch output, not the source of truth.
*
* 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 { ALLERGENS, DIETS, INGREDIENT_GROUPS } from "../src/db/reference-seed-data.js";
import { slugify } from "../src/utils/slugify.js";
const here = fileURLToPath(new URL(".", import.meta.url));
function toKeyLabelMap(labels: string[]): Record<string, string> {
const map: Record<string, string> = {};
const seenKeys = new Map<string, string>();
for (const label of labels) {
const key = slugify(label);
const clashingLabel = seenKeys.get(key);
if (clashingLabel !== undefined && clashingLabel !== label) {
throw new Error(`Slug collision: "${clashingLabel}" and "${label}" both slugify to "${key}"`);
}
seenKeys.set(key, label);
map[key] = label;
}
return map;
}
const dietLabels = DIETS;
const allergenLabels = ALLERGENS.map((a) => a.name);
const ingredientLabels = INGREDIENT_GROUPS.flatMap((g) => g.items.map((i) => i.name));
const diets = toKeyLabelMap(dietLabels);
const allergens = toKeyLabelMap(allergenLabels);
const ingredients = toKeyLabelMap(ingredientLabels);
console.log(
`diets: ${Object.keys(diets).length}, allergens: ${Object.keys(allergens).length}, ingredients: ${Object.keys(ingredients).length}`,
);
// --- Merge into the fr locale file -----------------------------------------
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}`);
// --- Emit the migration's backfill SQL --------------------------------------
function escapeSql(value: string): string {
return value.replace(/'/g, "''");
}
// The migration this feeds renames each table's "name" column to "key"
// *before* running this backfill — so by the time these UPDATEs run, the
// "key" column still holds the old French label value (just under its new
// column name), which is exactly what the WHERE clause below matches on.
const dietUpdates = Object.entries(diets)
.map(([key, label]) => `UPDATE "diet" SET "key" = '${escapeSql(key)}' WHERE "key" = '${escapeSql(label)}';`)
.join("\n");
const categoryUpdates = Object.entries(allergens)
.map(
([key, label]) => `UPDATE "category" SET "key" = '${escapeSql(key)}' WHERE "key" = '${escapeSql(label)}';`,
)
.join("\n");
const ingredientUpdates = Object.entries(ingredients)
.map(
([key, label]) =>
`UPDATE "ingredients" SET "key" = '${escapeSql(key)}' WHERE "key" = '${escapeSql(label)}';`,
)
.join("\n");
const sql = `-- 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.
${dietUpdates}
${categoryUpdates}
${ingredientUpdates}
`;
writeFileSync(here + "backfill.sql", sql);
console.log(`wrote ${here}backfill.sql`);