Compare commits
4 Commits
95e6d78619
...
8eacf1f5d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
8eacf1f5d0
|
|||
|
8a152c5fb2
|
|||
|
1d4daf11ad
|
|||
|
03df3b0d14
|
@@ -21,6 +21,66 @@ export function set_portions(){
|
||||
portions.update((p) => portions_local)
|
||||
}
|
||||
|
||||
export let lang: 'de' | 'en' = 'de';
|
||||
|
||||
// Translation strings
|
||||
const t = {
|
||||
de: {
|
||||
portions: 'Portionen:',
|
||||
ingredients: 'Zutaten',
|
||||
baseRecipe: 'Basisrezept',
|
||||
unnamed: 'Unbenannt',
|
||||
additionalIngredientsBefore: 'Zusätzliche Zutaten davor:',
|
||||
additionalIngredientsAfter: 'Zusätzliche Zutaten danach:',
|
||||
addIngredientBefore: 'Zutat davor hinzufügen',
|
||||
addIngredientAfter: 'Zutat danach hinzufügen',
|
||||
baseRecipeContent: '→ Inhalt vom Basisrezept wird hier eingefügt ←',
|
||||
insertBaseRecipe: 'Basisrezept einfügen',
|
||||
categoryOptional: 'Kategorie (optional)',
|
||||
editIngredient: 'Zutat verändern',
|
||||
renameCategory: 'Kategorie umbenennen',
|
||||
confirmDeleteReference: 'Bist du dir sicher, dass du diese Referenz löschen möchtest?',
|
||||
confirmDeleteList: 'Bist du dir sicher, dass du diese Liste löschen möchtest? Alle Zutaten der Liste werden hiermit auch gelöscht.',
|
||||
empty: 'Leer',
|
||||
editHeading: 'Überschrift bearbeiten',
|
||||
removeList: 'Liste entfernen',
|
||||
editIngredientAria: 'Zutat bearbeiten',
|
||||
removeIngredientAria: 'Zutat entfernen',
|
||||
moveUpAria: 'Nach oben verschieben',
|
||||
moveDownAria: 'Nach unten verschieben',
|
||||
moveReferenceUpAria: 'Referenz nach oben verschieben',
|
||||
moveReferenceDownAria: 'Referenz nach unten verschieben',
|
||||
removeReferenceAria: 'Referenz entfernen'
|
||||
},
|
||||
en: {
|
||||
portions: 'Portions:',
|
||||
ingredients: 'Ingredients',
|
||||
baseRecipe: 'Base Recipe',
|
||||
unnamed: 'Unnamed',
|
||||
additionalIngredientsBefore: 'Additional ingredients before:',
|
||||
additionalIngredientsAfter: 'Additional ingredients after:',
|
||||
addIngredientBefore: 'Add ingredient before',
|
||||
addIngredientAfter: 'Add ingredient after',
|
||||
baseRecipeContent: '→ Base recipe content will be inserted here ←',
|
||||
insertBaseRecipe: 'Insert Base Recipe',
|
||||
categoryOptional: 'Category (optional)',
|
||||
editIngredient: 'Edit Ingredient',
|
||||
renameCategory: 'Rename Category',
|
||||
confirmDeleteReference: 'Are you sure you want to delete this reference?',
|
||||
confirmDeleteList: 'Are you sure you want to delete this list? All ingredients in the list will also be deleted.',
|
||||
empty: 'Empty',
|
||||
editHeading: 'Edit heading',
|
||||
removeList: 'Remove list',
|
||||
editIngredientAria: 'Edit ingredient',
|
||||
removeIngredientAria: 'Remove ingredient',
|
||||
moveUpAria: 'Move up',
|
||||
moveDownAria: 'Move down',
|
||||
moveReferenceUpAria: 'Move reference up',
|
||||
moveReferenceDownAria: 'Move reference down',
|
||||
removeReferenceAria: 'Remove reference'
|
||||
}
|
||||
};
|
||||
|
||||
export let ingredients
|
||||
|
||||
let new_ingredient = {
|
||||
@@ -80,7 +140,7 @@ function handleSelect(recipe: any, options: any) {
|
||||
}
|
||||
|
||||
export function removeReference(list_index: number) {
|
||||
const confirmed = confirm("Bist du dir sicher, dass du diese Referenz löschen möchtest?");
|
||||
const confirmed = confirm(t[lang].confirmDeleteReference);
|
||||
if (confirmed) {
|
||||
ingredients.splice(list_index, 1);
|
||||
ingredients = ingredients;
|
||||
@@ -208,7 +268,7 @@ export function add_new_ingredient(){
|
||||
}
|
||||
export function remove_list(list_index){
|
||||
if(ingredients[list_index].list.length > 1){
|
||||
const response = confirm("Bist du dir sicher, dass du diese Liste löschen möchtest? Alle Zutaten der Liste werden hiermit auch gelöscht.");
|
||||
const response = confirm(t[lang].confirmDeleteList);
|
||||
if(!response){
|
||||
return
|
||||
}
|
||||
@@ -669,28 +729,28 @@ h3{
|
||||
</style>
|
||||
|
||||
<div class=list_wrapper >
|
||||
<h4>Portionen:</h4>
|
||||
<h4>{t[lang].portions}</h4>
|
||||
<p contenteditable type="text" bind:innerText={portions_local} on:blur={set_portions}></p>
|
||||
|
||||
<h2>Zutaten</h2>
|
||||
<h2>{t[lang].ingredients}</h2>
|
||||
{#each ingredients as list, list_index}
|
||||
{#if list.type === 'reference'}
|
||||
<!-- Reference item display -->
|
||||
<div class="reference-container">
|
||||
<div class="reference-header">
|
||||
<div class="move_buttons_container">
|
||||
<button on:click={() => update_list_position(list_index, 1)} aria-label="Referenz nach oben verschieben">
|
||||
<button on:click={() => update_list_position(list_index, 1)} aria-label={t[lang].moveReferenceUpAria}>
|
||||
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
|
||||
</button>
|
||||
<button on:click={() => update_list_position(list_index, -1)} aria-label="Referenz nach unten verschieben">
|
||||
<button on:click={() => update_list_position(list_index, -1)} aria-label={t[lang].moveReferenceDownAria}>
|
||||
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="reference-badge">
|
||||
📋 Basisrezept: {list.name || 'Unbenannt'}
|
||||
📋 {t[lang].baseRecipe}: {list.name || t[lang].unnamed}
|
||||
</div>
|
||||
<div class="mod_icons">
|
||||
<button class="action_button button_subtle" on:click={() => removeReference(list_index)} aria-label="Referenz entfernen">
|
||||
<button class="action_button button_subtle" on:click={() => removeReference(list_index)} aria-label={t[lang].removeReferenceAria}>
|
||||
<Cross fill="var(--nord11)"></Cross>
|
||||
</button>
|
||||
</div>
|
||||
@@ -698,7 +758,7 @@ h3{
|
||||
|
||||
<!-- Items before base recipe -->
|
||||
{#if list.itemsBefore && list.itemsBefore.length > 0}
|
||||
<h4 style="margin-block: 0.5em; color: var(--nord9);">Zusätzliche Zutaten davor:</h4>
|
||||
<h4 style="margin-block: 0.5em; color: var(--nord9);">{t[lang].additionalIngredientsBefore}</h4>
|
||||
<div class="ingredients_grid">
|
||||
{#each list.itemsBefore as item, item_index}
|
||||
<div class=move_buttons_container>
|
||||
@@ -711,10 +771,10 @@ h3{
|
||||
{@html item.name}
|
||||
</button>
|
||||
<div class="mod_icons">
|
||||
<button class="action_button button_subtle" on:click={() => editItemFromReference(list_index, 'before', item_index)} aria-label="Zutat bearbeiten">
|
||||
<button class="action_button button_subtle" on:click={() => editItemFromReference(list_index, 'before', item_index)} aria-label={t[lang].editIngredientAria}>
|
||||
<Pen fill="var(--nord6)" height="1em" width="1em"></Pen>
|
||||
</button>
|
||||
<button class="action_button button_subtle" on:click={() => removeItemFromReference(list_index, 'before', item_index)} aria-label="Zutat entfernen">
|
||||
<button class="action_button button_subtle" on:click={() => removeItemFromReference(list_index, 'before', item_index)} aria-label={t[lang].removeIngredientAria}>
|
||||
<Cross fill="var(--nord6)" height="1em" width="1em"></Cross>
|
||||
</button>
|
||||
</div>
|
||||
@@ -722,20 +782,20 @@ h3{
|
||||
</div>
|
||||
{/if}
|
||||
<button class="action_button button_subtle add-to-reference-button" on:click={() => openAddToReferenceModal(list_index, 'before')}>
|
||||
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> Zutat davor hinzufügen
|
||||
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> {t[lang].addIngredientBefore}
|
||||
</button>
|
||||
|
||||
<!-- Base recipe content indicator -->
|
||||
<div style="text-align: center; padding: 0.5em; margin: 0.5em 0; font-style: italic; color: var(--nord10); background-color: rgba(143, 188, 187, 0.4); border-radius: 5px;">
|
||||
→ Inhalt vom Basisrezept wird hier eingefügt ←
|
||||
{t[lang].baseRecipeContent}
|
||||
</div>
|
||||
|
||||
<!-- Items after base recipe -->
|
||||
<button class="action_button button_subtle add-to-reference-button" on:click={() => openAddToReferenceModal(list_index, 'after')}>
|
||||
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> Zutat danach hinzufügen
|
||||
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> {t[lang].addIngredientAfter}
|
||||
</button>
|
||||
{#if list.itemsAfter && list.itemsAfter.length > 0}
|
||||
<h4 style="margin-block: 0.5em; color: var(--nord9);">Zusätzliche Zutaten danach:</h4>
|
||||
<h4 style="margin-block: 0.5em; color: var(--nord9);">{t[lang].additionalIngredientsAfter}</h4>
|
||||
<div class="ingredients_grid">
|
||||
{#each list.itemsAfter as item, item_index}
|
||||
<div class=move_buttons_container>
|
||||
@@ -748,10 +808,10 @@ h3{
|
||||
{@html item.name}
|
||||
</button>
|
||||
<div class="mod_icons">
|
||||
<button class="action_button button_subtle" on:click={() => editItemFromReference(list_index, 'after', item_index)} aria-label="Zutat bearbeiten">
|
||||
<button class="action_button button_subtle" on:click={() => editItemFromReference(list_index, 'after', item_index)} aria-label={t[lang].editIngredientAria}>
|
||||
<Pen fill="var(--nord6)" height="1em" width="1em"></Pen>
|
||||
</button>
|
||||
<button class="action_button button_subtle" on:click={() => removeItemFromReference(list_index, 'after', item_index)} aria-label="Zutat entfernen">
|
||||
<button class="action_button button_subtle" on:click={() => removeItemFromReference(list_index, 'after', item_index)} aria-label={t[lang].removeIngredientAria}>
|
||||
<Cross fill="var(--nord6)" height="1em" width="1em"></Cross>
|
||||
</button>
|
||||
</div>
|
||||
@@ -775,23 +835,23 @@ h3{
|
||||
{#if list.name }
|
||||
{list.name}
|
||||
{:else}
|
||||
Leer
|
||||
{t[lang].empty}
|
||||
{/if}
|
||||
</button>
|
||||
<div class=mod_icons>
|
||||
<button class="action_button button_subtle" on:click="{() => show_modal_edit_subheading_ingredient(list_index)}" aria-label="Überschrift bearbeiten">
|
||||
<button class="action_button button_subtle" on:click="{() => show_modal_edit_subheading_ingredient(list_index)}" aria-label={t[lang].editHeading}>
|
||||
<Pen fill=var(--nord1)></Pen> </button>
|
||||
<button class="action_button button_subtle" on:click="{() => remove_list(list_index)}" aria-label="Liste entfernen">
|
||||
<button class="action_button button_subtle" on:click="{() => remove_list(list_index)}" aria-label={t[lang].removeList}>
|
||||
<Cross fill=var(--nord1)></Cross></button>
|
||||
</div>
|
||||
</h3>
|
||||
<div class=ingredients_grid>
|
||||
{#each list.list as ingredient, ingredient_index (ingredient_index)}
|
||||
<div class=move_buttons_container>
|
||||
<button on:click="{() => update_ingredient_position(list_index, ingredient_index, 1)}" aria-label="Zutat nach oben verschieben">
|
||||
<button on:click="{() => update_ingredient_position(list_index, ingredient_index, 1)}" aria-label={t[lang].moveUpAria}>
|
||||
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
|
||||
</button>
|
||||
<button on:click="{() => update_ingredient_position(list_index, ingredient_index, -1)}" aria-label="Zutat nach unten verschieben">
|
||||
<button on:click="{() => update_ingredient_position(list_index, ingredient_index, -1)}" aria-label={t[lang].moveDownAria}>
|
||||
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -801,9 +861,9 @@ h3{
|
||||
<button class="force_wrap ingredient-name-button" on:click={() => show_modal_edit_ingredient(list_index, ingredient_index)}>
|
||||
{@html ingredient.name}
|
||||
</button>
|
||||
<div class=mod_icons><button class="action_button button_subtle" on:click={() => show_modal_edit_ingredient(list_index, ingredient_index)} aria-label="Zutat bearbeiten">
|
||||
<div class=mod_icons><button class="action_button button_subtle" on:click={() => show_modal_edit_ingredient(list_index, ingredient_index)} aria-label={t[lang].editIngredientAria}>
|
||||
<Pen fill=var(--nord1) height=1em width=1em></Pen></button>
|
||||
<button class="action_button button_subtle" on:click="{() => remove_ingredient(list_index, ingredient_index)}" aria-label="Zutat entfernen"><Cross fill=var(--nord1) height=1em width=1em></Cross></button></div>
|
||||
<button class="action_button button_subtle" on:click="{() => remove_ingredient(list_index, ingredient_index)}" aria-label={t[lang].removeIngredientAria}><Cross fill=var(--nord1) height=1em width=1em></Cross></button></div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -812,12 +872,12 @@ h3{
|
||||
<!-- Button to insert base recipe -->
|
||||
<button class="insert-base-recipe-button" on:click={() => openSelector(ingredients.length)}>
|
||||
<Plus fill="white" style="display: inline; width: 1.5em; height: 1.5em; vertical-align: middle;"></Plus>
|
||||
Basisrezept einfügen
|
||||
{t[lang].insertBaseRecipe}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="adder shadow">
|
||||
<input class=category type="text" bind:value={new_ingredient.sublist} placeholder="Kategorie (optional)" on:keydown={(event) => do_on_key(event, 'Enter', false, add_new_ingredient)}>
|
||||
<input class=category type="text" bind:value={new_ingredient.sublist} placeholder={t[lang].categoryOptional} on:keydown={(event) => do_on_key(event, 'Enter', false, add_new_ingredient)}>
|
||||
<div class=add_ingredient>
|
||||
<input type="text" placeholder="250..." bind:value={new_ingredient.amount} on:keydown={(event) => do_on_key(event, 'Enter', false, add_new_ingredient)}>
|
||||
<input type="text" placeholder="mL..." bind:value={new_ingredient.unit} on:keydown={(event) => do_on_key(event, 'Enter', false, add_new_ingredient)}>
|
||||
@@ -828,9 +888,9 @@ h3{
|
||||
</div>
|
||||
</div>
|
||||
<dialog id=edit_ingredient_modal on:cancel={handleIngredientModalCancel}>
|
||||
<h2>Zutat verändern</h2>
|
||||
<h2>{t[lang].editIngredient}</h2>
|
||||
<div class=adder>
|
||||
<input class=category type="text" bind:value={edit_ingredient.sublist} placeholder="Kategorie (optional)">
|
||||
<input class=category type="text" bind:value={edit_ingredient.sublist} placeholder={t[lang].categoryOptional}>
|
||||
<div class=add_ingredient role="group" on:keydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
|
||||
<input type="text" placeholder="250..." bind:value={edit_ingredient.amount} on:keydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
|
||||
<input type="text" placeholder="mL..." bind:value={edit_ingredient.unit} on:keydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
|
||||
@@ -843,7 +903,7 @@ h3{
|
||||
</dialog>
|
||||
|
||||
<dialog id=edit_subheading_ingredient_modal>
|
||||
<h2>Kategorie umbenennen</h2>
|
||||
<h2>{t[lang].renameCategory}</h2>
|
||||
<div class=heading_wrapper>
|
||||
<input class=heading type="text" bind:value={edit_heading.name} on:keydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} >
|
||||
<button class=action_button on:keydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} on:click={edit_subheading_and_close_modal}>
|
||||
|
||||
@@ -11,6 +11,82 @@ import "$lib/css/action_button.css"
|
||||
import { do_on_key } from '$lib/components/do_on_key.js'
|
||||
import BaseRecipeSelector from '$lib/components/BaseRecipeSelector.svelte'
|
||||
|
||||
export let lang: 'de' | 'en' = 'de';
|
||||
|
||||
// Translation strings
|
||||
const t = {
|
||||
de: {
|
||||
preparation: 'Vorbereitung:',
|
||||
bulkFermentation: 'Stockgare:',
|
||||
finalFermentation: 'Stückgare:',
|
||||
baking: 'Backen:',
|
||||
cooking: 'Kochen:',
|
||||
totalTime: 'Auf dem Teller:',
|
||||
instructions: 'Zubereitung',
|
||||
baseRecipe: 'Basisrezept',
|
||||
unnamed: 'Unbenannt',
|
||||
additionalStepsBefore: 'Zusätzliche Schritte davor:',
|
||||
additionalStepsAfter: 'Zusätzliche Schritte danach:',
|
||||
addStepBefore: 'Schritt davor hinzufügen',
|
||||
addStepAfter: 'Schritt danach hinzufügen',
|
||||
baseRecipeContent: '→ Inhalt vom Basisrezept wird hier eingefügt ←',
|
||||
insertBaseRecipe: 'Basisrezept einfügen',
|
||||
categoryOptional: 'Kategorie (optional)',
|
||||
subcategoryOptional: 'Unterkategorie (optional)',
|
||||
editStep: 'Schritt verändern',
|
||||
renameCategory: 'Kategorie umbenennen',
|
||||
confirmDeleteReference: 'Bist du dir sicher, dass du diese Referenz löschen möchtest?',
|
||||
confirmDeleteList: 'Bist du dir sicher, dass du diese Liste löschen möchtest? Alle Zubereitungsschritte der Liste werden hiermit auch gelöscht.',
|
||||
empty: 'Leer',
|
||||
editHeading: 'Überschrift bearbeiten',
|
||||
removeList: 'Liste entfernen',
|
||||
editStepAria: 'Schritt bearbeiten',
|
||||
removeStepAria: 'Schritt entfernen',
|
||||
moveUpAria: 'Nach oben verschieben',
|
||||
moveDownAria: 'Nach unten verschieben',
|
||||
moveReferenceUpAria: 'Referenz nach oben verschieben',
|
||||
moveReferenceDownAria: 'Referenz nach unten verschieben',
|
||||
removeReferenceAria: 'Referenz entfernen',
|
||||
moveListUpAria: 'Liste nach oben verschieben',
|
||||
moveListDownAria: 'Liste nach unten verschieben'
|
||||
},
|
||||
en: {
|
||||
preparation: 'Preparation:',
|
||||
bulkFermentation: 'Bulk Fermentation:',
|
||||
finalFermentation: 'Final Fermentation:',
|
||||
baking: 'Baking:',
|
||||
cooking: 'Cooking:',
|
||||
totalTime: 'Total Time:',
|
||||
instructions: 'Instructions',
|
||||
baseRecipe: 'Base Recipe',
|
||||
unnamed: 'Unnamed',
|
||||
additionalStepsBefore: 'Additional steps before:',
|
||||
additionalStepsAfter: 'Additional steps after:',
|
||||
addStepBefore: 'Add step before',
|
||||
addStepAfter: 'Add step after',
|
||||
baseRecipeContent: '→ Base recipe content will be inserted here ←',
|
||||
insertBaseRecipe: 'Insert Base Recipe',
|
||||
categoryOptional: 'Category (optional)',
|
||||
subcategoryOptional: 'Subcategory (optional)',
|
||||
editStep: 'Edit Step',
|
||||
renameCategory: 'Rename Category',
|
||||
confirmDeleteReference: 'Are you sure you want to delete this reference?',
|
||||
confirmDeleteList: 'Are you sure you want to delete this list? All preparation steps in the list will also be deleted.',
|
||||
empty: 'Empty',
|
||||
editHeading: 'Edit heading',
|
||||
removeList: 'Remove list',
|
||||
editStepAria: 'Edit step',
|
||||
removeStepAria: 'Remove step',
|
||||
moveUpAria: 'Move up',
|
||||
moveDownAria: 'Move down',
|
||||
moveReferenceUpAria: 'Move reference up',
|
||||
moveReferenceDownAria: 'Move reference down',
|
||||
removeReferenceAria: 'Remove reference',
|
||||
moveListUpAria: 'Move list up',
|
||||
moveListDownAria: 'Move list down'
|
||||
}
|
||||
};
|
||||
|
||||
const step_placeholder = "Kartoffeln schälen..."
|
||||
export let instructions
|
||||
export let add_info
|
||||
@@ -61,7 +137,7 @@ function handleSelect(recipe: any, options: any) {
|
||||
}
|
||||
|
||||
export function removeReference(list_index: number) {
|
||||
const confirmed = confirm("Bist du dir sicher, dass du diese Referenz löschen möchtest?");
|
||||
const confirmed = confirm(t[lang].confirmDeleteReference);
|
||||
if (confirmed) {
|
||||
instructions.splice(list_index, 1);
|
||||
instructions = instructions;
|
||||
@@ -147,7 +223,7 @@ function get_sublist_index(sublist_name, list){
|
||||
}
|
||||
export function remove_list(list_index){
|
||||
if(instructions[list_index].steps.length > 1){
|
||||
const response = confirm("Bist du dir sicher, dass du diese Liste löschen möchtest? Alle Zubereitungsschritte der Liste werden hiermit auch gelöscht.");
|
||||
const response = confirm(t[lang].confirmDeleteList);
|
||||
if(!response){
|
||||
return
|
||||
}
|
||||
@@ -682,50 +758,50 @@ h3{
|
||||
<div class=instructions>
|
||||
<div class=additional_info>
|
||||
|
||||
<div><h4>Vorbereitung:</h4>
|
||||
<div><h4>{t[lang].preparation}</h4>
|
||||
<p contenteditable type="text" bind:innerText={add_info.preparation}></p>
|
||||
</div>
|
||||
|
||||
|
||||
<div><h4>Stockgare:</h4>
|
||||
<div><h4>{t[lang].bulkFermentation}</h4>
|
||||
<p contenteditable type="text" bind:innerText={add_info.fermentation.bulk}></p>
|
||||
</div>
|
||||
|
||||
<div><h4>Stückgare:</h4>
|
||||
<div><h4>{t[lang].finalFermentation}</h4>
|
||||
<p contenteditable type="text" bind:innerText={add_info.fermentation.final}></p>
|
||||
</div>
|
||||
|
||||
<div><h4>Backen:</h4>
|
||||
<div><h4>{t[lang].baking}</h4>
|
||||
<div><p type="text" bind:innerText={add_info.baking.length} contenteditable placeholder="40 min..."></p></div> bei <div><p type="text" bind:innerText={add_info.baking.temperature} contenteditable placeholder=200...></p></div> °C <div><p type="text" bind:innerText={add_info.baking.mode} contenteditable placeholder="Ober-/Unterhitze..."></p></div></div>
|
||||
|
||||
<div><h4>Kochen:</h4>
|
||||
<div><h4>{t[lang].cooking}</h4>
|
||||
<p contenteditable type="text" bind:innerText={add_info.cooking}></p>
|
||||
</div>
|
||||
|
||||
<div><h4>Auf dem Teller:</h4>
|
||||
<div><h4>{t[lang].totalTime}</h4>
|
||||
<p contenteditable type="text" bind:innerText={add_info.total_time}></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Zubereitung</h2>
|
||||
<h2>{t[lang].instructions}</h2>
|
||||
{#each instructions as list, list_index}
|
||||
{#if list.type === 'reference'}
|
||||
<!-- Reference item display -->
|
||||
<div class="reference-container">
|
||||
<div class="reference-header">
|
||||
<div class="move_buttons_container">
|
||||
<button on:click={() => update_list_position(list_index, 1)} aria-label="Referenz nach oben verschieben">
|
||||
<button on:click={() => update_list_position(list_index, 1)} aria-label={t[lang].moveReferenceUpAria}>
|
||||
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
|
||||
</button>
|
||||
<button on:click={() => update_list_position(list_index, -1)} aria-label="Referenz nach unten verschieben">
|
||||
<button on:click={() => update_list_position(list_index, -1)} aria-label={t[lang].moveReferenceDownAria}>
|
||||
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="reference-badge">
|
||||
📋 Basisrezept: {list.name || 'Unbenannt'}
|
||||
📋 {t[lang].baseRecipe}: {list.name || t[lang].unnamed}
|
||||
</div>
|
||||
<div class="mod_icons">
|
||||
<button class="action_button button_subtle" on:click={() => removeReference(list_index)} aria-label="Referenz entfernen">
|
||||
<button class="action_button button_subtle" on:click={() => removeReference(list_index)} aria-label={t[lang].removeReferenceAria}>
|
||||
<Cross fill="var(--nord11)"></Cross>
|
||||
</button>
|
||||
</div>
|
||||
@@ -733,7 +809,7 @@ h3{
|
||||
|
||||
<!-- Steps before base recipe -->
|
||||
{#if list.stepsBefore && list.stepsBefore.length > 0}
|
||||
<h4 style="margin-block: 0.5em; color: var(--nord9);">Zusätzliche Schritte davor:</h4>
|
||||
<h4 style="margin-block: 0.5em; color: var(--nord9);">{t[lang].additionalStepsBefore}</h4>
|
||||
<ol>
|
||||
{#each list.stepsBefore as step, step_index}
|
||||
<li>
|
||||
@@ -745,10 +821,10 @@ h3{
|
||||
{@html step}
|
||||
</button>
|
||||
<div>
|
||||
<button class="action_button button_subtle" on:click={() => editStepFromReference(list_index, 'before', step_index)} aria-label="Schritt bearbeiten">
|
||||
<button class="action_button button_subtle" on:click={() => editStepFromReference(list_index, 'before', step_index)} aria-label={t[lang].editStepAria}>
|
||||
<Pen fill="var(--nord6)" height="1em" width="1em"></Pen>
|
||||
</button>
|
||||
<button class="action_button button_subtle" on:click={() => removeStepFromReference(list_index, 'before', step_index)} aria-label="Schritt entfernen">
|
||||
<button class="action_button button_subtle" on:click={() => removeStepFromReference(list_index, 'before', step_index)} aria-label={t[lang].removeStepAria}>
|
||||
<Cross fill="var(--nord6)" height="1em" width="1em"></Cross>
|
||||
</button>
|
||||
</div>
|
||||
@@ -758,20 +834,20 @@ h3{
|
||||
</ol>
|
||||
{/if}
|
||||
<button class="action_button button_subtle add-to-reference-button" on:click={() => openAddToReferenceModal(list_index, 'before')}>
|
||||
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> Schritt davor hinzufügen
|
||||
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> {t[lang].addStepBefore}
|
||||
</button>
|
||||
|
||||
<!-- Base recipe content indicator -->
|
||||
<div style="text-align: center; padding: 0.5em; margin: 0.5em 0; font-style: italic; color: var(--nord10); background-color: rgba(143, 188, 187, 0.4); border-radius: 5px;">
|
||||
→ Inhalt vom Basisrezept wird hier eingefügt ←
|
||||
{t[lang].baseRecipeContent}
|
||||
</div>
|
||||
|
||||
<!-- Steps after base recipe -->
|
||||
<button class="action_button button_subtle add-to-reference-button" on:click={() => openAddToReferenceModal(list_index, 'after')}>
|
||||
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> Schritt danach hinzufügen
|
||||
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> {t[lang].addStepAfter}
|
||||
</button>
|
||||
{#if list.stepsAfter && list.stepsAfter.length > 0}
|
||||
<h4 style="margin-block: 0.5em; color: var(--nord9);">Zusätzliche Schritte danach:</h4>
|
||||
<h4 style="margin-block: 0.5em; color: var(--nord9);">{t[lang].additionalStepsAfter}</h4>
|
||||
<ol>
|
||||
{#each list.stepsAfter as step, step_index}
|
||||
<li>
|
||||
@@ -783,10 +859,10 @@ h3{
|
||||
{@html step}
|
||||
</button>
|
||||
<div>
|
||||
<button class="action_button button_subtle" on:click={() => editStepFromReference(list_index, 'after', step_index)} aria-label="Schritt bearbeiten">
|
||||
<button class="action_button button_subtle" on:click={() => editStepFromReference(list_index, 'after', step_index)} aria-label={t[lang].editStepAria}>
|
||||
<Pen fill="var(--nord6)" height="1em" width="1em"></Pen>
|
||||
</button>
|
||||
<button class="action_button button_subtle" on:click={() => removeStepFromReference(list_index, 'after', step_index)} aria-label="Schritt entfernen">
|
||||
<button class="action_button button_subtle" on:click={() => removeStepFromReference(list_index, 'after', step_index)} aria-label={t[lang].removeStepAria}>
|
||||
<Cross fill="var(--nord6)" height="1em" width="1em"></Cross>
|
||||
</button>
|
||||
</div>
|
||||
@@ -800,10 +876,10 @@ h3{
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<h3>
|
||||
<div class=move_buttons_container>
|
||||
<button on:click="{() => update_list_position(list_index, 1)}" aria-label="Liste nach oben verschieben">
|
||||
<button on:click="{() => update_list_position(list_index, 1)}" aria-label={t[lang].moveListUpAria}>
|
||||
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
|
||||
</button>
|
||||
<button on:click="{() => update_list_position(list_index, -1)}" aria-label="Liste nach unten verschieben">
|
||||
<button on:click="{() => update_list_position(list_index, -1)}" aria-label={t[lang].moveListDownAria}>
|
||||
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -811,12 +887,12 @@ h3{
|
||||
{#if list.name}
|
||||
{list.name}
|
||||
{:else}
|
||||
Leer
|
||||
{t[lang].empty}
|
||||
{/if}
|
||||
</button>
|
||||
<button class="action_button button_subtle" on:click="{() => show_modal_edit_subheading_step(list_index)}" aria-label="Überschrift bearbeiten">
|
||||
<button class="action_button button_subtle" on:click="{() => show_modal_edit_subheading_step(list_index)}" aria-label={t[lang].editHeading}>
|
||||
<Pen fill=var(--nord1)></Pen> </button>
|
||||
<button class="action_button button_subtle" on:click="{() => remove_list(list_index)}" aria-label="Liste entfernen">
|
||||
<button class="action_button button_subtle" on:click="{() => remove_list(list_index)}" aria-label={t[lang].removeList}>
|
||||
<Cross fill=var(--nord1)></Cross>
|
||||
</button>
|
||||
</h3>
|
||||
@@ -825,10 +901,10 @@ h3{
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<li>
|
||||
<div class="move_buttons_container step_move_buttons">
|
||||
<button on:click="{() => update_step_position(list_index, step_index, 1)}" aria-label="Schritt nach oben verschieben">
|
||||
<button on:click="{() => update_step_position(list_index, step_index, 1)}" aria-label={t[lang].moveUpAria}>
|
||||
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
|
||||
</button>
|
||||
<button on:click="{() => update_step_position(list_index, step_index, -1)}" aria-label="Schritt nach unten verschieben">
|
||||
<button on:click="{() => update_step_position(list_index, step_index, -1)}" aria-label={t[lang].moveDownAria}>
|
||||
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -836,10 +912,10 @@ h3{
|
||||
<button on:click={() => show_modal_edit_step(list_index, step_index)} class="step-button">
|
||||
{@html step}
|
||||
</button>
|
||||
<div><button class="action_button button_subtle" on:click={() => show_modal_edit_step(list_index, step_index)}>
|
||||
<div><button class="action_button button_subtle" on:click={() => show_modal_edit_step(list_index, step_index)} aria-label={t[lang].editStepAria}>
|
||||
<Pen fill=var(--nord1)></Pen>
|
||||
</button>
|
||||
<button class="action_button button_subtle" on:click="{() => remove_step(list_index, step_index)}">
|
||||
<button class="action_button button_subtle" on:click="{() => remove_step(list_index, step_index)}" aria-label={t[lang].removeStepAria}>
|
||||
<Cross fill=var(--nord1)></Cross>
|
||||
</button>
|
||||
</div></div>
|
||||
@@ -852,12 +928,12 @@ h3{
|
||||
<!-- Button to insert base recipe -->
|
||||
<button class="insert-base-recipe-button" on:click={() => openSelector(instructions.length)}>
|
||||
<Plus fill="white" style="display: inline; width: 1.5em; height: 1.5em; vertical-align: middle;"></Plus>
|
||||
Basisrezept einfügen
|
||||
{t[lang].insertBaseRecipe}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class='adder shadow'>
|
||||
<input class=category type="text" bind:value={new_step.name} placeholder="Kategorie (optional)"on:keydown={(event) => do_on_key(event, 'Enter', false , add_new_step)} >
|
||||
<input class=category type="text" bind:value={new_step.name} placeholder={t[lang].categoryOptional} on:keydown={(event) => do_on_key(event, 'Enter', false , add_new_step)} >
|
||||
<div class=add_step>
|
||||
<p id=step contenteditable on:focus='{clear_step}' on:blur={add_placeholder} bind:innerText={new_step.step} on:keydown={(event) => do_on_key(event, 'Enter', true , add_new_step)}></p>
|
||||
<button on:click={() => add_new_step()} class=action_button>
|
||||
@@ -867,9 +943,9 @@ h3{
|
||||
</div>
|
||||
</div>
|
||||
<dialog id=edit_step_modal on:cancel={handleStepModalCancel}>
|
||||
<h2>Schritt verändern</h2>
|
||||
<h2>{t[lang].editStep}</h2>
|
||||
<div class=adder>
|
||||
<input class=category type="text" bind:value={edit_step.name} placeholder="Unterkategorie (optional)" on:keydown={(event) => do_on_key(event, 'Enter', false , edit_step_and_close_modal)}>
|
||||
<input class=category type="text" bind:value={edit_step.name} placeholder={t[lang].subcategoryOptional} on:keydown={(event) => do_on_key(event, 'Enter', false , edit_step_and_close_modal)}>
|
||||
<div class=add_step>
|
||||
<p id=step contenteditable bind:innerText={edit_step.step} on:keydown={(event) => do_on_key(event, 'Enter', true , edit_step_and_close_modal)}></p>
|
||||
<button class=action_button on:click="{() => edit_step_and_close_modal()}" >
|
||||
@@ -879,7 +955,7 @@ h3{
|
||||
</dialog>
|
||||
|
||||
<dialog id=edit_subheading_steps_modal>
|
||||
<h2>Kategorie umbenennen</h2>
|
||||
<h2>{t[lang].renameCategory}</h2>
|
||||
<div class=heading_wrapper>
|
||||
<input class="heading" type="text" bind:value={edit_heading.name} on:keydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_steps_and_close_modal)}>
|
||||
<button on:click={edit_subheading_steps_and_close_modal} class=action_button>
|
||||
|
||||
@@ -7,27 +7,32 @@ import HefeSwapper from './HefeSwapper.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
// Flatten ingredient references for display
|
||||
const flattenedIngredients = $derived.by(() => {
|
||||
if (!data.ingredients) return [];
|
||||
// Recursively flatten nested ingredient references
|
||||
function flattenIngredientReferences(items, lang, visited = new Set()) {
|
||||
const result = [];
|
||||
|
||||
return data.ingredients.flatMap((item) => {
|
||||
for (const item of items) {
|
||||
if (item.type === 'reference' && item.resolvedRecipe) {
|
||||
const sections = [];
|
||||
// Prevent circular references
|
||||
const recipeId = item.resolvedRecipe._id?.toString() || item.resolvedRecipe.short_name;
|
||||
if (visited.has(recipeId)) {
|
||||
console.warn('Circular reference detected:', recipeId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newVisited = new Set(visited);
|
||||
newVisited.add(recipeId);
|
||||
|
||||
// Get translated or original ingredients
|
||||
const lang = data.lang || 'de';
|
||||
const ingredientsToUse = (lang === 'en' &&
|
||||
item.resolvedRecipe.translations?.en?.ingredients)
|
||||
? item.resolvedRecipe.translations.en.ingredients
|
||||
: item.resolvedRecipe.ingredients || [];
|
||||
|
||||
// Filter to only sections (not nested references)
|
||||
const baseIngredients = item.includeIngredients
|
||||
? ingredientsToUse.filter(i => i.type === 'section' || !i.type)
|
||||
: [];
|
||||
// Recursively flatten nested references
|
||||
const flattenedNested = flattenIngredientReferences(ingredientsToUse, lang, newVisited);
|
||||
|
||||
// Combine all items into one section
|
||||
// Combine all items into one list
|
||||
const combinedList = [];
|
||||
|
||||
// Add items before
|
||||
@@ -35,12 +40,14 @@ const flattenedIngredients = $derived.by(() => {
|
||||
combinedList.push(...item.itemsBefore);
|
||||
}
|
||||
|
||||
// Add base recipe ingredients
|
||||
baseIngredients.forEach(section => {
|
||||
// Add base recipe ingredients (now recursively flattened)
|
||||
if (item.includeIngredients) {
|
||||
flattenedNested.forEach(section => {
|
||||
if (section.list) {
|
||||
combinedList.push(...section.list);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add items after
|
||||
if (item.itemsAfter && item.itemsAfter.length > 0) {
|
||||
@@ -49,12 +56,11 @@ const flattenedIngredients = $derived.by(() => {
|
||||
|
||||
// Push as one section with optional label
|
||||
if (combinedList.length > 0) {
|
||||
// Use labelOverride if present, otherwise use base recipe name (translated if viewing in English)
|
||||
const baseRecipeName = (lang === 'en' && item.resolvedRecipe.translations?.en?.name)
|
||||
? item.resolvedRecipe.translations.en.name
|
||||
: item.resolvedRecipe.name;
|
||||
|
||||
sections.push({
|
||||
result.push({
|
||||
type: 'section',
|
||||
name: item.showLabel ? (item.labelOverride || baseRecipeName) : '',
|
||||
list: combinedList,
|
||||
@@ -62,13 +68,20 @@ const flattenedIngredients = $derived.by(() => {
|
||||
short_name: item.resolvedRecipe.short_name
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
} else if (item.type === 'section' || !item.type) {
|
||||
// Regular section - pass through
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Regular section - pass through
|
||||
return [item];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Flatten ingredient references for display
|
||||
const flattenedIngredients = $derived.by(() => {
|
||||
if (!data.ingredients) return [];
|
||||
const lang = data.lang || 'de';
|
||||
return flattenIngredientReferences(data.ingredients, lang);
|
||||
});
|
||||
let multiplier = $state(data.multiplier || 1);
|
||||
|
||||
|
||||
@@ -3,25 +3,32 @@ let { data } = $props();
|
||||
|
||||
let multiplier = $state(data.multiplier || 1);
|
||||
|
||||
// Flatten instruction references for display
|
||||
const flattenedInstructions = $derived.by(() => {
|
||||
if (!data.instructions) return [];
|
||||
// Recursively flatten nested instruction references
|
||||
function flattenInstructionReferences(items, lang, visited = new Set()) {
|
||||
const result = [];
|
||||
|
||||
return data.instructions.flatMap((item) => {
|
||||
for (const item of items) {
|
||||
if (item.type === 'reference' && item.resolvedRecipe) {
|
||||
// Prevent circular references
|
||||
const recipeId = item.resolvedRecipe._id?.toString() || item.resolvedRecipe.short_name;
|
||||
if (visited.has(recipeId)) {
|
||||
console.warn('Circular reference detected:', recipeId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newVisited = new Set(visited);
|
||||
newVisited.add(recipeId);
|
||||
|
||||
// Get translated or original instructions
|
||||
const lang = data.lang || 'de';
|
||||
const instructionsToUse = (lang === 'en' &&
|
||||
item.resolvedRecipe.translations?.en?.instructions)
|
||||
? item.resolvedRecipe.translations.en.instructions
|
||||
: item.resolvedRecipe.instructions || [];
|
||||
|
||||
// Filter to only sections (not nested references)
|
||||
const baseInstructions = item.includeInstructions
|
||||
? instructionsToUse.filter(i => i.type === 'section' || !i.type)
|
||||
: [];
|
||||
// Recursively flatten nested references
|
||||
const flattenedNested = flattenInstructionReferences(instructionsToUse, lang, newVisited);
|
||||
|
||||
// Combine all steps into one section
|
||||
// Combine all steps into one list
|
||||
const combinedSteps = [];
|
||||
|
||||
// Add steps before
|
||||
@@ -29,12 +36,14 @@ const flattenedInstructions = $derived.by(() => {
|
||||
combinedSteps.push(...item.stepsBefore);
|
||||
}
|
||||
|
||||
// Add base recipe instructions
|
||||
baseInstructions.forEach(section => {
|
||||
// Add base recipe instructions (now recursively flattened)
|
||||
if (item.includeInstructions) {
|
||||
flattenedNested.forEach(section => {
|
||||
if (section.steps) {
|
||||
combinedSteps.push(...section.steps);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add steps after
|
||||
if (item.stepsAfter && item.stepsAfter.length > 0) {
|
||||
@@ -43,26 +52,32 @@ const flattenedInstructions = $derived.by(() => {
|
||||
|
||||
// Push as one section with optional label
|
||||
if (combinedSteps.length > 0) {
|
||||
// Use labelOverride if present, otherwise use base recipe name (translated if viewing in English)
|
||||
const baseRecipeName = (lang === 'en' && item.resolvedRecipe.translations?.en?.name)
|
||||
? item.resolvedRecipe.translations.en.name
|
||||
: item.resolvedRecipe.name;
|
||||
|
||||
return [{
|
||||
result.push({
|
||||
type: 'section',
|
||||
name: item.showLabel ? (item.labelOverride || baseRecipeName) : '',
|
||||
steps: combinedSteps,
|
||||
isReference: item.showLabel,
|
||||
short_name: item.resolvedRecipe.short_name
|
||||
}];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Regular section - pass through
|
||||
return [item];
|
||||
});
|
||||
}
|
||||
} else if (item.type === 'section' || !item.type) {
|
||||
// Regular section - pass through
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Flatten instruction references for display
|
||||
const flattenedInstructions = $derived.by(() => {
|
||||
if (!data.instructions) return [];
|
||||
const lang = data.lang || 'de';
|
||||
return flattenInstructionReferences(data.instructions, lang);
|
||||
});
|
||||
|
||||
const isEnglish = $derived(data.lang === 'en');
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { TranslatedRecipeType } from '$types/types';
|
||||
import TranslationFieldComparison from './TranslationFieldComparison.svelte';
|
||||
import EditableIngredients from './EditableIngredients.svelte';
|
||||
import EditableInstructions from './EditableInstructions.svelte';
|
||||
import CreateIngredientList from './CreateIngredientList.svelte';
|
||||
import CreateStepList from './CreateStepList.svelte';
|
||||
|
||||
export let germanData: any;
|
||||
export let englishData: TranslatedRecipeType | null = null;
|
||||
@@ -17,7 +17,7 @@
|
||||
let errorMessage: string = '';
|
||||
let validationErrors: string[] = [];
|
||||
|
||||
// Editable English data (clone of englishData)
|
||||
// Editable English data (clone of englishData or initialized from germanData)
|
||||
let editableEnglish: any = englishData ? { ...englishData } : null;
|
||||
|
||||
// Store old recipe data for granular change detection
|
||||
@@ -26,6 +26,170 @@
|
||||
// Translation metadata (tracks which items were re-translated)
|
||||
let translationMetadata: any = null;
|
||||
|
||||
// Track base recipes that need translation
|
||||
let untranslatedBaseRecipes: { shortName: string, name: string }[] = [];
|
||||
let checkingBaseRecipes = false;
|
||||
|
||||
// Sync base recipe references from German to English
|
||||
async function syncBaseRecipeReferences() {
|
||||
if (!germanData) return;
|
||||
|
||||
checkingBaseRecipes = true;
|
||||
|
||||
// Helper to extract short_name from baseRecipeRef (which might be an object or string)
|
||||
const getShortName = (baseRecipeRef: any): string => {
|
||||
return typeof baseRecipeRef === 'object' ? baseRecipeRef.short_name : baseRecipeRef;
|
||||
};
|
||||
|
||||
// Collect all base recipe references from German data
|
||||
const germanBaseRecipeShortNames = new Set<string>();
|
||||
const baseRecipeRefMap = new Map<string, any>(); // Map short_name to baseRecipeRef (ID or object)
|
||||
|
||||
(germanData.ingredients || []).forEach((ing: any) => {
|
||||
if (ing.type === 'reference' && ing.baseRecipeRef) {
|
||||
const shortName = getShortName(ing.baseRecipeRef);
|
||||
germanBaseRecipeShortNames.add(shortName);
|
||||
baseRecipeRefMap.set(shortName, ing.baseRecipeRef);
|
||||
}
|
||||
});
|
||||
(germanData.instructions || []).forEach((inst: any) => {
|
||||
if (inst.type === 'reference' && inst.baseRecipeRef) {
|
||||
const shortName = getShortName(inst.baseRecipeRef);
|
||||
germanBaseRecipeShortNames.add(shortName);
|
||||
baseRecipeRefMap.set(shortName, inst.baseRecipeRef);
|
||||
}
|
||||
});
|
||||
|
||||
// If no base recipes in German, just initialize editableEnglish from German data if needed
|
||||
if (germanBaseRecipeShortNames.size === 0) {
|
||||
if (!editableEnglish) {
|
||||
editableEnglish = {
|
||||
...germanData,
|
||||
translationStatus: 'pending',
|
||||
ingredients: JSON.parse(JSON.stringify(germanData.ingredients || [])),
|
||||
instructions: JSON.parse(JSON.stringify(germanData.instructions || []))
|
||||
};
|
||||
}
|
||||
checkingBaseRecipes = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all base recipes and check their English translations
|
||||
const untranslated: { shortName: string, name: string }[] = [];
|
||||
const baseRecipeTranslations = new Map<string, { deName: string, enName: string }>();
|
||||
|
||||
for (const shortName of germanBaseRecipeShortNames) {
|
||||
try {
|
||||
const response = await fetch(`/api/rezepte/items/${shortName}`);
|
||||
if (response.ok) {
|
||||
const recipe = await response.json();
|
||||
if (!recipe.translations?.en) {
|
||||
untranslated.push({ shortName, name: recipe.name });
|
||||
} else {
|
||||
baseRecipeTranslations.set(shortName, {
|
||||
deName: recipe.name,
|
||||
enName: recipe.translations.en.name
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching base recipe ${shortName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
untranslatedBaseRecipes = untranslated;
|
||||
checkingBaseRecipes = false;
|
||||
|
||||
// Don't proceed if there are untranslated base recipes
|
||||
if (untranslated.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Now merge German base recipe references into editableEnglish
|
||||
// This works for both new translations and existing translations
|
||||
|
||||
if (!editableEnglish) {
|
||||
// No existing English translation - create from German structure with English base recipe names
|
||||
editableEnglish = {
|
||||
...germanData,
|
||||
translationStatus: 'pending',
|
||||
ingredients: JSON.parse(JSON.stringify(germanData.ingredients || [])).map((ing: any) => {
|
||||
if (ing.type === 'reference' && ing.baseRecipeRef) {
|
||||
const shortName = getShortName(ing.baseRecipeRef);
|
||||
const translation = baseRecipeTranslations.get(shortName);
|
||||
return translation ? { ...ing, name: translation.enName } : ing;
|
||||
}
|
||||
return ing;
|
||||
}),
|
||||
instructions: JSON.parse(JSON.stringify(germanData.instructions || [])).map((inst: any) => {
|
||||
if (inst.type === 'reference' && inst.baseRecipeRef) {
|
||||
const shortName = getShortName(inst.baseRecipeRef);
|
||||
const translation = baseRecipeTranslations.get(shortName);
|
||||
return translation ? { ...inst, name: translation.enName } : inst;
|
||||
}
|
||||
return inst;
|
||||
})
|
||||
};
|
||||
} else {
|
||||
// Existing English translation - merge German structure with English translations
|
||||
// Use German structure but keep English translations where they exist
|
||||
editableEnglish = {
|
||||
...editableEnglish,
|
||||
ingredients: germanData.ingredients.map((germanIng: any, index: number) => {
|
||||
if (germanIng.type === 'reference' && germanIng.baseRecipeRef) {
|
||||
// This is a base recipe reference - use English base recipe name
|
||||
const shortName = getShortName(germanIng.baseRecipeRef);
|
||||
const translation = baseRecipeTranslations.get(shortName);
|
||||
const englishIng = editableEnglish.ingredients[index];
|
||||
|
||||
// If English already has this reference at same position, keep it
|
||||
if (englishIng?.type === 'reference' && englishIng.baseRecipeRef === germanIng.baseRecipeRef) {
|
||||
return englishIng;
|
||||
}
|
||||
|
||||
// Otherwise, create new reference with English base recipe name
|
||||
return translation ? { ...germanIng, name: translation.enName } : germanIng;
|
||||
} else {
|
||||
// Regular ingredient section - keep existing English translation if it exists
|
||||
const englishIng = editableEnglish.ingredients[index];
|
||||
if (englishIng && englishIng.type !== 'reference') {
|
||||
return englishIng;
|
||||
}
|
||||
// If no English translation exists, use German structure (will be translated later)
|
||||
return germanIng;
|
||||
}
|
||||
}),
|
||||
instructions: germanData.instructions.map((germanInst: any, index: number) => {
|
||||
if (germanInst.type === 'reference' && germanInst.baseRecipeRef) {
|
||||
// This is a base recipe reference - use English base recipe name
|
||||
const shortName = getShortName(germanInst.baseRecipeRef);
|
||||
const translation = baseRecipeTranslations.get(shortName);
|
||||
const englishInst = editableEnglish.instructions[index];
|
||||
|
||||
// If English already has this reference at same position, keep it
|
||||
if (englishInst?.type === 'reference' && englishInst.baseRecipeRef === germanInst.baseRecipeRef) {
|
||||
return englishInst;
|
||||
}
|
||||
|
||||
// Otherwise, create new reference with English base recipe name
|
||||
return translation ? { ...germanInst, name: translation.enName } : germanInst;
|
||||
} else {
|
||||
// Regular instruction section - keep existing English translation if it exists
|
||||
const englishInst = editableEnglish.instructions[index];
|
||||
if (englishInst && englishInst.type !== 'reference') {
|
||||
return englishInst;
|
||||
}
|
||||
// If no English translation exists, use German structure (will be translated later)
|
||||
return germanInst;
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Always sync base recipe references when component mounts
|
||||
syncBaseRecipeReferences();
|
||||
|
||||
// Handle auto-translate button click
|
||||
async function handleAutoTranslate() {
|
||||
translationState = 'translating';
|
||||
@@ -98,21 +262,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ingredients changes
|
||||
function handleIngredientsChange(event: CustomEvent) {
|
||||
if (editableEnglish) {
|
||||
editableEnglish.ingredients = event.detail.ingredients;
|
||||
editableEnglish = editableEnglish; // Trigger reactivity
|
||||
}
|
||||
}
|
||||
|
||||
// Handle instructions changes
|
||||
function handleInstructionsChange(event: CustomEvent) {
|
||||
if (editableEnglish) {
|
||||
editableEnglish.instructions = event.detail.instructions;
|
||||
editableEnglish = editableEnglish; // Trigger reactivity
|
||||
}
|
||||
}
|
||||
// Create add_info object for CreateStepList that references editableEnglish properties
|
||||
// This allows CreateStepList to modify the values directly
|
||||
$: englishAddInfo = editableEnglish ? {
|
||||
get preparation() { return editableEnglish.preparation || ''; },
|
||||
set preparation(value) { editableEnglish.preparation = value; },
|
||||
fermentation: {
|
||||
get bulk() { return editableEnglish.fermentation?.bulk || ''; },
|
||||
set bulk(value) {
|
||||
if (!editableEnglish.fermentation) editableEnglish.fermentation = { bulk: '', final: '' };
|
||||
editableEnglish.fermentation.bulk = value;
|
||||
},
|
||||
get final() { return editableEnglish.fermentation?.final || ''; },
|
||||
set final(value) {
|
||||
if (!editableEnglish.fermentation) editableEnglish.fermentation = { bulk: '', final: '' };
|
||||
editableEnglish.fermentation.final = value;
|
||||
},
|
||||
},
|
||||
baking: {
|
||||
get length() { return editableEnglish.baking?.length || ''; },
|
||||
set length(value) {
|
||||
if (!editableEnglish.baking) editableEnglish.baking = { length: '', temperature: '', mode: '' };
|
||||
editableEnglish.baking.length = value;
|
||||
},
|
||||
get temperature() { return editableEnglish.baking?.temperature || ''; },
|
||||
set temperature(value) {
|
||||
if (!editableEnglish.baking) editableEnglish.baking = { length: '', temperature: '', mode: '' };
|
||||
editableEnglish.baking.temperature = value;
|
||||
},
|
||||
get mode() { return editableEnglish.baking?.mode || ''; },
|
||||
set mode(value) {
|
||||
if (!editableEnglish.baking) editableEnglish.baking = { length: '', temperature: '', mode: '' };
|
||||
editableEnglish.baking.mode = value;
|
||||
},
|
||||
},
|
||||
get total_time() { return editableEnglish.total_time || ''; },
|
||||
set total_time(value) { editableEnglish.total_time = value; },
|
||||
get cooking() { return editableEnglish.cooking || ''; },
|
||||
set cooking(value) { editableEnglish.cooking = value; },
|
||||
} : null;
|
||||
|
||||
// Handle approval
|
||||
function handleApprove() {
|
||||
@@ -156,6 +344,11 @@
|
||||
dispatch('cancelled');
|
||||
}
|
||||
|
||||
// Handle force full retranslation
|
||||
function handleForceFullRetranslation() {
|
||||
dispatch('forceFullRetranslation');
|
||||
}
|
||||
|
||||
// Get status badge color
|
||||
function getStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
@@ -221,16 +414,31 @@
|
||||
background: var(--nord12);
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
.translation-preview {
|
||||
max-width: 1000px;
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
.field-section {
|
||||
margin-bottom: 1.5rem;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 1000px;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.list-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,292 +609,57 @@ button:disabled {
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if translationState === 'idle'}
|
||||
<div class="idle-state">
|
||||
<p>Click "Auto-translate" to generate English translation using DeepL.</p>
|
||||
<div class="actions">
|
||||
<button class="btn-primary" on:click={handleAutoTranslate}>
|
||||
Auto-translate
|
||||
</button>
|
||||
<button class="btn-secondary" on:click={handleSkip}>
|
||||
Skip Translation
|
||||
</button>
|
||||
{#if checkingBaseRecipes}
|
||||
<div style="background: var(--nord9); color: var(--nord6); padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem; text-align: center;">
|
||||
<p>Checking if referenced base recipes are translated...</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if untranslatedBaseRecipes.length > 0}
|
||||
<div style="background: var(--nord12); color: var(--nord0); padding: 1.5rem; border-radius: 4px; margin-bottom: 1.5rem;">
|
||||
<h4 style="margin-top: 0;">⚠️ Base Recipes Need Translation</h4>
|
||||
<p>The following base recipes need to be translated to English before you can translate this recipe:</p>
|
||||
<ul style="margin: 1rem 0;">
|
||||
{#each untranslatedBaseRecipes as baseRecipe}
|
||||
<li>
|
||||
<strong>{baseRecipe.name}</strong>
|
||||
<a href="/de/edit/{baseRecipe.id}" target="_blank" rel="noopener noreferrer" style="margin-left: 0.5rem; color: var(--nord10);">
|
||||
Open in new tab →
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p style="margin-bottom: 0;">
|
||||
<button class="btn-secondary" on:click={syncBaseRecipeReferences}>
|
||||
Re-check Base Recipes
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if translationState === 'idle'}
|
||||
<div style="background: var(--nord13); color: var(--nord0); padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem; text-align: center;">
|
||||
<strong>Preview (Not yet translated)</strong>
|
||||
<p style="margin: 0.5rem 0;">The structure below shows what will be translated. Click "Auto-translate" to generate English translation.</p>
|
||||
</div>
|
||||
|
||||
{:else if translationState === 'translating'}
|
||||
{/if}
|
||||
|
||||
{#if translationState === 'translating'}
|
||||
<div class="idle-state">
|
||||
<p>
|
||||
<span class="loading-spinner"></span>
|
||||
Translating recipe...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{:else if translationState === 'preview' || translationState === 'approved'}
|
||||
<div class="comparison-grid">
|
||||
<div>
|
||||
<div class="column-header">🇩🇪 German (Original)</div>
|
||||
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Name"
|
||||
germanValue={germanData.name}
|
||||
englishValue={editableEnglish?.name || ''}
|
||||
fieldName="name"
|
||||
readonly={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Short Name (URL)"
|
||||
germanValue={germanData.short_name}
|
||||
englishValue={editableEnglish?.short_name || ''}
|
||||
fieldName="short_name"
|
||||
readonly={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Description"
|
||||
germanValue={germanData.description}
|
||||
englishValue={editableEnglish?.description || ''}
|
||||
fieldName="description"
|
||||
readonly={true}
|
||||
multiline={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Category"
|
||||
germanValue={germanData.category}
|
||||
englishValue={editableEnglish?.category || ''}
|
||||
fieldName="category"
|
||||
readonly={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if germanData.tags && germanData.tags.length > 0}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Tags"
|
||||
germanValue={germanData.tags.join(', ')}
|
||||
englishValue={editableEnglish?.tags?.join(', ') || ''}
|
||||
fieldName="tags"
|
||||
readonly={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.preamble}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Preamble"
|
||||
germanValue={germanData.preamble}
|
||||
englishValue={editableEnglish?.preamble || ''}
|
||||
fieldName="preamble"
|
||||
readonly={true}
|
||||
multiline={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if translationState === 'idle' || translationState === 'preview' || translationState === 'approved'}
|
||||
<div class="translation-preview">
|
||||
<h3 style="margin-bottom: 1.5rem; color: var(--nord8);">🇬🇧 English Translation</h3>
|
||||
|
||||
{#if germanData.addendum}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Addendum"
|
||||
germanValue={germanData.addendum}
|
||||
englishValue={editableEnglish?.addendum || ''}
|
||||
fieldName="addendum"
|
||||
readonly={true}
|
||||
multiline={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.note}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Note"
|
||||
germanValue={germanData.note}
|
||||
englishValue={editableEnglish?.note || ''}
|
||||
fieldName="note"
|
||||
readonly={true}
|
||||
multiline={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.portions}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Portions"
|
||||
germanValue={germanData.portions}
|
||||
englishValue={editableEnglish?.portions || ''}
|
||||
fieldName="portions"
|
||||
readonly={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.preparation}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Preparation Time"
|
||||
germanValue={germanData.preparation}
|
||||
englishValue={editableEnglish?.preparation || ''}
|
||||
fieldName="preparation"
|
||||
readonly={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.cooking}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Cooking Time"
|
||||
germanValue={germanData.cooking}
|
||||
englishValue={editableEnglish?.cooking || ''}
|
||||
fieldName="cooking"
|
||||
readonly={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.total_time}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Total Time"
|
||||
germanValue={germanData.total_time}
|
||||
englishValue={editableEnglish?.total_time || ''}
|
||||
fieldName="total_time"
|
||||
readonly={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.baking && (germanData.baking.temperature || germanData.baking.length || germanData.baking.mode)}
|
||||
<div class="field-group">
|
||||
<div class="field-label">Baking</div>
|
||||
<div class="field-value readonly readonly-text">
|
||||
{#if germanData.baking.temperature}Temperature: {germanData.baking.temperature}<br>{/if}
|
||||
{#if germanData.baking.length}Time: {germanData.baking.length}<br>{/if}
|
||||
{#if germanData.baking.mode}Mode: {germanData.baking.mode}{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.fermentation && (germanData.fermentation.bulk || germanData.fermentation.final)}
|
||||
<div class="field-group">
|
||||
<div class="field-label">Fermentation</div>
|
||||
<div class="field-value readonly readonly-text">
|
||||
{#if germanData.fermentation.bulk}Bulk: {germanData.fermentation.bulk}<br>{/if}
|
||||
{#if germanData.fermentation.final}Final: {germanData.fermentation.final}{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.ingredients && germanData.ingredients.length > 0}
|
||||
<div class="field-group">
|
||||
<div class="field-label">Ingredients</div>
|
||||
<div class="field-value readonly readonly-text">
|
||||
{#each germanData.ingredients as ing}
|
||||
{#if ing.type === 'reference'}
|
||||
<div style="background: var(--nord3); padding: 0.5rem; border-radius: 4px; margin-bottom: 0.5rem;">
|
||||
<strong>🔗 Base Recipe Reference</strong>
|
||||
{#if ing.labelOverride}
|
||||
<div><em>Label: {ing.labelOverride}</em></div>
|
||||
{/if}
|
||||
{#if ing.itemsBefore && ing.itemsBefore.length > 0}
|
||||
<div style="margin-top: 0.5rem;"><strong>Items Before:</strong></div>
|
||||
<ul>
|
||||
{#each ing.itemsBefore as item}
|
||||
<li>{item.amount} {item.unit} {item.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if ing.itemsAfter && ing.itemsAfter.length > 0}
|
||||
<div style="margin-top: 0.5rem;"><strong>Items After:</strong></div>
|
||||
<ul>
|
||||
{#each ing.itemsAfter as item}
|
||||
<li>{item.amount} {item.unit} {item.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<strong>{ing.name || 'Ingredients'}</strong>
|
||||
<ul>
|
||||
{#each ing.list as item}
|
||||
<li>{item.amount} {item.unit} {item.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if germanData.instructions && germanData.instructions.length > 0}
|
||||
<div class="field-group">
|
||||
<div class="field-label">Instructions</div>
|
||||
<div class="field-value readonly readonly-text">
|
||||
{#each germanData.instructions as inst}
|
||||
{#if inst.type === 'reference'}
|
||||
<div style="background: var(--nord3); padding: 0.5rem; border-radius: 4px; margin-bottom: 0.5rem;">
|
||||
<strong>🔗 Base Recipe Reference</strong>
|
||||
{#if inst.labelOverride}
|
||||
<div><em>Label: {inst.labelOverride}</em></div>
|
||||
{/if}
|
||||
{#if inst.stepsBefore && inst.stepsBefore.length > 0}
|
||||
<div style="margin-top: 0.5rem;"><strong>Steps Before:</strong></div>
|
||||
<ol>
|
||||
{#each inst.stepsBefore as step}
|
||||
<li>{step}</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
{#if inst.stepsAfter && inst.stepsAfter.length > 0}
|
||||
<div style="margin-top: 0.5rem;"><strong>Steps After:</strong></div>
|
||||
<ol>
|
||||
{#each inst.stepsAfter as step}
|
||||
<li>{step}</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<strong>{inst.name || 'Steps'}</strong>
|
||||
<ol>
|
||||
{#each inst.steps as step}
|
||||
<li>{step}</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="column-header">🇬🇧 English (Translated)</div>
|
||||
|
||||
<div class="field-group">
|
||||
<!-- Basic Fields -->
|
||||
<div class="field-section">
|
||||
<TranslationFieldComparison
|
||||
label="Name"
|
||||
germanValue={germanData.name}
|
||||
@@ -697,7 +670,7 @@ button:disabled {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-section">
|
||||
<TranslationFieldComparison
|
||||
label="Short Name (URL)"
|
||||
germanValue={germanData.short_name}
|
||||
@@ -708,7 +681,7 @@ button:disabled {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-section">
|
||||
<TranslationFieldComparison
|
||||
label="Description"
|
||||
germanValue={germanData.description}
|
||||
@@ -720,7 +693,7 @@ button:disabled {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-section">
|
||||
<TranslationFieldComparison
|
||||
label="Category"
|
||||
germanValue={germanData.category}
|
||||
@@ -732,7 +705,7 @@ button:disabled {
|
||||
</div>
|
||||
|
||||
{#if editableEnglish?.tags}
|
||||
<div class="field-group">
|
||||
<div class="field-section">
|
||||
<TranslationFieldComparison
|
||||
label="Tags"
|
||||
germanValue={germanData.tags?.join(', ') || ''}
|
||||
@@ -744,11 +717,11 @@ button:disabled {
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.preamble}
|
||||
<div class="field-group">
|
||||
{#if editableEnglish?.preamble !== undefined}
|
||||
<div class="field-section">
|
||||
<TranslationFieldComparison
|
||||
label="Preamble"
|
||||
germanValue={germanData.preamble}
|
||||
germanValue={germanData.preamble || ''}
|
||||
englishValue={editableEnglish.preamble}
|
||||
fieldName="preamble"
|
||||
readonly={false}
|
||||
@@ -758,25 +731,11 @@ button:disabled {
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.addendum}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Addendum"
|
||||
germanValue={germanData.addendum}
|
||||
englishValue={editableEnglish.addendum}
|
||||
fieldName="addendum"
|
||||
readonly={false}
|
||||
multiline={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.note}
|
||||
<div class="field-group">
|
||||
{#if editableEnglish?.note !== undefined}
|
||||
<div class="field-section">
|
||||
<TranslationFieldComparison
|
||||
label="Note"
|
||||
germanValue={germanData.note}
|
||||
germanValue={germanData.note || ''}
|
||||
englishValue={editableEnglish.note}
|
||||
fieldName="note"
|
||||
readonly={false}
|
||||
@@ -787,7 +746,7 @@ button:disabled {
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.portions !== undefined}
|
||||
<div class="field-group">
|
||||
<div class="field-section">
|
||||
<TranslationFieldComparison
|
||||
label="Portions"
|
||||
germanValue={germanData.portions || ''}
|
||||
@@ -799,130 +758,59 @@ button:disabled {
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.preparation !== undefined}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Preparation Time"
|
||||
germanValue={germanData.preparation || ''}
|
||||
englishValue={editableEnglish.preparation}
|
||||
fieldName="preparation"
|
||||
readonly={false}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
<!-- Ingredients and Instructions in two-column layout -->
|
||||
{#if editableEnglish?.ingredients || editableEnglish?.instructions}
|
||||
<div class="list-wrapper">
|
||||
<div>
|
||||
{#if editableEnglish?.ingredients}
|
||||
<CreateIngredientList bind:ingredients={editableEnglish.ingredients} lang="en" />
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.cooking !== undefined}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Cooking Time"
|
||||
germanValue={germanData.cooking || ''}
|
||||
englishValue={editableEnglish.cooking}
|
||||
fieldName="cooking"
|
||||
readonly={false}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{#if editableEnglish?.instructions && englishAddInfo}
|
||||
<CreateStepList bind:instructions={editableEnglish.instructions} add_info={englishAddInfo} lang="en" />
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.total_time !== undefined}
|
||||
<div class="field-group">
|
||||
<TranslationFieldComparison
|
||||
label="Total Time"
|
||||
germanValue={germanData.total_time || ''}
|
||||
englishValue={editableEnglish.total_time}
|
||||
fieldName="total_time"
|
||||
readonly={false}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.baking}
|
||||
<div class="field-group">
|
||||
<div class="field-label">Baking (Editable)</div>
|
||||
<div class="field-value">
|
||||
<TranslationFieldComparison
|
||||
label="Temperature"
|
||||
germanValue={germanData.baking?.temperature || ''}
|
||||
englishValue={editableEnglish.baking.temperature}
|
||||
fieldName="baking.temperature"
|
||||
readonly={false}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
<TranslationFieldComparison
|
||||
label="Time"
|
||||
germanValue={germanData.baking?.length || ''}
|
||||
englishValue={editableEnglish.baking.length}
|
||||
fieldName="baking.length"
|
||||
readonly={false}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
<TranslationFieldComparison
|
||||
label="Mode"
|
||||
germanValue={germanData.baking?.mode || ''}
|
||||
englishValue={editableEnglish.baking.mode}
|
||||
fieldName="baking.mode"
|
||||
readonly={false}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.fermentation}
|
||||
<div class="field-group">
|
||||
<div class="field-label">Fermentation (Editable)</div>
|
||||
<div class="field-value">
|
||||
{#if editableEnglish?.addendum !== undefined}
|
||||
<div class="field-section">
|
||||
<TranslationFieldComparison
|
||||
label="Bulk"
|
||||
germanValue={germanData.fermentation?.bulk || ''}
|
||||
englishValue={editableEnglish.fermentation.bulk}
|
||||
fieldName="fermentation.bulk"
|
||||
readonly={false}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
<TranslationFieldComparison
|
||||
label="Final"
|
||||
germanValue={germanData.fermentation?.final || ''}
|
||||
englishValue={editableEnglish.fermentation.final}
|
||||
fieldName="fermentation.final"
|
||||
label="Addendum"
|
||||
germanValue={germanData.addendum || ''}
|
||||
englishValue={editableEnglish.addendum}
|
||||
fieldName="addendum"
|
||||
readonly={false}
|
||||
multiline={true}
|
||||
on:change={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.ingredients && editableEnglish.ingredients.length > 0}
|
||||
<div class="field-group">
|
||||
<div class="field-label">Ingredients (Editable)</div>
|
||||
<EditableIngredients
|
||||
ingredients={editableEnglish.ingredients}
|
||||
translationMetadata={translationMetadata?.ingredientTranslations}
|
||||
on:change={handleIngredientsChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editableEnglish?.instructions && editableEnglish.instructions.length > 0}
|
||||
<div class="field-group">
|
||||
<div class="field-label">Instructions (Editable)</div>
|
||||
<EditableInstructions
|
||||
instructions={editableEnglish.instructions}
|
||||
translationMetadata={translationMetadata?.instructionTranslations}
|
||||
on:change={handleInstructionsChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
{#if translationState !== 'approved'}
|
||||
{#if translationState === 'idle'}
|
||||
<button class="btn-danger" on:click={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn-secondary" on:click={handleSkip}>
|
||||
Skip Translation
|
||||
</button>
|
||||
<button class="btn-primary" on:click={handleAutoTranslate} disabled={untranslatedBaseRecipes.length > 0}>
|
||||
{#if untranslatedBaseRecipes.length > 0}
|
||||
Translate base recipes first
|
||||
{:else}
|
||||
Auto-translate
|
||||
{/if}
|
||||
</button>
|
||||
{:else if translationState !== 'approved'}
|
||||
<button class="btn-danger" on:click={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn-secondary" on:click={handleForceFullRetranslation}>
|
||||
Vollständig neu übersetzen
|
||||
</button>
|
||||
<button class="btn-secondary" on:click={handleAutoTranslate}>
|
||||
Re-translate
|
||||
</button>
|
||||
|
||||
@@ -540,9 +540,6 @@ button.action_button{
|
||||
{#if !showTranslationWorkflow}
|
||||
<div class=submit_buttons>
|
||||
<button class=action_button on:click={doDelete}><p>Löschen</p><Cross fill=white width=2rem height=2rem></Cross></button>
|
||||
{#if translationData}
|
||||
<button class=action_button style="background-color: var(--nord13);" on:click={forceFullRetranslation}><p>Vollständig neu übersetzen</p><Check fill=white width=2rem height=2rem></Check></button>
|
||||
{/if}
|
||||
<button class=action_button on:click={prepareSubmit}><p>Weiter zur Übersetzung</p><Check fill=white width=2rem height=2rem></Check></button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -558,6 +555,7 @@ button.action_button{
|
||||
on:approved={handleTranslationApproved}
|
||||
on:skipped={handleTranslationSkipped}
|
||||
on:cancelled={handleTranslationCancelled}
|
||||
on:forceFullRetranslation={forceFullRetranslation}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -17,11 +17,27 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
})
|
||||
.populate({
|
||||
path: 'translations.en.ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations',
|
||||
populate: {
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations',
|
||||
populate: {
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations'
|
||||
}
|
||||
}
|
||||
})
|
||||
.populate({
|
||||
path: 'translations.en.instructions.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations',
|
||||
populate: {
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations',
|
||||
populate: {
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name ingredients instructions translations'
|
||||
}
|
||||
}
|
||||
})
|
||||
.lean();
|
||||
|
||||
@@ -64,23 +80,32 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
germanShortName: recipe.short_name,
|
||||
};
|
||||
|
||||
// Map populated base recipe refs to resolvedRecipe field
|
||||
if (englishRecipe.ingredients) {
|
||||
englishRecipe.ingredients = englishRecipe.ingredients.map((item: any) => {
|
||||
// Recursively map populated base recipe refs to resolvedRecipe field
|
||||
function mapBaseRecipeRefs(items: any[]): any[] {
|
||||
return items.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
const resolvedRecipe = { ...item.baseRecipeRef };
|
||||
|
||||
// Recursively map nested baseRecipeRefs
|
||||
if (resolvedRecipe.ingredients) {
|
||||
resolvedRecipe.ingredients = mapBaseRecipeRefs(resolvedRecipe.ingredients);
|
||||
}
|
||||
if (resolvedRecipe.instructions) {
|
||||
resolvedRecipe.instructions = mapBaseRecipeRefs(resolvedRecipe.instructions);
|
||||
}
|
||||
|
||||
return { ...item, resolvedRecipe };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
if (englishRecipe.instructions) {
|
||||
englishRecipe.instructions = englishRecipe.instructions.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
if (englishRecipe.ingredients) {
|
||||
englishRecipe.ingredients = mapBaseRecipeRefs(englishRecipe.ingredients);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
if (englishRecipe.instructions) {
|
||||
englishRecipe.instructions = mapBaseRecipeRefs(englishRecipe.instructions);
|
||||
}
|
||||
|
||||
// Merge English alt/caption with original image paths
|
||||
|
||||
@@ -8,12 +8,28 @@ export const GET: RequestHandler = async ({params}) => {
|
||||
await dbConnect();
|
||||
let recipe = await Recipe.findOne({ short_name: params.name})
|
||||
.populate({
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients translations',
|
||||
populate: {
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients translations',
|
||||
populate: {
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients translations'
|
||||
}
|
||||
}
|
||||
})
|
||||
.populate({
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name instructions translations',
|
||||
populate: {
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name instructions translations',
|
||||
populate: {
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name instructions translations'
|
||||
}
|
||||
}
|
||||
})
|
||||
.lean() as RecipeModelType[];
|
||||
|
||||
@@ -22,23 +38,32 @@ export const GET: RequestHandler = async ({params}) => {
|
||||
throw error(404, "Recipe not found")
|
||||
}
|
||||
|
||||
// Map populated refs to resolvedRecipe field
|
||||
if (recipe?.ingredients) {
|
||||
recipe.ingredients = recipe.ingredients.map((item: any) => {
|
||||
// Recursively map populated refs to resolvedRecipe field
|
||||
function mapBaseRecipeRefs(items: any[]): any[] {
|
||||
return items.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
const resolvedRecipe = { ...item.baseRecipeRef };
|
||||
|
||||
// Recursively map nested baseRecipeRefs
|
||||
if (resolvedRecipe.ingredients) {
|
||||
resolvedRecipe.ingredients = mapBaseRecipeRefs(resolvedRecipe.ingredients);
|
||||
}
|
||||
if (resolvedRecipe.instructions) {
|
||||
resolvedRecipe.instructions = mapBaseRecipeRefs(resolvedRecipe.instructions);
|
||||
}
|
||||
|
||||
return { ...item, resolvedRecipe };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
if (recipe?.instructions) {
|
||||
recipe.instructions = recipe.instructions.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
if (recipe?.ingredients) {
|
||||
recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
if (recipe?.instructions) {
|
||||
recipe.instructions = mapBaseRecipeRefs(recipe.instructions);
|
||||
}
|
||||
|
||||
return json(recipe);
|
||||
|
||||
@@ -931,6 +931,98 @@ class DeepLTranslationService {
|
||||
const group = newIngredients[i];
|
||||
const existingGroup = existingTranslatedIngredients[i];
|
||||
|
||||
// Handle base recipe references
|
||||
if (group.type === 'reference') {
|
||||
// If entire group doesn't exist in old version or no change info, translate all reference fields
|
||||
if (!changeInfo || !existingGroup) {
|
||||
const textsToTranslate: string[] = [group.labelOverride || ''];
|
||||
(group.itemsBefore || []).forEach((item: any) => {
|
||||
textsToTranslate.push(item.name || '');
|
||||
textsToTranslate.push(item.unit || '');
|
||||
});
|
||||
(group.itemsAfter || []).forEach((item: any) => {
|
||||
textsToTranslate.push(item.name || '');
|
||||
textsToTranslate.push(item.unit || '');
|
||||
});
|
||||
|
||||
const translated = await this.translateBatch(textsToTranslate);
|
||||
let index = 0;
|
||||
|
||||
result.push({
|
||||
type: 'reference',
|
||||
name: group.name,
|
||||
baseRecipeRef: group.baseRecipeRef,
|
||||
includeIngredients: group.includeIngredients,
|
||||
showLabel: group.showLabel,
|
||||
labelOverride: translated[index++],
|
||||
itemsBefore: (group.itemsBefore || []).map((item: any) => ({
|
||||
name: translated[index++],
|
||||
unit: translated[index++],
|
||||
amount: item.amount,
|
||||
})),
|
||||
itemsAfter: (group.itemsAfter || []).map((item: any) => ({
|
||||
name: translated[index++],
|
||||
unit: translated[index++],
|
||||
amount: item.amount,
|
||||
}))
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reference changed - translate changed fields
|
||||
const translatedRef: any = {
|
||||
type: 'reference',
|
||||
name: group.name,
|
||||
baseRecipeRef: group.baseRecipeRef,
|
||||
includeIngredients: group.includeIngredients,
|
||||
showLabel: group.showLabel,
|
||||
labelOverride: existingGroup.labelOverride,
|
||||
itemsBefore: existingGroup.itemsBefore || [],
|
||||
itemsAfter: existingGroup.itemsAfter || []
|
||||
};
|
||||
|
||||
// Translate labelOverride if changed
|
||||
if (changeInfo.nameChanged) {
|
||||
translatedRef.labelOverride = await this.translateText(group.labelOverride || '');
|
||||
}
|
||||
|
||||
// Translate itemsBefore if changed
|
||||
if (JSON.stringify(group.itemsBefore) !== JSON.stringify(existingGroup.itemsBefore)) {
|
||||
const textsToTranslate: string[] = [];
|
||||
(group.itemsBefore || []).forEach((item: any) => {
|
||||
textsToTranslate.push(item.name || '');
|
||||
textsToTranslate.push(item.unit || '');
|
||||
});
|
||||
const translated = await this.translateBatch(textsToTranslate);
|
||||
let index = 0;
|
||||
translatedRef.itemsBefore = (group.itemsBefore || []).map((item: any) => ({
|
||||
name: translated[index++],
|
||||
unit: translated[index++],
|
||||
amount: item.amount,
|
||||
}));
|
||||
}
|
||||
|
||||
// Translate itemsAfter if changed
|
||||
if (JSON.stringify(group.itemsAfter) !== JSON.stringify(existingGroup.itemsAfter)) {
|
||||
const textsToTranslate: string[] = [];
|
||||
(group.itemsAfter || []).forEach((item: any) => {
|
||||
textsToTranslate.push(item.name || '');
|
||||
textsToTranslate.push(item.unit || '');
|
||||
});
|
||||
const translated = await this.translateBatch(textsToTranslate);
|
||||
let index = 0;
|
||||
translatedRef.itemsAfter = (group.itemsAfter || []).map((item: any) => ({
|
||||
name: translated[index++],
|
||||
unit: translated[index++],
|
||||
amount: item.amount,
|
||||
}));
|
||||
}
|
||||
|
||||
result.push(translatedRef);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle regular ingredient sections
|
||||
// If entire group doesn't exist in old version or no change info, translate everything
|
||||
if (!changeInfo || !existingGroup) {
|
||||
const textsToTranslate: string[] = [group.name || ''];
|
||||
@@ -1068,6 +1160,76 @@ class DeepLTranslationService {
|
||||
const group = newInstructions[i];
|
||||
const existingGroup = existingTranslatedInstructions[i];
|
||||
|
||||
// Handle base recipe references
|
||||
if (group.type === 'reference') {
|
||||
// If entire group doesn't exist in old version or no change info, translate all reference fields
|
||||
if (!changeInfo || !existingGroup) {
|
||||
const textsToTranslate: string[] = [group.labelOverride || ''];
|
||||
(group.stepsBefore || []).forEach((step: string) => {
|
||||
textsToTranslate.push(step || '');
|
||||
});
|
||||
(group.stepsAfter || []).forEach((step: string) => {
|
||||
textsToTranslate.push(step || '');
|
||||
});
|
||||
|
||||
const translated = await this.translateBatch(textsToTranslate);
|
||||
let index = 0;
|
||||
|
||||
result.push({
|
||||
type: 'reference',
|
||||
name: group.name,
|
||||
baseRecipeRef: group.baseRecipeRef,
|
||||
includeInstructions: group.includeInstructions,
|
||||
showLabel: group.showLabel,
|
||||
labelOverride: translated[index++],
|
||||
stepsBefore: (group.stepsBefore || []).map(() => translated[index++]),
|
||||
stepsAfter: (group.stepsAfter || []).map(() => translated[index++])
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reference changed - translate changed fields
|
||||
const translatedRef: any = {
|
||||
type: 'reference',
|
||||
name: group.name,
|
||||
baseRecipeRef: group.baseRecipeRef,
|
||||
includeInstructions: group.includeInstructions,
|
||||
showLabel: group.showLabel,
|
||||
labelOverride: existingGroup.labelOverride,
|
||||
stepsBefore: existingGroup.stepsBefore || [],
|
||||
stepsAfter: existingGroup.stepsAfter || []
|
||||
};
|
||||
|
||||
// Translate labelOverride if changed
|
||||
if (changeInfo.nameChanged) {
|
||||
translatedRef.labelOverride = await this.translateText(group.labelOverride || '');
|
||||
}
|
||||
|
||||
// Translate stepsBefore if changed
|
||||
if (JSON.stringify(group.stepsBefore) !== JSON.stringify(existingGroup.stepsBefore)) {
|
||||
const textsToTranslate: string[] = [];
|
||||
(group.stepsBefore || []).forEach((step: string) => {
|
||||
textsToTranslate.push(step || '');
|
||||
});
|
||||
const translated = await this.translateBatch(textsToTranslate);
|
||||
translatedRef.stepsBefore = translated;
|
||||
}
|
||||
|
||||
// Translate stepsAfter if changed
|
||||
if (JSON.stringify(group.stepsAfter) !== JSON.stringify(existingGroup.stepsAfter)) {
|
||||
const textsToTranslate: string[] = [];
|
||||
(group.stepsAfter || []).forEach((step: string) => {
|
||||
textsToTranslate.push(step || '');
|
||||
});
|
||||
const translated = await this.translateBatch(textsToTranslate);
|
||||
translatedRef.stepsAfter = translated;
|
||||
}
|
||||
|
||||
result.push(translatedRef);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle regular instruction sections
|
||||
// If entire group doesn't exist in old version or no change info, translate everything
|
||||
if (!changeInfo || !existingGroup) {
|
||||
const textsToTranslate: string[] = [group.name || ''];
|
||||
|
||||
Reference in New Issue
Block a user