feat(i18n, frontend): enhance course planning with optional steps and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s

- Added a new section for optional learning steps in the course planning UI, allowing users to engage with additional content when no mandatory tasks are due.
- Updated localization files for Cebuano, German, English, Spanish, and French to reflect changes in course planning instructions and titles, ensuring clarity and consistency across languages.
- Improved pedagogical logic for lesson recommendations, focusing on cognitive load and spaced repetition principles to enhance user learning experience.
This commit is contained in:
Torsten Schulz (local)
2026-04-10 13:33:25 +02:00
parent d17c8a341d
commit 545314e905
6 changed files with 117 additions and 14 deletions

View File

@@ -74,6 +74,26 @@
</li>
</ol>
</div>
<div v-else-if="showTodayPlanSoftOptional" class="course-today-plan course-today-plan--soft">
<h4 class="course-today-plan__title">{{ $t('socialnetwork.vocab.courses.courseTodayPlanSoftTitle') }}</h4>
<p class="course-today-plan__intro">{{ $t('socialnetwork.vocab.courses.courseTodayPlanSoftIntro') }}</p>
<ol class="course-today-plan__list">
<li
v-for="(step, idx) in todayPlanOptionalSteps"
:key="`opt-${step.type}-${step.lesson.id}-${idx}`"
class="course-today-plan__item"
>
<div class="course-today-plan__item-main">
<span class="course-today-plan__step-label">{{ todayPlanStepLabel(step.type) }}</span>
<span class="course-today-plan__lesson-title">{{ step.lesson.title }}</span>
<span class="course-today-plan__lesson-meta">#{{ step.lesson.lessonNumber }}</span>
</div>
<button type="button" class="course-today-plan__action course-today-plan__action--soft" @click="openTodayPlanStep(step)">
{{ step.type === 'practice' ? $t('socialnetwork.vocab.courses.courseTodayPlanTrainer') : $t('socialnetwork.vocab.courses.courseTodayPlanOpen') }}
</button>
</li>
</ol>
</div>
<div v-else class="course-today-plan course-today-plan--empty">
<h4 class="course-today-plan__title">{{ $t('socialnetwork.vocab.courses.courseTodayPlanTitle') }}</h4>
<p class="course-today-plan__intro">{{ $t('socialnetwork.vocab.courses.courseTodayPlanEmpty') }}</p>
@@ -400,6 +420,15 @@ export default {
})
.slice(0, 4);
},
/**
* Didaktischer Tagesvorschlag (nicht Kalender-fixiert, aber nach Prinzipien):
* 1) Spacing: fällige Kurz-Wiederholungen zuerst (Abruf vor neuem Input).
* 2) Kognitive Last: nur eine begrenzte „Einheit“ neuer Block-Lektionen pro Aufruf,
* schwere/checkpoint/intensive Lektionen zählen stärker (siehe pickPedagogicalBlockSteps).
* 3) Dann Intensivphase, wenn freigeschaltet.
* Keine „Fallbacks“ in dieser Liste: weitere Lektion/Trainer nur unter
* todayPlanOptionalSteps mit eigener Einleitung (Pause/Spacing sinnvoller als Pflicht).
*/
todayRecommendedSteps() {
const out = [];
const seen = new Set();
@@ -411,20 +440,36 @@ export default {
out.push({ type, lesson });
};
this.dueReviewLessons.forEach((l) => push('review_due', l));
this.currentBlockLessons.slice(0, 3).forEach((l) => push('block', l));
this.pickPedagogicalBlockSteps().forEach((l) => push('block', l));
if (this.nextIntensiveReviewLesson) {
push('intensive', this.nextIntensiveReviewLesson);
}
if (out.length === 0 && this.currentLesson) {
return out.slice(0, 8);
},
/** Optional: nur wenn kein didaktischer Pflichtplan semantisch „könntest du, Pause oft besser“. */
todayPlanOptionalSteps() {
const out = [];
const seen = new Set();
const push = (type, lesson) => {
if (!lesson?.id || seen.has(lesson.id)) {
return;
}
seen.add(lesson.id);
out.push({ type, lesson });
};
if (this.currentLesson) {
const p = this.getLessonProgress(this.currentLesson.id, this.currentLesson);
if (!p?.completed) {
push('continue', this.currentLesson);
}
}
if (out.length === 0 && this.freePracticeLessons.length > 0) {
if (this.freePracticeLessons.length > 0) {
push('practice', this.freePracticeLessons[0]);
}
return out.slice(0, 8);
return out;
},
showTodayPlanSoftOptional() {
return this.todayRecommendedSteps.length === 0 && this.todayPlanOptionalSteps.length > 0;
},
isLessonNumberValid() {
return Number(this.newLesson.lessonNumber) > 0;
@@ -510,6 +555,45 @@ export default {
const progress = this.getLessonProgress(lessonId, lesson);
return progress?.lastAccessedAt || progress?.completedAt || progress?.updatedAt || '';
},
/**
* Grobe „Last-Einheit“ pro Lektion für Tageskappung (1 = leicht, 2 = schwer/Checkpoint/Intensiv im Block).
*/
blockLessonDailyLoadUnits(lesson) {
if (!lesson) {
return 1;
}
const p = lesson.pedagogy || {};
const mode = p.didacticMode;
if (mode === 'checkpoint' || mode === 'intensive_review' || p.isIntensiveReview) {
return 2;
}
const w = Number(p.difficultyWeight);
if (Number.isFinite(w) && w >= 4) {
return 2;
}
return 1;
},
/**
* Offene Lektionen im Block nach Nummer, mit Obergrenze für „neues Material“ pro Empfehlungsliste
* (Spacing + cognitive load: nicht drei schwere Lektionen auf einmal).
*/
pickPedagogicalBlockSteps() {
const list = [...this.currentBlockLessons].sort(
(a, b) => Number(a.lessonNumber) - Number(b.lessonNumber)
);
const MAX_UNITS = 3;
const picked = [];
let units = 0;
for (const l of list) {
const cost = this.blockLessonDailyLoadUnits(l);
if (units + cost > MAX_UNITS) {
break;
}
picked.push(l);
units += cost;
}
return picked;
},
daysSince(dateString) {
if (!dateString) {
return 0;
@@ -1360,6 +1444,11 @@ export default {
border: 1px solid rgba(212, 184, 150, 0.45);
}
.course-today-plan--soft {
border-style: dashed;
opacity: 0.95;
}
.course-today-plan--empty {
background: rgba(250, 250, 250, 0.85);
border-color: var(--color-border, #e0dcd6);
@@ -1433,6 +1522,10 @@ export default {
cursor: pointer;
}
.course-today-plan__action--soft {
font-weight: normal;
}
.course-today-plan__action:hover {
border-color: #b8a896;
background: #fff;