batchCooking/apps/api/src/lib/recipe-translation.ts
Nicolas 5b868e4422 feat(recipes): traduction des étapes d'une recette importée en tech steps
Ajoute la brique "Traduction en étapes" du pipeline d'import décrit
dans specs/batch-cooking-architecture.md — prend un ParsedRecipe
(sortie de parse() d'un adaptateur, recipe-source-adapter.ts) et
déclare, pour chaque étape, sa séquence de tech steps détectée.

- recipe-translation.ts : TranslatedRecipe/TranslatedRecipeStep
  (ParsedRecipe/ParsedRecipeStep + techStepIds: number[], même forme
  que Step.techSteps/StepTechStep). translateRecipeSteps() est pure
  (prend les mappings en argument, comme matchTechSteps lui-même) ;
  translateRecipe() est le wrapper qui charge le catalogue depuis la
  DB pour une locale donnée — même séparation pur/DB que
  tech-step-matcher.ts.
- Ne touche pas aux ingrédients (résolution vers Ingredient/Unit
  toujours hors scope) ni ne produit une Recipe sauvegardable (pas de
  dietIds/visibility/auteur) — une seule brique du pipeline, pas tout
  le pipeline.
- Documente explicitement la limite actuelle : le catalogue de tech
  steps n'a que des mappings "fr", donc une source anglophone comme
  TheMealDB traduite avec cette locale obtient des séquences vides
  sur toutes ses étapes (vérifié par un test dédié avec du texte
  réel de TheMealDB).

8 nouveaux tests (partie pure + partie DB avec le vrai catalogue
"fr" seedé). 180 tests passent au total. Build et lint propres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 11:39:15 +02:00

86 lines
3.9 KiB
TypeScript

import type { ParsedRecipe, ParsedRecipeStep } from "./recipe-source-adapter.js";
import {
type TechStepMappingRule,
loadTechStepMappingRules,
matchTechSteps,
} from "./tech-step-matcher.js";
/**
* The "Traduction en étapes" stage of the import pipeline described in
* specs/batch-cooking-architecture.md (Import depuis source → **Traduction
* en étapes** → Sauvegarde) — takes a source-agnostic {@link ParsedRecipe}
* (recipe-source-adapter.ts's `parse()` output) and declares each step's
* technique sequence, the same `techStepIds: number[]` shape
* `Step.techSteps`/`StepTechStep` (schema.prisma) will eventually persist.
*
* Deliberately doesn't touch ingredients — resolving free-text ingredient
* lines against our `Ingredient`/`Unit` catalogs is a separate, not-yet-built
* concern (see `ParsedRecipeIngredient`'s doc comment) — and doesn't turn
* the result into a saveable `Recipe` either (no `dietIds`/`visibility`/
* author, a source can't know those). This is one step of the pipeline, not
* the whole thing.
*
* `translateRecipeSteps` is pure (takes `techStepMappings` as a plain
* argument, same convention as `matchTechSteps` itself) so it's unit-testable
* without a database; `translateRecipe` is the DB-backed convenience wrapper
* a caller reaches for in practice, mirroring `tech-step-matcher.ts`'s own
* pure/DB-touching split.
*/
/** A {@link ParsedRecipeStep}, after tech-step detection — declares its technique sequence alongside the description/picture it already had. */
export interface TranslatedRecipeStep extends ParsedRecipeStep {
/** Ordered sequence of detected `TechStep` ids (see `matchTechSteps`) — empty if this step doesn't mention any known technique. */
techStepIds: number[];
}
/** A {@link ParsedRecipe} whose `steps` have been translated — everything else (name, ingredients, portions, …) passes through unchanged. */
export interface TranslatedRecipe extends Omit<ParsedRecipe, "steps"> {
steps: TranslatedRecipeStep[];
}
/**
* Declares each of `recipe`'s steps' technique sequence against
* `techStepMappings`, leaving everything else about the recipe untouched.
* Pure — testable with a hand-built mapping list, no database involved (see
* `translateRecipe` for the DB-backed loader). `techStepMappings` should
* already be filtered to the locale the caller cares about, same
* requirement `matchTechSteps` itself has.
*/
export function translateRecipeSteps(
recipe: ParsedRecipe,
techStepMappings: TechStepMappingRule[],
): TranslatedRecipe {
return {
...recipe,
steps: recipe.steps.map((step) => ({
...step,
techStepIds: matchTechSteps(step.description, techStepMappings),
})),
};
}
/**
* Convenience wrapper around {@link translateRecipeSteps} that loads
* `locale`'s mapping catalog itself — what a caller reaches for when
* translating a single recipe on its own (e.g. the eventual "import this
* one recipe" endpoint). A caller translating many recipes at once should
* call `loadTechStepMappingRules` once and reuse it across
* `translateRecipeSteps` calls instead, the same "don't requery per item"
* reasoning `recipe.service.ts`'s `createRecipe`/`updateRecipe` already
* follow for manually-authored recipes.
*
* No user- or recipe-level language preference exists anywhere in the app
* yet (see `tech-step-matcher.ts`'s `loadTechStepMappingRules`) — callers
* pass a locale explicitly rather than this module guessing one. Note that
* an English-language source (e.g. TheMealDB) translated against `"fr"`
* mappings will currently get an empty `techStepIds` sequence on every
* step — matching-language mappings for that source's language don't exist
* yet, this stage doesn't invent them.
*/
export async function translateRecipe(
recipe: ParsedRecipe,
locale: string,
): Promise<TranslatedRecipe> {
const techStepMappings = await loadTechStepMappingRules(locale);
return translateRecipeSteps(recipe, techStepMappings);
}