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>
86 lines
3.9 KiB
TypeScript
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);
|
|
}
|