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

@@ -409,7 +409,7 @@
"courseFlowTitle": "Maayong ipadayon karon",
"courseFlowIntro": "Una ang gisugyot karon nga sunod-sunod sa ibabaw. Sa ubos, ang upat ka bahin: angay nga balik-balik, kasamtangang block, intensive phase, libre nga pagpalalom.",
"courseTodayPlanTitle": "Gisugyot karong adlawa",
"courseTodayPlanIntro": "Praktikal nga paagi: una ang tanan nga angay na nga mubo nga balik-balik, dayon ang abli nga mga leksiyon sa imong block, unya ang intensive kung naay ipakita. Ang mubo nga balik-balik tulo ka mubo nga petsa human mahuman ang leksiyon ( kasagaran mga 1, 3 ug 7 ka adlaw) aron mahuptan ang mga pulong.",
"courseTodayPlanIntro": "Sunodsunod nga makat-on: una ang tanan nga angay nga mubo nga balik-balik (spacing). Unya limitado ra nga abli nga mga leksiyon sa block — ang lisud, checkpoint o intensive mokabatog doble aron dili kaayo bug-at ang adlaw. Unya ang intensive kung naay. Ang mubo nga balik-balik tulo ka petsa human sa leksiyon (mga 1, 3, 7 ka adlaw).",
"courseTodayPlanStepReviewDue": "Mubo nga balik-balik karon",
"courseTodayPlanStepBlock": "Padayon sa kasamtangang block",
"courseTodayPlanStepIntensive": "Intensive nga balik-balik",
@@ -418,6 +418,8 @@
"courseTodayPlanOpen": "Ablihi ang leksiyon",
"courseTodayPlanTrainer": "Ablihi ang trainer",
"courseTodayPlanEmpty": "Walay gikatakda nga angay karon ug walay klaro nga sunod nga lakang sa block. Pilia ang leksiyon sa ubos o gamita ang libre nga pagpalalom sa trainer.",
"courseTodayPlanSoftTitle": "Walay gikinahanglan karon — okay ang pahuway",
"courseTodayPlanSoftIntro": "Ang pag-ila sa gilayon makatabang sa memorya: walay urgent sa imong agianan karon. Ang mosunod opsyonal ra; mas maayo usahay ugma nga bag-ong huna-huna.",
"courseFlowReviewStat": "Angay balikon: {count}",
"courseFlowBlockStat": "Aktibong block: {block}",
"courseFlowReviewTitle": "Angay nga balik-balikon",
@@ -717,7 +719,7 @@
"quickReviewPromptMeaning": "What does \"{term}\" mean?",
"quickReviewPromptTarget": "Type sa target pinulongan: \"{term}\"",
"quickReviewAcknowledge": "Read, continue",
"courseTodayPlanIntroNoDueReview": "Walay quick review is due today. Sugdi ang uban sa ang open leksiyons sa imong current block, then do intensive review if shown. Quick reviews reappear automatically sa ang 1/3/7-day rhythm."
"courseTodayPlanIntroNoDueReview": "Walay angay nga mubo nga balik-balik karon. Makita nimo ang sunod nga makatarunganon nga lakang sa block (limitado sa kabug-aton), unya ang intensive kung naay. Ang mubo nga balik-balik mobalik sa 1/3/7 ka adlaw."
},
"title": "Trainer sa bokabularyo",
"description": "Paghimo og pinulongans (or subscribe aron them) ug share them uban sa friends.",

View File

@@ -696,8 +696,8 @@
"quickReviewPromptTarget": "Tippe auf Zielsprache: \"{term}\"",
"quickReviewAcknowledge": "Gelesen, weiter",
"courseTodayPlanTitle": "Empfehlung für heute",
"courseTodayPlanIntro": "So kannst du vorgehen: zuerst alles unter „Jetzt kurz wiederholen“, dann die offenen Lektionen deines Blocks, danach ggf. die Intensivphase. Kurz-Wiederholungen sind die drei kleinen Termine nach Lektionsende (typisch nach etwa 1, 3 und 7 Tagen), damit Vokabeln hängen bleiben.",
"courseTodayPlanIntroNoDueReview": "Heute ist keine Kurz-Wiederholung fällig. Starte mit den offenen Lektionen im aktuellen Block und nimm danach ggf. die Intensivphase mit. Kurz-Wiederholungen erscheinen automatisch wieder nach dem 1/3/7-Tage-Rhythmus.",
"courseTodayPlanIntro": "Didaktische Reihenfolge: Zuerst alle fälligen Kurz-Wiederholungen (Abstandslernen Abruf, bevor neues Material kommt). Danach nur eine begrenzte Auswahl offener Lektionen aus deinem Block: schwere, Prüfungs- oder Intensiv-Lektionen zählen doppelt, damit die Tageslast nicht zu hoch wird. Anschließend ggf. die Intensivphase. Die Kurz-Wiederholungen sind die drei Termine nach Lektionsende (typisch etwa 1, 3 und 7 Tage).",
"courseTodayPlanIntroNoDueReview": "Heute ist keine Kurz-Wiederholung fällig. Es werden nur die nächsten sinnvollen Schritte im aktuellen Block vorgeschlagen (Anzahl begrenzt nach Schwierigkeit), danach ggf. die Intensivphase. Kurz-Wiederholungen erscheinen automatisch wieder nach dem 1/3/7-Tage-Rhythmus.",
"courseTodayPlanStepReviewDue": "Jetzt kurz wiederholen",
"courseTodayPlanStepBlock": "Weiter im aktuellen Block",
"courseTodayPlanStepIntensive": "Intensive Wiederholung",
@@ -706,6 +706,8 @@
"courseTodayPlanOpen": "Lektion öffnen",
"courseTodayPlanTrainer": "Im Trainer üben",
"courseTodayPlanEmpty": "Gerade ist keine gestaffelte Wiederholung fällig und es gibt keinen klaren nächsten Block-Schritt. Wähle unten eine Lektion oder nutze die freie Vertiefung mit dem Trainer.",
"courseTodayPlanSoftTitle": "Heute nichts Pflichtiges Pause ist erlaubt",
"courseTodayPlanSoftIntro": "Abstand stärkt das Gedächtnis: Auf deiner Lernroute ist gerade nichts Dringendes vorgesehen. Die folgenden Punkte sind nur optional oft ist es didaktisch sinnvoller, morgen mit frischer Aufmerksamkeit weiterzumachen.",
"courseFlowReviewStat": "Fällige Wiederholung: {count}",
"courseFlowBlockStat": "Aktiver Block: {block}",
"courseFlowReviewTitle": "Fällige Wiederholung",

View File

@@ -696,8 +696,8 @@
"quickReviewPromptTarget": "Type in target language: \"{term}\"",
"quickReviewAcknowledge": "Read, continue",
"courseTodayPlanTitle": "Suggested for today",
"courseTodayPlanIntro": "Practical order: first everything that is due for a quick review, then open lessons in your current block, then intensive review if shown. Quick reviews are three short revisit dates after you finish a lesson (typically about 1, 3 and 7 days apart) so vocabulary sticks.",
"courseTodayPlanIntroNoDueReview": "No quick review is due today. Start with the open lessons in your current block, then do intensive review if shown. Quick reviews reappear automatically on the 1/3/7-day rhythm.",
"courseTodayPlanIntro": "Pedagogical order: first every due quick review (spaced retrieval before new input). Then only a limited set of open lessons from your current block—harder, checkpoint or intensive lessons count double so the daily load stays manageable. Then intensive review if unlocked. Quick reviews are the three short revisit dates after a lesson (typically about 1, 3 and 7 days apart).",
"courseTodayPlanIntroNoDueReview": "No quick review is due today. Youll see the next sensible steps in your current block only (capped by difficulty), then intensive review if shown. Quick reviews reappear automatically on the 1/3/7-day rhythm.",
"courseTodayPlanStepReviewDue": "Quick review now",
"courseTodayPlanStepBlock": "Continue in your current block",
"courseTodayPlanStepIntensive": "Intensive review",
@@ -706,6 +706,8 @@
"courseTodayPlanOpen": "Open lesson",
"courseTodayPlanTrainer": "Open trainer",
"courseTodayPlanEmpty": "Nothing scheduled as due right now and no clear next block step. Pick a lesson below or use free practice with the trainer.",
"courseTodayPlanSoftTitle": "Nothing required today — a break is fine",
"courseTodayPlanSoftIntro": "Spacing helps memory: nothing urgent is on your route right now. The items below are optional—often it is better pedagogy to continue tomorrow with a fresh mind.",
"courseFlowReviewStat": "Due review: {count}",
"courseFlowBlockStat": "Active block: {block}",
"courseFlowReviewTitle": "Due review",

View File

@@ -694,8 +694,8 @@
"quickReviewPromptTarget": "Escribe en la lengua meta: \"{term}\"",
"quickReviewAcknowledge": "Leído, continuar",
"courseTodayPlanTitle": "Sugerencia para hoy",
"courseTodayPlanIntro": "Orden práctico: primero todo lo marcado como repaso breve pendiente, luego las lecciones abiertas de tu bloque, después la fase intensiva si aparece. Los repasos breves son tres citas cortas tras terminar una lección (típicamente aprox. 1, 3 y 7 días) para fijar vocabulario.",
"courseTodayPlanIntroNoDueReview": "Hoy no hay repaso breve pendiente. Empieza con las lecciones abiertas de tu bloque actual y luego haz la fase intensiva si aparece. Los repasos breves vuelven automáticamente con el ritmo de 1/3/7 días.",
"courseTodayPlanIntro": "Orden pedagógico: primero los repasos breves pendientes (recuperación espaciada antes de material nuevo). Luego solo un número limitado de lecciones abiertas del bloque actual: las difíciles, de control o intensivas cuentan doble para no sobrecargar el día. Después, fase intensiva si aplica. Los repasos breves son tres citas tras la lección (aprox. 1, 3 y 7 días).",
"courseTodayPlanIntroNoDueReview": "Hoy no hay repaso breve pendiente. Verás solo los siguientes pasos razonables en tu bloque (limitados por dificultad), luego la fase intensiva si aparece. Los repasos breves vuelven con el ritmo de 1/3/7 días.",
"courseTodayPlanStepReviewDue": "Repaso breve ahora",
"courseTodayPlanStepBlock": "Seguir en el bloque actual",
"courseTodayPlanStepIntensive": "Repaso intensivo",
@@ -704,6 +704,8 @@
"courseTodayPlanOpen": "Abrir lección",
"courseTodayPlanTrainer": "Abrir entrenador",
"courseTodayPlanEmpty": "Ahora no hay repaso escalonado pendiente ni un siguiente paso claro en el bloque. Elige una lección abajo o usa la práctica libre con el entrenador.",
"courseTodayPlanSoftTitle": "Hoy nada obligatorio — descansar está bien",
"courseTodayPlanSoftIntro": "El espaciado ayuda a la memoria: ahora no hay nada urgente en tu ruta. Lo siguiente es opcional; a menudo es mejor seguir mañana con la mente fresca.",
"courseFlowReviewStat": "Repaso pendiente: {count}",
"courseFlowBlockStat": "Bloque activo: {block}",
"courseFlowReviewTitle": "Repaso pendiente",

View File

@@ -694,8 +694,8 @@
"quickReviewPromptTarget": "Appuyez sur la langue cible : \"{term}\"",
"quickReviewAcknowledge": "Lire, continuer",
"courseTodayPlanTitle": "Recommandation pour aujourd'hui",
"courseTodayPlanIntro": "Voici comment procéder : tout d'abord sous « Maintenant, répétez brièvement », puis les leçons ouvertes dans votre bloc, puis, si nécessaire, la phase intensive. Les répétitions courtes sont les trois petites dates après la fin de la leçon (généralement après environ 1, 3 et 7 jours) afin que le vocabulaire reste fidèle.",
"courseTodayPlanIntroNoDueReview": "Aucune courte répétition nest prévue aujourdhui. Commencez par les cours ouverts du bloc en cours puis, si cessaire, passez à la phase intensive. De courtes répétitions réapparaissent automatiquement sur un cycle de 1/3/7 jours.",
"courseTodayPlanIntro": "Ordre pédagogique : dabord chaque répétition courte due (rappel espacé avant le nouveau contenu). Ensuite seulement un nombre limité de leçons ouvertes dans le bloc en cours : les leçons difficiles, bilan ou intensives comptent double pour limiter la charge du jour. Puis la phase intensive si elle est débloquée. Les répétitions courtes sont les trois dates après la leçon (souvent environ 1, 3 et 7 jours).",
"courseTodayPlanIntroNoDueReview": "Aucune courte répétition nest due aujourdhui. Vous verrez seulement les prochaines étapes utiles du bloc (plafonnées selon la difficulté), puis la phase intensive si besoin. Les répétitions courtes reviennent selon le rythme 1/3/7 jours.",
"courseTodayPlanStepReviewDue": "Maintenant, répétez brièvement",
"courseTodayPlanStepBlock": "Continuer dans le bloc actuel",
"courseTodayPlanStepIntensive": "Répétition intense",
@@ -704,6 +704,8 @@
"courseTodayPlanOpen": "Leçon ouverte",
"courseTodayPlanTrainer": "Pratique dans le formateur",
"courseTodayPlanEmpty": "Il ny a actuellement aucune répétition échelonnée et il ny a pas détape de bloc suivante claire. Choisissez une leçon ci-dessous ou utilisez l'étude approfondie gratuite avec le formateur.",
"courseTodayPlanSoftTitle": "Rien dobligatoire aujourdhui — une pause, cest bien",
"courseTodayPlanSoftIntro": "Lespacement renforce la mémoire : rien durgent sur votre parcours pour linstant. Les éléments ci-dessous sont facultatifs ; souvent il vaut mieux reprendre demain lesprit frais.",
"courseFlowReviewStat": "Répétition due : {count}",
"courseFlowBlockStat": "Bloc actif : {block}",
"courseFlowReviewTitle": "Répétition due",

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;