feat(bisaya): enhance grammar focus and exercises in Bisaya course content
All checks were successful
Deploy to production / deploy (push) Successful in 4m10s
All checks were successful
Deploy to production / deploy (push) Successful in 4m10s
This commit is contained in:
@@ -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.' }
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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?"?',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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?' },
|
||||
|
||||
@@ -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' }] }
|
||||
|
||||
@@ -305,9 +305,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentVocabQuestion" class="vocab-question">
|
||||
<div class="vocab-prompt">
|
||||
<div class="vocab-direction">{{ vocabTrainerDirection === 'L2R' ? $t('socialnetwork.vocab.courses.translateTo') : $t('socialnetwork.vocab.courses.translateFrom') }}</div>
|
||||
<div class="vocab-prompt">
|
||||
<div class="vocab-direction">{{ currentVocabQuestion.directionLabel || (vocabTrainerDirection === 'L2R' ? $t('socialnetwork.vocab.courses.translateTo') : $t('socialnetwork.vocab.courses.translateFrom')) }}</div>
|
||||
<div class="vocab-word">{{ currentVocabQuestion.prompt }}</div>
|
||||
<p v-if="currentVocabQuestion.grammarHint" class="vocab-grammar-hint">{{ currentVocabQuestion.grammarHint }}</p>
|
||||
</div>
|
||||
<div v-if="vocabTrainerAnswered" class="vocab-feedback" :class="{ correct: vocabTrainerLastCorrect, wrong: !vocabTrainerLastCorrect }">
|
||||
<div v-if="vocabTrainerLastCorrect">{{ $t('socialnetwork.vocab.courses.correct') }}!</div>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user