feat(bisaya-course): restructure core patterns and enhance vocabulary preparation
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s

- Updated core patterns in various scripts to use an object format with target phrases and glosses, improving clarity and usability for learners.
- Enhanced the VocabService to normalize core pattern entries, ensuring consistent handling of vocabulary data.
- Introduced new vocabulary preparation steps in the VocabLessonView, guiding users through active review processes before engaging with the vocabulary trainer.
- Added localization support for new vocabulary preparation hints and instructions in multiple languages, enhancing user experience across the application.
This commit is contained in:
Torsten Schulz (local)
2026-04-01 08:12:57 +02:00
parent 7e45049e94
commit 0c89c48e68
9 changed files with 312 additions and 59 deletions

View File

@@ -447,6 +447,14 @@
"grammarImpulse": "Grammatik-Impuls",
"learningGoals": "Lernziele",
"corePatterns": "Kernmuster",
"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.",
"vocabPrepConfirm1": "Erste Durchsicht erledigt",
"vocabPrepStep2": "Gehe die gleichen Begriffe noch einmal durch (aktive Wiederholung, ohne zu üben).",
"vocabPrepConfirm2": "Zweite Durchsicht erledigt",
"vocabPrepReady": "Du kannst jetzt mit dem Vokabeltrainer starten.",
"vocabTrainerLockedHint": "Bitte bestätige zuerst zwei Lern-Durchgänge bei „Vorbereitung vor dem Vokabeltrainer“.",
"speakingTasks": "Sprechaufträge",
"speakingPrompt": "Sprechauftrag",
"practicalTasks": "Praxisaufgaben",

View File

@@ -447,6 +447,14 @@
"grammarImpulse": "Grammar Focus",
"learningGoals": "Learning Goals",
"corePatterns": "Core Patterns",
"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.",
"vocabPrepConfirm1": "First pass done",
"vocabPrepStep2": "Go through the same items again (active review, not testing yet).",
"vocabPrepConfirm2": "Second pass done",
"vocabPrepReady": "You can start the vocabulary trainer now.",
"vocabTrainerLockedHint": "Please confirm two preparation steps under “Preparation before the vocabulary trainer” first.",
"speakingTasks": "Speaking Tasks",
"speakingPrompt": "Speaking Prompt",
"practicalTasks": "Practical Tasks",

View File

@@ -445,6 +445,14 @@
"grammarImpulse": "Impulso gramatical",
"learningGoals": "Objetivos",
"corePatterns": "Patrones básicos",
"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).",
"vocabPrepConfirm1": "Primera lectura hecha",
"vocabPrepStep2": "Repasa los mismos elementos otra vez (repaso activo, aún sin practicar).",
"vocabPrepConfirm2": "Segunda lectura hecha",
"vocabPrepReady": "Ya puedes iniciar el entrenador de vocabulario.",
"vocabTrainerLockedHint": "Confirma primero los dos pasos de preparación arriba.",
"speakingTasks": "Tareas orales",
"speakingPrompt": "Tarea oral",
"practicalTasks": "Tareas prácticas",

View File

@@ -92,11 +92,15 @@
</div>
</div>
<div v-if="lessonDidactics.corePatterns.length > 0" class="didactic-card">
<div v-if="normalizedCorePatterns.length > 0" class="didactic-card">
<h4>{{ $t('socialnetwork.vocab.courses.corePatterns') }}</h4>
<p v-if="corePatternsHaveGloss" class="core-patterns-hint">
{{ $t('socialnetwork.vocab.courses.corePatternsHint') }}
</p>
<div class="pattern-list">
<div v-for="(pattern, index) in lessonDidactics.corePatterns" :key="'pattern-' + index" class="pattern-item">
{{ pattern }}
<div v-for="(pattern, index) in normalizedCorePatterns" :key="'pattern-' + index" class="pattern-item">
<div class="pattern-target">{{ pattern.target }}</div>
<div v-if="pattern.gloss" class="pattern-gloss">{{ pattern.gloss }}</div>
</div>
</div>
</div>
@@ -213,6 +217,40 @@
</div>
</div>
<!-- Wichtige Begriffe (vor dem Trainer: passiv, mit DE Zielsprache) -->
<div v-if="lesson && importantVocab && importantVocab.length > 0 && !vocabTrainerActive" 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">
<div v-for="(vocab, index) in importantVocab" :key="index" class="vocab-item">
<strong>{{ vocab.learning }}</strong>
<span class="separator"></span>
<span>{{ vocab.reference }}</span>
</div>
</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>
@@ -225,10 +263,13 @@
<p>{{ exerciseUnlockHint }}</p>
</div>
<div v-if="!vocabTrainerActive" class="vocab-trainer-start">
<p>{{ hasPreviousVocab ? 'Starte mit den neuen Vokabeln dieser Lektion. Mit fortschreitendem Üben mischt der Trainer automatisch passende Wiederholungen ein.' : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p>
<button @click="startVocabTrainer" class="btn-start-trainer">
{{ hasPreviousVocab ? 'Lektion starten' : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
</button>
<template v-if="canStartVocabTrainerPrep">
<p>{{ hasPreviousVocab ? 'Starte mit den neuen Vokabeln dieser Lektion. Mit fortschreitendem Üben mischt der Trainer automatisch passende Wiederholungen ein.' : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p>
<button @click="startVocabTrainer" class="btn-start-trainer">
{{ hasPreviousVocab ? 'Lektion starten' : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
</button>
</template>
<p v-else class="vocab-trainer-locked-hint">{{ $t('socialnetwork.vocab.courses.vocabTrainerLockedHint') }}</p>
</div>
<div v-else class="vocab-trainer-active">
<div class="vocab-trainer-stats">
@@ -313,19 +354,6 @@
</div>
</div>
<!-- Wichtige Begriffe Liste (nur Anzeige) -->
<div v-if="lesson && importantVocab && importantVocab.length > 0 && !vocabTrainerActive" 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">
<div v-for="(vocab, index) in importantVocab" :key="index" class="vocab-item">
<strong>{{ vocab.learning }}</strong>
<span class="separator"></span>
<span>{{ vocab.reference }}</span>
</div>
</div>
</div>
<!-- Hinweis wenn keine Vokabeln vorhanden -->
<div v-else-if="lesson && (!importantVocab || importantVocab.length === 0)" class="no-vocab-info">
<p>{{ $t('socialnetwork.vocab.courses.noVocabInfo') }}</p>
@@ -734,6 +762,8 @@ export default {
vocabTrainerCurrentAttempts: 0,
vocabTrainerReviewAttempts: 0,
exercisePreparationCompleted: false,
/** 0 = noch kein Durchgang, 1 = erste Durchsicht, 2 = zweite Durchsicht — dann Vokabeltrainer */
lessonPrepStage: 0,
currentVocabQuestion: null,
vocabTrainerAnswer: '',
vocabTrainerSelectedChoice: null,
@@ -897,6 +927,21 @@ export default {
practicalTasks: []
};
},
normalizedCorePatterns() {
const raw = this.lessonDidactics.corePatterns || [];
return raw
.map((p) => this.normalizeCorePatternEntry(p))
.filter(Boolean);
},
corePatternsHaveGloss() {
return this.normalizedCorePatterns.some((p) => p.gloss);
},
canStartVocabTrainerPrep() {
if (!this.importantVocab || this.lessonPrepStage < 2) {
return false;
}
return true;
},
lessonPedagogy() {
return this.lesson?.pedagogy || {
didacticMode: null,
@@ -943,6 +988,29 @@ export default {
}
},
methods: {
normalizeCorePatternEntry(p) {
if (p && typeof p === 'object' && p.target) {
return {
target: String(p.target).trim(),
gloss: String(p.gloss || '').trim()
};
}
const s = String(p || '').trim();
if (!s) return null;
const i = s.indexOf('|');
if (i !== -1) {
return {
target: s.slice(0, i).trim(),
gloss: s.slice(i + 1).trim()
};
}
return { target: s, gloss: '' };
},
corePatternToDisplayString(p) {
const n = this.normalizeCorePatternEntry(p);
if (!n) return '';
return n.gloss ? `${n.target} (${n.gloss})` : n.target;
},
openExercisesTab() {
if (!this.canAccessExercises) {
this.activeTab = 'learn';
@@ -1133,6 +1201,7 @@ export default {
this.assistantInput = '';
this.assistantError = '';
this.exercisePreparationCompleted = false;
this.lessonPrepStage = 0;
this.vocabTrainerActive = false;
this.vocabTrainerPool = [];
this.vocabTrainerMixedPool = [];
@@ -1222,10 +1291,11 @@ export default {
buildAssistantPrompt(preset) {
const lessonTitle = this.lesson?.title || this.$t('socialnetwork.vocab.courses.thisLesson');
const firstPattern = this.lessonDidactics.corePatterns?.[0];
const firstPatternStr = firstPattern ? this.corePatternToDisplayString(firstPattern) : '';
const firstGrammar = this.lessonDidactics.grammarFocus?.[0]?.text;
if (preset === 'explain') {
return `${this.$t('socialnetwork.vocab.courses.languageAssistantPresetExplainStart')} "${lessonTitle}". ${firstPattern ? `${this.$t('socialnetwork.vocab.courses.languageAssistantPatternHint')} ${firstPattern}.` : ''} ${firstGrammar || ''}`.trim();
return `${this.$t('socialnetwork.vocab.courses.languageAssistantPresetExplainStart')} "${lessonTitle}". ${firstPatternStr ? `${this.$t('socialnetwork.vocab.courses.languageAssistantPatternHint')} ${firstPatternStr}.` : ''} ${firstGrammar || ''}`.trim();
}
if (preset === 'correct') {
return this.$t('socialnetwork.vocab.courses.languageAssistantPresetCorrectStart', { lesson: lessonTitle });
@@ -1669,6 +1739,9 @@ export default {
debugLog('[VocabLessonView] Keine Vokabeln vorhanden');
return;
}
if (!this.canStartVocabTrainerPrep) {
return;
}
debugLog('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
debugLog('[VocabLessonView] Alte Vokabeln:', this.previousVocab?.length || 0);
this.vocabTrainerActive = true;
@@ -2329,6 +2402,42 @@ export default {
border-radius: 8px;
}
.pattern-target {
font-weight: 600;
color: #1a1a1a;
}
.pattern-gloss {
margin-top: 6px;
font-size: 0.92rem;
color: #555;
}
.core-patterns-hint {
margin: 0 0 12px;
font-size: 0.9rem;
color: #666;
}
.vocab-prep-pass {
margin-bottom: 18px;
}
.vocab-prep-pass .btn-prep-pass {
margin-top: 8px;
}
.vocab-prep-pass__ready {
margin: 0;
color: #2d6a3e;
font-weight: 600;
}
.vocab-trainer-locked-hint {
margin: 0;
color: #8a5a00;
}
.grammar-example,
.speaking-cue,
.pattern-drill-hint {