From 039059b4acf0f8c01d74d1b490af81efba4f7624 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 22 Sep 2026 09:54:27 +0200 Subject: [PATCH] feat(bisaya): enhance grammar focus and exercises in Bisaya course content --- .../scripts/apply-bisaya-course-refresh.js | 6 +- backend/scripts/bisaya-course-phase1.js | 7 + .../scripts/create-bisaya-course-content.js | 24 +++ backend/scripts/create-bisaya-course.js | 12 ++ backend/scripts/update-bisaya-didactics.js | 3 +- backend/services/vocabService.js | 109 ++++++++-- frontend/src/views/social/VocabLessonView.vue | 202 ++++++++++++++---- 7 files changed, 294 insertions(+), 69 deletions(-) diff --git a/backend/scripts/apply-bisaya-course-refresh.js b/backend/scripts/apply-bisaya-course-refresh.js index 6430aee..e5e94d1 100755 --- a/backend/scripts/apply-bisaya-course-refresh.js +++ b/backend/scripts/apply-bisaya-course-refresh.js @@ -104,7 +104,8 @@ const LESSON_DIDACTICS = { ], grammarFocus: [ { title: 'Bitte-Formeln mit palihug', text: '"Palihug" macht Bitten höflich und taucht in vielen Überlebenssätzen auf.', example: 'Palihug ka mubalik? / Tabangi ko, palihug.' }, - { title: 'Kurze Verständnisfragen', text: 'Sehr kurze Fragen helfen dir im Alltag oft mehr als lange Sätze.', example: 'Unsay pasabot ani? Asa ang CR?' } + { title: 'Kurze Verständnisfragen', text: 'Sehr kurze Fragen helfen dir im Alltag oft mehr als lange Sätze.', example: 'Unsay pasabot ani? Asa ang CR?' }, + { title: 'ko und ka: ich / du im Satz', text: 'ko steht in diesen kurzen Sätzen für „ich/mich“, ka für „du/dich“. Anders als im Deutschen stehen die Formen oft nach dem wichtigen Wort oder Verb.', example: 'Wala ko kasabot. / Palihug ka mubalik? / Tabangi ko, palihug.' } ], speakingPrompts: [ { title: 'Wenn du etwas nicht verstehst', prompt: 'Sage, dass du etwas nicht verstehst, und bitte um Wiederholung.', cue: 'Wala ko kasabot. Palihug ka mubalik?' }, @@ -245,6 +246,9 @@ const LESSON_DIDACTICS = { { target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.', alternatives: ['Ich ruhe mich später aus.'] }, { target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' } ], + grammarFocus: [ + { title: 'imong und koy im Alltag', text: 'imong bedeutet „dein/deine“ vor einem Nomen. koy ist die zusammengezogene Form in „Naa koy …“ und drückt aus, dass du etwas hast oder vorhast.', example: 'Unsa imong buhat karon? / Naa koy lakaw karong hapon.' } + ], speakingPrompts: [ { title: 'Tagesablauf abstimmen', prompt: 'Frage nach dem Plan und sage, was du heute erledigst.', cue: 'Unsa imong buhat karon? Naglimpyo ko sa balay.' } ], diff --git a/backend/scripts/bisaya-course-phase1.js b/backend/scripts/bisaya-course-phase1.js index 848bbe3..2094000 100755 --- a/backend/scripts/bisaya-course-phase1.js +++ b/backend/scripts/bisaya-course-phase1.js @@ -51,6 +51,13 @@ export const BISAYA_DIDACTICS_FRAGMENTS = { cue: 'Unsa imong buhat karon? Naglimpyo ko sa balay.' } ], + grammarFocus: [ + { + title: 'imong und koy im Alltag', + text: 'imong bedeutet „dein/deine“ vor einem Nomen. koy ist die zusammengezogene Form in „Naa koy …“ und drückt aus, dass du etwas hast oder vorhast.', + example: 'Unsa imong buhat karon? / Naa koy lakaw karong hapon.' + } + ], practicalTasks: [ { title: 'Alltagscheck', diff --git a/backend/scripts/create-bisaya-course-content.js b/backend/scripts/create-bisaya-course-content.js index cd6569a..f87b545 100755 --- a/backend/scripts/create-bisaya-course-content.js +++ b/backend/scripts/create-bisaya-course-content.js @@ -3582,6 +3582,18 @@ const BISAYA_EXERCISES = { }, explanation: '"Wala ko kasabot" = "Ich verstehe nicht", "Palihug ka mubalik?" = "Bitte wiederholen".' }, + { + exerciseTypeId: 1, + title: 'ko oder ka? – erste Personenformen', + instruction: 'Ergänze ko oder ka. Lies jeden Satz anschließend laut.', + questionData: { + type: 'gap_fill', + text: 'Wala {gap} kasabot. (Ich verstehe nicht.) | Palihug {gap} mubalik? (Kannst du bitte wiederholen?) | Tabangi {gap}, palihug. (Hilf mir bitte.)', + gaps: 3 + }, + answerData: { type: 'gap_fill', answers: ['ko', 'ka', 'ko'] }, + explanation: 'ko steht hier für „ich/mich“: Wala ko kasabot, Tabangi ko. ka richtet sich an die andere Person: Palihug ka mubalik?' + }, { exerciseTypeId: 4, title: '„Wo ist die Toilette?" übersetzen', @@ -3708,6 +3720,18 @@ const BISAYA_EXERCISES = { // Lektion 11: Alltagsgespräche - Teil 1 (~20–30 Min Übungsmaterial) 'Alltagsgespräche - Teil 1': [ + { + exerciseTypeId: 1, + title: 'imong oder koy? – Besitz und eigener Plan', + instruction: 'Ergänze die passende kurze Form.', + questionData: { + type: 'gap_fill', + text: 'Unsa {gap} buhat karon? (Was machst du heute?) | Naa {gap} lakaw karong hapon. (Ich habe heute Nachmittag etwas zu erledigen.)', + gaps: 2 + }, + answerData: { type: 'gap_fill', answers: ['imong', 'koy'] }, + explanation: 'imong heißt „dein/deine“ und steht vor buhat. koy ist die zusammengezogene Form in Naa koy …: „Ich habe / bei mir ist …“. ' + }, { exerciseTypeId: 2, // multiple_choice title: 'Wie sagt man "Wie war dein Tag?"?', diff --git a/backend/scripts/create-bisaya-course.js b/backend/scripts/create-bisaya-course.js index f47fd0e..23578d9 100755 --- a/backend/scripts/create-bisaya-course.js +++ b/backend/scripts/create-bisaya-course.js @@ -174,6 +174,11 @@ const LESSON_DIDACTICS = { title: 'Kurze Verständnisfragen', text: 'Sehr kurze Fragen helfen dir im Alltag oft mehr als lange Sätze.', example: 'Unsay pasabot ani? Asa ang CR?' + }, + { + title: 'ko und ka: ich / du im Satz', + text: 'ko steht in diesen kurzen Sätzen für „ich/mich“, ka für „du/dich“. Anders als im Deutschen stehen die Formen oft nach dem wichtigen Wort oder Verb.', + example: 'Wala ko kasabot. / Palihug ka mubalik? / Tabangi ko, palihug.' } ], speakingPrompts: [ @@ -401,6 +406,13 @@ const LESSON_DIDACTICS = { { target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.', alternatives: ['Ich ruhe mich später aus.'] }, { target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' } ], + grammarFocus: [ + { + title: 'imong und koy im Alltag', + text: 'imong bedeutet „dein/deine“ vor einem Nomen. koy ist die zusammengezogene Form in „Naa koy …“ und drückt aus, dass du etwas hast oder vorhast.', + example: 'Unsa imong buhat karon? / Naa koy lakaw karong hapon.' + } + ], speakingPrompts: [ { title: 'Tagesablauf abstimmen', diff --git a/backend/scripts/update-bisaya-didactics.js b/backend/scripts/update-bisaya-didactics.js index 377b545..9fb66e3 100755 --- a/backend/scripts/update-bisaya-didactics.js +++ b/backend/scripts/update-bisaya-didactics.js @@ -136,7 +136,8 @@ export const LESSON_DIDACTICS = { ], grammarFocus: [ { title: 'Bitte-Formeln mit palihug', text: '"Palihug" macht Bitten höflich und taucht in vielen Überlebenssätzen auf.', example: 'Palihug ka mubalik? / Tabangi ko, palihug.' }, - { title: 'Kurze Verständnisfragen', text: 'Sehr kurze Fragen helfen dir im Alltag oft mehr als lange Sätze.', example: 'Unsay pasabot ani? Asa ang CR?' } + { title: 'Kurze Verständnisfragen', text: 'Sehr kurze Fragen helfen dir im Alltag oft mehr als lange Sätze.', example: 'Unsay pasabot ani? Asa ang CR?' }, + { title: 'ko und ka: ich / du im Satz', text: 'ko steht in diesen kurzen Sätzen für „ich/mich“, ka für „du/dich“. Anders als im Deutschen stehen die Formen oft nach dem wichtigen Wort oder Verb.', example: 'Wala ko kasabot. / Palihug ka mubalik? / Tabangi ko, palihug.' } ], speakingPrompts: [ { title: 'Wenn du etwas nicht verstehst', prompt: 'Sage, dass du etwas nicht verstehst, und bitte um Wiederholung.', cue: 'Wala ko kasabot. Palihug ka mubalik?' }, diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index 9e8e7fd..630e07b 100755 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -2918,6 +2918,33 @@ export default class VocabService { }; } + _buildSyntheticLexemeGapExercisePlain(lessonId, row, exerciseNumber) { + const learning = String(row.learning || '').trim(); + const reference = String(row.reference || '').trim(); + if (!learning || !reference) { + return null; + } + // A short contextual tail makes multi-word entries a real gap exercise, + // while single words still have a useful translation cue. + const parts = reference.split(/\s+/).filter(Boolean); + const text = parts.length > 1 + ? `{gap} ${parts.slice(1).join(' ')} (${learning})` + : `{gap} (${learning})`; + return { + id: `syn-gap-${lessonId}-${row.id}-l2r`, + lessonId, + exerciseTypeId: 1, + exerciseType: { id: 1, name: 'gap_fill' }, + exerciseNumber, + title: `Fehlendes Wort: ${learning}`, + instruction: 'Ergänze das fehlende Wort.', + questionData: { type: 'gap_fill', text }, + answerData: { type: 'gap_fill', answers: [parts[0]] }, + explanation: null, + createdByUserId: 0 + }; + } + async _fetchChapterLexemeRowsForMc(chapterId) { const id = Number.parseInt(chapterId, 10); if (!Number.isFinite(id)) { @@ -2951,26 +2978,9 @@ export default class VocabService { if (plainLesson.lessonType === 'review' || plainLesson.lessonType === 'vocab_review' || plainLesson.lessonType === 'weekly_review') { return list; } - let rows = []; - - // If this lesson belongs to a week, prefer vocab from previous lessons of the same week - if (plainLesson.weekNumber) { - try { - const weekExercises = await this._getWeekVocabExercises(plainLesson.courseId, plainLesson.weekNumber, plainLesson.lessonNumber); - const extracted = this._extractTrainerVocabsFromExercises(weekExercises || [], { allowGapFill: false }); - if (extracted && extracted.length) { - // Map extracted pairs to row-like objects with numeric ids - rows = extracted.map((item, idx) => ({ id: 2000000 + idx, learning: item.learning, reference: item.reference })); - } - } catch (err) { - // ignore and fallback to chapter lexemes - rows = []; - } - } - - if (!rows.length) { - rows = await this._fetchChapterLexemeRowsForMc(plainLesson.chapterId); - } + // Synthetic IDs are checked against the chapter lexeme table. Do not use + // extracted, temporary weekly rows here: they have no stable ID to verify. + const rows = await this._fetchChapterLexemeRowsForMc(plainLesson.chapterId); if (!rows.length) { plainLesson.chapterLexemeExamCount = 0; @@ -3044,6 +3054,19 @@ export default class VocabService { } } + // Add active-recall questions as well as recognition questions. A smaller + // sample keeps a chapter test manageable while ensuring every test contains + // several genuine "which word is missing?" tasks. + const gapTarget = Math.min(8, Math.max(3, Math.ceil(examSelected.length * 0.5))); + const gapRows = this._seededShuffle(examSelected.slice(), (seed + 7919) >>> 0).slice(0, gapTarget); + for (const row of gapRows) { + maxNum += 1; + const ex = this._buildSyntheticLexemeGapExercisePlain(plainLesson.id, row, maxNum); + if (ex) { + list.push(ex); + } + } + return list; } @@ -3121,6 +3144,43 @@ export default class VocabService { }; } + async _checkSyntheticLexemeGapAnswer(user, lessonId, chapterLexemeId, userAnswer) { + const lesson = await VocabCourseLesson.findByPk(lessonId, { + include: [{ model: VocabCourse, as: 'course' }] + }); + if (!lesson || (lesson.course.ownerUserId !== user.id && !lesson.course.isPublic)) { + const err = new Error('Exercise not found'); + err.status = lesson ? 403 : 404; + throw err; + } + const enrollment = await VocabCourseEnrollment.findOne({ + where: { userId: user.id, courseId: lesson.courseId } + }); + if (!enrollment || !lesson.chapterId) { + const err = new Error('Exercise not found'); + err.status = enrollment ? 404 : 403; + throw err; + } + const rows = await this._fetchChapterLexemeRowsForMc(lesson.chapterId); + const row = rows.find((entry) => Number(entry.id) === Number(chapterLexemeId)); + const referenceParts = String(row?.reference || '').trim().split(/\s+/).filter(Boolean); + if (!referenceParts.length) { + const err = new Error('Exercise not found'); + err.status = 404; + throw err; + } + const correctAnswer = referenceParts[0]; + const submitted = Array.isArray(userAnswer) ? userAnswer[0] : userAnswer; + const correct = this._isEquivalentAnswer(submitted, correctAnswer); + return { + correct, + correctAnswer, + alternatives: [], + explanation: null, + progress: { attempts: 1, correctAttempts: correct ? 1 : 0, lastAttemptAt: new Date(), completed: correct, completedAt: correct ? new Date() : null } + }; + } + async getLesson(hashedUserId, lessonId) { const user = await this._getUserByHashedId(hashedUserId); const lesson = await VocabCourseLesson.findByPk(lessonId, { @@ -4483,6 +4543,15 @@ export default class VocabService { userAnswer ); } + const synGapMatch = /^syn-gap-(\d+)-(\d+)-l2r$/.exec(exIdStr); + if (synGapMatch) { + return this._checkSyntheticLexemeGapAnswer( + user, + Number(synGapMatch[1]), + Number(synGapMatch[2]), + userAnswer + ); + } const exercise = await VocabGrammarExercise.findByPk(exerciseId, { include: [ { model: VocabCourseLesson, as: 'lesson', include: [{ model: VocabCourse, as: 'course' }] } diff --git a/frontend/src/views/social/VocabLessonView.vue b/frontend/src/views/social/VocabLessonView.vue index 72cace1..4f34f23 100755 --- a/frontend/src/views/social/VocabLessonView.vue +++ b/frontend/src/views/social/VocabLessonView.vue @@ -305,9 +305,10 @@
-
-
{{ vocabTrainerDirection === 'L2R' ? $t('socialnetwork.vocab.courses.translateTo') : $t('socialnetwork.vocab.courses.translateFrom') }}
+
+
{{ currentVocabQuestion.directionLabel || (vocabTrainerDirection === 'L2R' ? $t('socialnetwork.vocab.courses.translateTo') : $t('socialnetwork.vocab.courses.translateFrom')) }}
{{ currentVocabQuestion.prompt }}
+

{{ currentVocabQuestion.grammarHint }}

{{ $t('socialnetwork.vocab.courses.correct') }}!
@@ -1464,14 +1465,11 @@ export default { exerciseTargetScore() { return Number(this.lesson?.targetScorePercent) || 80; }, - /** Kapitel-Prüfung: eine Frage pro Ansicht (Essen & Trinken: deterministisch gemischt). */ + /** Kapitel-Prüfung: eine Frage pro Ansicht, immer unabhängig von der Eingabereihenfolge gemischt. */ scrambledChapterExamExercises() { const raw = this.effectiveExercises; if (!raw.length) return []; - if ((this.lesson?.title || '').trim() === 'Essen & Trinken') { - return this._deterministicShuffle(raw.slice(), Number(this.lessonId) || 1); - } - return raw; + return this._deterministicShuffle(raw.slice(), Number(this.lessonId) || 1); }, sequentialPanelActive() { return (this.scrambledChapterExamExercises?.length || 0) > 1; @@ -2908,46 +2906,6 @@ export default { } await this.$nextTick(); let exercises = this.effectiveExercises; - // Wenn das Backend eine chapterLexemeTraining-Liste liefert, wandle sie - // in temporäre gap_fill-Übungen um und mische sie in die grammarExercises - try { - if (this.lesson && Array.isArray(this.lesson.chapterLexemeTraining) && this.lesson.chapterLexemeTraining.length) { - const temps = this.lesson.chapterLexemeTraining.map((item, idx) => { - const id = `cht-${this.lesson.id}-${String(item.id)}`; - // Wenn reference mehrere Wörter enthält, ersetze das erste Wort durch die Lücke - const ref = String(item.reference || '').trim(); - let qText = `{gap} (${item.learning})`; - let answers = [ref]; - if (ref) { - const parts = ref.split(/\s+/).filter(Boolean); - if (parts.length > 1) { - const tail = parts.slice(1).join(' '); - qText = `{gap} ${tail}`; - answers = [parts[0]]; - } else { - // single token reference: show blank plus learning hint - qText = `{gap} (${item.learning})`; - answers = [parts[0]]; - } - } - return { - id, - lessonId: this.lesson.id, - exerciseTypeId: 1, - title: `Kapitel-Vokabel: ${item.learning}`, - instruction: this.$t('socialnetwork.vocab.courses.fillTheBlank') || 'Lückentext', - questionData: { type: 'gap_fill', text: qText }, - answerData: { type: 'gap_fill', answers } - }; - }); - this.lesson.grammarExercises = Array.isArray(this.lesson.grammarExercises) - ? [...this.lesson.grammarExercises, ...temps] - : temps; - exercises = this.effectiveExercises; - } - } catch (err) { - console.warn('[VocabLessonView] Fehler beim Einfügen von chapterLexemeTraining:', err); - } if (!exercises || exercises.length === 0) { debugLog('[VocabLessonView] Lade Übungen separat...'); await this.loadGrammarExercises(); @@ -4187,6 +4145,129 @@ export default { } return arr; }, + isGermanCourse() { + return /^(deutsch|german)$/i.test(String(this.courseLanguageName || '').trim()); + }, + germanFormSource(vocab) { + if (!this.isGermanCourse()) return ''; + return [vocab?.learning, vocab?.reference] + .map((value) => String(value || '').trim()) + .find((value) => /^(?:der|die|das)\s+[^\s]+$/i.test(value) || /^[a-zäöüß]+en$/i.test(value)) || ''; + }, + germanPresentForms(infinitive) { + const verb = String(infinitive || '').trim().toLowerCase(); + const irregular = { + gehen: ['gehe', 'gehst', 'geht', 'gehen', 'geht', 'gehen'], + sein: ['bin', 'bist', 'ist', 'sind', 'seid', 'sind'], + haben: ['habe', 'hast', 'hat', 'haben', 'habt', 'haben'], + werden: ['werde', 'wirst', 'wird', 'werden', 'werdet', 'werden'], + sprechen: ['spreche', 'sprichst', 'spricht', 'sprechen', 'sprecht', 'sprechen'], + fahren: ['fahre', 'fährst', 'fährt', 'fahren', 'fahrt', 'fahren'], + lesen: ['lese', 'liest', 'liest', 'lesen', 'lest', 'lesen'], + sehen: ['sehe', 'siehst', 'sieht', 'sehen', 'seht', 'sehen'] + }; + if (irregular[verb]) return irregular[verb]; + // Regular verbs only: this intentionally avoids teaching incorrect + // stem changes for verbs whose form is not known here. + if (!/^[a-zäöüß]+en$/i.test(verb) || /(?:eln|ern)$/i.test(verb)) return null; + const stem = verb.slice(0, -2); + if (!stem) return null; + const needsExtraE = /(?:d|t|chn|ffn|gn|tm)$/i.test(stem); + const du = /(?:s|ß|x|z)$/i.test(stem) ? `${stem}t` : `${stem}${needsExtraE ? 'est' : 'st'}`; + const er = `${stem}${needsExtraE ? 'et' : 't'}`; + return [`${stem}e`, du, er, `${stem}en`, `${stem}${needsExtraE ? 'et' : 't'}`, `${stem}en`]; + }, + buildGrammarPracticeQuestion(vocab, source) { + const german = this.germanFormSource(vocab); + if (!german || Math.random() >= 0.35) return null; + const articleMatch = german.match(/^(der|die|das)\s+([^\s]+)$/i); + if (articleMatch) { + const article = articleMatch[1].toLowerCase(); + const noun = articleMatch[2]; + return { + vocab, + prompt: `_____ ${noun}`, + answers: [article], + answer: article, + key: this.getVocabKey(vocab), + source, + kind: 'article', + directionLabel: 'Welcher Artikel passt?', + choiceOptions: ['der', 'die', 'das'] + }; + } + const forms = this.germanPresentForms(german); + if (!forms) return null; + const pronouns = ['ich', 'du', 'er/sie/es', 'wir', 'ihr', 'sie/Sie']; + const index = Math.floor(Math.random() * pronouns.length); + const answer = forms[index]; + return { + vocab, + prompt: `${pronouns[index]} _____ (${german})`, + answers: [answer], + answer, + key: this.getVocabKey(vocab), + source, + kind: 'conjugation', + directionLabel: 'Setze die richtige Verbform ein.', + choiceOptions: [...new Set(forms)].sort(() => Math.random() - 0.5) + }; + }, + isBisayaCourse() { + return /(?:bisaya|cebuano)/i.test(String(this.courseLanguageName || '').trim()); + }, + bisayaCliticRule(token) { + const rules = { + ko: 'ko: „ich/mir“ als kurze Personenform, z. B. „Maayo ko.“', + ka: 'ka: „du/dir“ als kurze Personenform, z. B. „Kumusta ka?“', + imong: 'imong: „dein/deine“ direkt vor einem Nomen, z. B. „imong bag“.', + koy: 'koy: zusammengezogene Form von ko + ay; häufig nach naa/aduna: „Naa koy …“', + nako: 'nako: „von mir/für mich“ in Mustern wie „Kinahanglan nako …“', + nimo: 'nimo: „von dir/für dich“ bzw. „du“ in Mustern wie „Pwede nimo ko tabangan?“', + tika: 'tika: „dich“ als angehängte Objektform, z. B. „Tabangan tika.“' + }; + return rules[token] || ''; + }, + lessonPatternTexts() { + const patterns = Array.isArray(this.lessonDidactics?.corePatterns) + ? this.lessonDidactics.corePatterns + : []; + const grammarExamples = Array.isArray(this.lessonDidactics?.grammarFocus) + ? this.lessonDidactics.grammarFocus.map((entry) => entry?.example) + : []; + return [...patterns, ...grammarExamples] + .map((entry) => typeof entry === 'object' ? entry?.target : entry) + .map((entry) => String(entry || '').trim()) + .filter(Boolean); + }, + buildBisayaLessonPatternQuestion(vocab, source) { + if (!this.isBisayaCourse() || Math.random() >= 0.35) return null; + const tokens = ['imong', 'nimo', 'nako', 'tika', 'koy', 'ko', 'ka']; + const candidates = []; + this.lessonPatternTexts().forEach((text) => { + tokens.forEach((token) => { + const matcher = new RegExp(`(^|\\s)${token}(?=\\s|[,.?!]|$)`, 'i'); + if (!matcher.test(text)) return; + candidates.push({ text, token }); + }); + }); + if (!candidates.length) return null; + const picked = candidates[Math.floor(Math.random() * candidates.length)]; + const matcher = new RegExp(`(^|\\s)${picked.token}(?=\\s|[,.?!]|$)`, 'i'); + const prompt = picked.text.replace(matcher, (_match, prefix) => `${prefix}_____`); + return { + vocab, + prompt, + answers: [picked.token], + answer: picked.token, + key: this.getVocabKey(vocab), + source, + kind: 'bisaya_clitic', + directionLabel: 'Ergänze die passende Bisaya-Form.', + grammarHint: this.bisayaCliticRule(picked.token), + choiceOptions: this._shuffleArray(['ko', 'ka', 'imong', 'koy', 'nako', 'nimo', 'tika']) + }; + }, nextVocabQuestion() { debugLog('[VocabLessonView] nextVocabQuestion aufgerufen'); this.clearVocabTrainerContinueTimer(); @@ -4318,6 +4399,25 @@ export default { this.currentVocabQuestion = null; return; } + // Grammar belongs in the trainer itself: German learners regularly + // retrieve articles and verb forms alongside translations. + const grammarQuestion = !dueRepeatVocab + ? (this.buildBisayaLessonPatternQuestion(vocab, questionSource) + || this.buildGrammarPracticeQuestion(vocab, questionSource)) + : null; + if (grammarQuestion) { + this.currentVocabQuestion = grammarQuestion; + this.vocabTrainerAnswer = ''; + this.vocabTrainerSelectedChoice = null; + this.vocabTrainerAnswered = false; + if (this.vocabTrainerMode === 'multiple_choice') { + this.vocabTrainerChoiceOptions = grammarQuestion.choiceOptions; + } + if (this.vocabTrainerMode === 'typing') { + this.$nextTick(() => this.$refs.vocabInput?.focus?.()); + } + return; + } this.vocabTrainerDirection = Math.random() < 0.5 ? 'L2R' : 'R2L'; const allTrainerVocabs = [...this.trainableLessonVocab, ...this.vocabTrainerMixedPool]; const direction = this.vocabTrainerDirection; @@ -6082,6 +6182,14 @@ export default { color: #333; } +.vocab-grammar-hint { + margin: 10px 0 0; + padding-top: 9px; + border-top: 1px solid #ddd; + color: #4b5563; + font-size: 0.92em; +} + .vocab-answer-area { margin-bottom: 15px; }