feat(vocab-prep): implement enhanced vocabulary preparation steps in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m58s

- Introduced a structured vocabulary preparation process with two review stages before engaging with the vocabulary trainer.
- Added localization support for new vocabulary preparation messages in German, English, and Spanish, improving accessibility for users.
- Updated the VocabLessonView component to display current progress and next steps during vocabulary preparation, enhancing user guidance.
- Refactored related logic to manage preparation stages and item navigation effectively.
This commit is contained in:
Torsten Schulz (local)
2026-04-01 08:58:36 +02:00
parent 0c89c48e68
commit 02b3636e10
4 changed files with 105 additions and 24 deletions

View File

@@ -450,6 +450,8 @@
"corePatternsHint": "Zuerst die Zielsprache lesen, darunter die deutsche Bedeutung — so lernst du jedes Muster bewusst in beiden Richtungen.",
"vocabPrepTitle": "Vorbereitung vor dem Vokabeltrainer",
"vocabPrepStep1": "Lies Kernmuster und Wortliste (Deutsch ↔ Zielsprache) einmal in Ruhe durch.",
"vocabPrepProgress": "Durchgang {pass}: Begriff {current} von {total}",
"vocabPrepNextItem": "Nächster Begriff",
"vocabPrepConfirm1": "Erste Durchsicht erledigt",
"vocabPrepStep2": "Gehe die gleichen Begriffe noch einmal durch (aktive Wiederholung, ohne zu üben).",
"vocabPrepConfirm2": "Zweite Durchsicht erledigt",

View File

@@ -450,6 +450,8 @@
"corePatternsHint": "Read the target language first, then the meaning below — you learn each pattern both ways.",
"vocabPrepTitle": "Preparation before the vocabulary trainer",
"vocabPrepStep1": "Read through core patterns and the word list (native language ↔ target language) once.",
"vocabPrepProgress": "Pass {pass}: item {current} of {total}",
"vocabPrepNextItem": "Next item",
"vocabPrepConfirm1": "First pass done",
"vocabPrepStep2": "Go through the same items again (active review, not testing yet).",
"vocabPrepConfirm2": "Second pass done",

View File

@@ -448,6 +448,8 @@
"corePatternsHint": "Primero la lengua meta; debajo, el significado en tu idioma.",
"vocabPrepTitle": "Preparación antes del entrenador de vocabulario",
"vocabPrepStep1": "Lee una vez los patrones clave y la lista de palabras (idioma nativo ↔ lengua meta).",
"vocabPrepProgress": "Pasada {pass}: término {current} de {total}",
"vocabPrepNextItem": "Siguiente término",
"vocabPrepConfirm1": "Primera lectura hecha",
"vocabPrepStep2": "Repasa los mismos elementos otra vez (repaso activo, aún sin practicar).",
"vocabPrepConfirm2": "Segunda lectura hecha",

View File

@@ -217,8 +217,41 @@
</div>
</div>
<!-- Wichtige Begriffe (vor dem Trainer: passiv, mit DE Zielsprache) -->
<div v-if="lesson && importantVocab && importantVocab.length > 0 && !vocabTrainerActive" class="vocab-list">
<!-- Zwei Durchgänge: dieselben Kernmuster schrittweise vor dem Trainer -->
<div
v-if="prepItems.length > 0 && !vocabTrainerActive"
class="vocab-prep-pass didactic-card"
>
<h4>{{ $t('socialnetwork.vocab.courses.vocabPrepTitle') }}</h4>
<template v-if="lessonPrepStage < 2 && currentPrepItem">
<p class="vocab-prep-pass__step">
{{ $t('socialnetwork.vocab.courses.vocabPrepProgress', { pass: lessonPrepStage + 1, current: lessonPrepIndex + 1, total: prepItems.length }) }}
</p>
<p>
{{ lessonPrepStage === 0
? $t('socialnetwork.vocab.courses.vocabPrepStep1')
: $t('socialnetwork.vocab.courses.vocabPrepStep2') }}
</p>
<div class="vocab-prep-card">
<div class="vocab-prep-card__target">{{ currentPrepItem.target }}</div>
<div v-if="currentPrepItem.gloss" class="vocab-prep-card__gloss">{{ currentPrepItem.gloss }}</div>
</div>
<button type="button" class="btn-prep-pass" @click="advancePrepPass">
{{ isLastPrepItemInPass
? (lessonPrepStage === 0
? $t('socialnetwork.vocab.courses.vocabPrepConfirm1')
: $t('socialnetwork.vocab.courses.vocabPrepConfirm2'))
: $t('socialnetwork.vocab.courses.vocabPrepNextItem') }}
</button>
</template>
<p v-else class="vocab-prep-pass__ready">{{ $t('socialnetwork.vocab.courses.vocabPrepReady') }}</p>
</div>
<!-- Wichtige Begriffe erst nach den zwei Durchgängen gesammelt anzeigen -->
<div
v-if="lesson && importantVocab && importantVocab.length > 0 && !vocabTrainerActive && lessonPrepStage >= 2"
class="vocab-list"
>
<h4>{{ $t('socialnetwork.vocab.courses.importantVocab') }}</h4>
<p class="vocab-info-text">{{ $t('socialnetwork.vocab.courses.vocabInfoText') }}</p>
<div class="vocab-items">
@@ -230,27 +263,6 @@
</div>
</div>
<!-- Zwei Durchgänge vor dem aktiven Üben -->
<div
v-if="importantVocab && importantVocab.length > 0 && !vocabTrainerActive"
class="vocab-prep-pass didactic-card"
>
<h4>{{ $t('socialnetwork.vocab.courses.vocabPrepTitle') }}</h4>
<template v-if="lessonPrepStage === 0">
<p>{{ $t('socialnetwork.vocab.courses.vocabPrepStep1') }}</p>
<button type="button" class="btn-prep-pass" @click="lessonPrepStage = 1">
{{ $t('socialnetwork.vocab.courses.vocabPrepConfirm1') }}
</button>
</template>
<template v-else-if="lessonPrepStage === 1">
<p>{{ $t('socialnetwork.vocab.courses.vocabPrepStep2') }}</p>
<button type="button" class="btn-prep-pass" @click="lessonPrepStage = 2">
{{ $t('socialnetwork.vocab.courses.vocabPrepConfirm2') }}
</button>
</template>
<p v-else class="vocab-prep-pass__ready">{{ $t('socialnetwork.vocab.courses.vocabPrepReady') }}</p>
</div>
<!-- Vokabeltrainer -->
<div v-if="importantVocab && importantVocab.length > 0" class="vocab-trainer-section">
<h4>{{ $t('socialnetwork.vocab.courses.vocabTrainer') }}</h4>
@@ -764,6 +776,7 @@ export default {
exercisePreparationCompleted: false,
/** 0 = noch kein Durchgang, 1 = erste Durchsicht, 2 = zweite Durchsicht — dann Vokabeltrainer */
lessonPrepStage: 0,
lessonPrepIndex: 0,
currentVocabQuestion: null,
vocabTrainerAnswer: '',
vocabTrainerSelectedChoice: null,
@@ -933,11 +946,34 @@ export default {
.map((p) => this.normalizeCorePatternEntry(p))
.filter(Boolean);
},
prepItems() {
if (this.normalizedCorePatterns.length > 0) {
return this.normalizedCorePatterns;
}
return (this.importantVocab || [])
.map((item) => ({
target: String(item?.reference || '').trim(),
gloss: String(item?.learning || '').trim()
}))
.filter((item) => item.target);
},
currentPrepItem() {
return this.prepItems[this.lessonPrepIndex] || null;
},
isLastPrepItemInPass() {
if (this.prepItems.length === 0) {
return true;
}
return this.lessonPrepIndex >= this.prepItems.length - 1;
},
corePatternsHaveGloss() {
return this.normalizedCorePatterns.some((p) => p.gloss);
},
canStartVocabTrainerPrep() {
if (!this.importantVocab || this.lessonPrepStage < 2) {
if (!this.importantVocab || this.importantVocab.length === 0) {
return false;
}
if (this.prepItems.length > 0 && this.lessonPrepStage < 2) {
return false;
}
return true;
@@ -1011,6 +1047,17 @@ export default {
if (!n) return '';
return n.gloss ? `${n.target} (${n.gloss})` : n.target;
},
advancePrepPass() {
if (this.lessonPrepStage >= 2 || this.prepItems.length === 0) {
return;
}
if (!this.isLastPrepItemInPass) {
this.lessonPrepIndex += 1;
return;
}
this.lessonPrepStage += 1;
this.lessonPrepIndex = 0;
},
openExercisesTab() {
if (!this.canAccessExercises) {
this.activeTab = 'learn';
@@ -1202,6 +1249,7 @@ export default {
this.assistantError = '';
this.exercisePreparationCompleted = false;
this.lessonPrepStage = 0;
this.lessonPrepIndex = 0;
this.vocabTrainerActive = false;
this.vocabTrainerPool = [];
this.vocabTrainerMixedPool = [];
@@ -2423,6 +2471,33 @@ export default {
margin-bottom: 18px;
}
.vocab-prep-pass__step {
margin: 0 0 10px;
font-size: 0.9rem;
color: #7a6848;
font-weight: 600;
}
.vocab-prep-card {
margin-top: 12px;
padding: 16px 18px;
background: #fff;
border: 1px solid #e5dccf;
border-radius: 10px;
}
.vocab-prep-card__target {
font-size: 1.2rem;
font-weight: 700;
color: #1f1a16;
}
.vocab-prep-card__gloss {
margin-top: 8px;
font-size: 1rem;
color: #6a5a44;
}
.vocab-prep-pass .btn-prep-pass {
margin-top: 8px;
}