Files
yourpart3/backend/scripts/create-german-for-bisaya-course-content.js
Torsten Schulz (local) c6caeefb5f
All checks were successful
Deploy to production / deploy (push) Successful in 2m47s
feat(bisaya-course): enhance German course content and localization support
- Updated the create-german-for-bisaya-course-content.js script to improve lesson pattern retrieval by introducing a new function for generating a lesson pattern pool.
- Added new exercises for various topics including 'Wohnung & Nachbarn', 'Besuch empfangen', 'Arzt, Apotheke, Termin', and 'Amt, Dokumente, Anmeldung', enhancing practical language skills for learners.
- Improved localization by integrating translation keys for various UI elements and error messages across multiple components, ensuring a consistent user experience in both German and Bisaya.
- Enhanced the main.js file to recognize Bisaya language preferences in browser settings, improving accessibility for users.
2026-03-31 17:40:03 +02:00

1330 lines
45 KiB
JavaScript

#!/usr/bin/env node
/**
* Script zum Erstellen von Übungen für Deutschkurse aus Sicht von Bisaya-Lernenden.
*
* Verwendung:
* node backend/scripts/create-german-for-bisaya-course-content.js
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import User from '../models/community/user.js';
import { GERMAN_FOR_BISAYA_PHASE1_DIDACTICS } from './german-for-bisaya-phase1.js';
import { GERMAN_FOR_BISAYA_PHASE3_DIDACTICS } from './german-for-bisaya-phase3-extension.js';
import { GERMAN_FOR_BISAYA_PHASE4_DIDACTICS } from './german-for-bisaya-phase4-extension.js';
import { GERMAN_FOR_BISAYA_PHASE5_DIDACTICS } from './german-for-bisaya-phase5-extension.js';
function withTypeName(exerciseTypeName, exercise) {
return {
...exercise,
exerciseTypeName
};
}
const GERMAN_DIDACTICS = {
...GERMAN_FOR_BISAYA_PHASE1_DIDACTICS,
...GERMAN_FOR_BISAYA_PHASE3_DIDACTICS,
...GERMAN_FOR_BISAYA_PHASE4_DIDACTICS,
...GERMAN_FOR_BISAYA_PHASE5_DIDACTICS
};
function normalizeText(value) {
return String(value || '')
.trim()
.replace(/\s+/g, ' ');
}
function simpleHash(value) {
return Array.from(String(value || '')).reduce((sum, char) => sum + char.charCodeAt(0), 0);
}
function rotateArray(values, offset) {
if (!Array.isArray(values) || values.length === 0) return [];
const normalizedOffset = ((offset % values.length) + values.length) % values.length;
return values.slice(normalizedOffset).concat(values.slice(0, normalizedOffset));
}
function getLessonDidactics(lesson) {
const staticDidactics = GERMAN_DIDACTICS[lesson.title] || {};
return {
learningGoals: Array.isArray(lesson.learningGoals) ? lesson.learningGoals : (staticDidactics.learningGoals || []),
corePatterns: (Array.isArray(lesson.corePatterns) ? lesson.corePatterns : (staticDidactics.corePatterns || []))
.map((entry) => normalizeText(entry))
.filter(Boolean),
grammarFocus: Array.isArray(lesson.grammarFocus) ? lesson.grammarFocus : (staticDidactics.grammarFocus || []),
speakingPrompts: Array.isArray(lesson.speakingPrompts) ? lesson.speakingPrompts : (staticDidactics.speakingPrompts || []),
practicalTasks: Array.isArray(lesson.practicalTasks) ? lesson.practicalTasks : (staticDidactics.practicalTasks || [])
};
}
function getScenarioPrompt(lesson, didactics) {
const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null;
const practicalTask = Array.isArray(didactics.practicalTasks) ? didactics.practicalTasks[0] : null;
if (speakingPrompt?.prompt) return speakingPrompt.prompt;
if (practicalTask?.text) return practicalTask.text;
if (lesson.description) return lesson.description;
return `Reagiere passend in einer Alltagssituation aus der Lektion "${lesson.title}".`;
}
function getChoiceQuestion(lesson, didactics) {
const scenarioPrompt = getScenarioPrompt(lesson, didactics);
switch (lesson.lessonType) {
case 'conversation':
return `${scenarioPrompt} Welche deutsche Formulierung passt am besten?`;
case 'grammar':
return `Welche deutsche Struktur passt am besten zum Schwerpunkt "${lesson.title}"?`;
case 'review':
return `Welche Formulierung solltest du aus "${lesson.title}" sicher wiedererkennen?`;
case 'culture':
return `Welche Formulierung passt besonders gut zum kulturellen Schwerpunkt "${lesson.title}"?`;
case 'vocab':
default:
return `Welche Formulierung gehört thematisch zu "${lesson.title}"?`;
}
}
function pickDistractors(pattern, allPatterns, count) {
return allPatterns
.filter((entry) => entry !== pattern)
.slice(0, count);
}
function getLessonPatternPool(didactics) {
const speakingCues = (Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts : [])
.flatMap((entry) => [entry?.cue, entry?.prompt])
.map((entry) => normalizeText(entry))
.filter(Boolean);
return Array.from(new Set([
...didactics.corePatterns,
...speakingCues
]));
}
function buildChoiceExercise(lesson, didactics, pattern, allPatterns, variant = 0) {
const distractors = pickDistractors(pattern, allPatterns, 3);
if (distractors.length < 3) return null;
const options = rotateArray([pattern, ...distractors], simpleHash(`${lesson.title}:${pattern}:${variant}`) % 4);
return {
exerciseTypeId: 2,
title: `${lesson.title}: Passende Formulierung wählen`,
instruction: 'Wähle die natürlichste deutsche Formulierung für die Situation oder den Schwerpunkt der Lektion.',
questionData: {
type: 'multiple_choice',
question: getChoiceQuestion(lesson, didactics),
options
},
answerData: {
type: 'multiple_choice',
correctAnswer: options.indexOf(pattern)
},
explanation: `"${pattern}" ist ein zentrales deutsches Muster dieser Lektion.`
};
}
function pickGapTarget(pattern) {
const tokens = normalizeText(pattern)
.split(' ')
.map((token) => token.trim())
.filter(Boolean);
const candidates = tokens
.map((token, index) => ({ token, index, score: token.replace(/[.,?!]/g, '').length }))
.filter(({ token }) => token.replace(/[.,?!]/g, '').length >= 3);
if (candidates.length === 0) return null;
candidates.sort((left, right) => right.score - left.score);
return candidates[0];
}
function buildGapExercise(lessonTitle, pattern) {
const gapTarget = pickGapTarget(pattern);
if (!gapTarget) return null;
const tokens = normalizeText(pattern).split(' ');
tokens[gapTarget.index] = '{gap}';
return {
exerciseTypeId: 1,
title: `${lessonTitle}: Muster vervollständigen`,
instruction: 'Fülle die Lücke mit dem passenden deutschen Ausdruck.',
questionData: {
type: 'gap_fill',
text: tokens.join(' '),
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: [gapTarget.token.replace(/[.,?!]/g, '')]
},
explanation: `Das vollständige deutsche Kernmuster lautet: "${pattern}".`
};
}
function buildSentenceExercise(lessonTitle, pattern) {
const tokens = normalizeText(pattern)
.replace(/[?!]/g, '')
.split(' ')
.filter(Boolean);
if (tokens.length < 2) return null;
return {
exerciseTypeId: 3,
title: `${lessonTitle}: Satz bauen`,
instruction: 'Ordne die Wörter zu einem korrekten deutschen Satz.',
questionData: {
type: 'sentence_building',
question: `Baue das Kernmuster aus der Lektion "${lessonTitle}".`,
tokens
},
answerData: {
correct: [normalizeText(pattern)]
},
explanation: `Dieses Kernmuster gehört zur Lektion "${lessonTitle}".`
};
}
function buildSpeakingExercise(lessonTitle, didactics, fallbackPattern) {
const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null;
const expectedText = normalizeText(speakingPrompt?.cue || fallbackPattern);
if (!expectedText) return null;
const keywords = expectedText
.toLowerCase()
.replace(/[.,?!]/g, '')
.split(' ')
.filter((token) => token.length >= 3)
.slice(0, 5);
return {
exerciseTypeId: 8,
title: `${lessonTitle}: Frei sprechen`,
instruction: 'Sprich das zentrale deutsche Muster frei nach.',
questionData: {
type: 'speaking_from_memory',
question: speakingPrompt?.prompt || `Sprich ein zentrales Muster aus der Lektion "${lessonTitle}".`,
expectedText,
keywords
},
answerData: {
type: 'speaking_from_memory'
},
explanation: 'Wichtig sind ein flüssiger Abruf und die zentralen deutschen Schlüsselwörter.'
};
}
function buildSituationalExercise(lessonTitle, didactics, fallbackPattern) {
const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null;
const modelAnswer = normalizeText(speakingPrompt?.cue || fallbackPattern);
if (!modelAnswer) return null;
const keywords = modelAnswer
.toLowerCase()
.replace(/[.,?!]/g, '')
.split(' ')
.filter((token) => token.length >= 3)
.slice(0, 5);
return withTypeName('situational_response', {
title: `${lessonTitle}: Situativ reagieren`,
instruction: 'Antworte kurz und passend auf Deutsch.',
questionData: {
type: 'situational_response',
question: speakingPrompt?.prompt || `Reagiere passend mit einem Ausdruck aus der Lektion "${lessonTitle}".`,
keywords
},
answerData: {
modelAnswer,
keywords
},
explanation: `Das Kernmuster "${modelAnswer}" passt natürlich zu dieser Situation.`
});
}
function buildCultureExercise(lesson, pattern, allPatterns) {
const distractors = pickDistractors(pattern, allPatterns, 3);
if (distractors.length < 3) return null;
const options = rotateArray([pattern, ...distractors], simpleHash(`${lesson.title}:culture`) % 4);
const culturalNote = normalizeText(lesson.culturalNotes || '');
return {
exerciseTypeId: 2,
title: `${lesson.title}: Kulturell einordnen`,
instruction: 'Ordne den Ausdruck dem kulturellen Schwerpunkt der Lektion zu.',
questionData: {
type: 'multiple_choice',
question: culturalNote
? `${culturalNote} Welche Formulierung passt dazu besonders gut?`
: `Welche Formulierung passt besonders gut zum Schwerpunkt "${lesson.title}"?`,
options
},
answerData: {
type: 'multiple_choice',
correctAnswer: options.indexOf(pattern)
},
explanation: `"${pattern}" ist eng mit dem kulturellen Schwerpunkt dieser Lektion verbunden.`
};
}
function generateExercisesFromDidactics(lesson) {
const didactics = getLessonDidactics(lesson);
const corePatterns = didactics.corePatterns;
if (corePatterns.length === 0) return [];
const patternA = corePatterns[0];
const patternB = corePatterns[1] || corePatterns[0];
const pool = getLessonPatternPool(didactics);
if (lesson.lessonType === 'conversation') {
return [
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
buildGapExercise(lesson.title, patternA),
buildSentenceExercise(lesson.title, patternB),
buildSituationalExercise(lesson.title, didactics, patternA),
buildSpeakingExercise(lesson.title, didactics, patternB)
].filter(Boolean);
}
if (lesson.lessonType === 'grammar') {
return [
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
buildChoiceExercise(lesson, didactics, patternB, pool, 1),
buildGapExercise(lesson.title, patternA),
buildSentenceExercise(lesson.title, patternB),
buildSpeakingExercise(lesson.title, didactics, patternA)
].filter(Boolean);
}
if (lesson.lessonType === 'review' || lesson.didacticMode === 'intensive_review') {
return [
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
buildChoiceExercise(lesson, didactics, patternB, pool, 1),
buildGapExercise(lesson.title, patternA),
buildSentenceExercise(lesson.title, patternB),
buildSituationalExercise(lesson.title, didactics, patternA),
buildSpeakingExercise(lesson.title, didactics, patternB)
].filter(Boolean);
}
if (lesson.lessonType === 'culture') {
return [
buildCultureExercise(lesson, patternA, pool),
buildGapExercise(lesson.title, patternA),
buildSpeakingExercise(lesson.title, didactics, patternB)
].filter(Boolean);
}
return [
buildChoiceExercise(lesson, didactics, patternA, pool, 0),
buildChoiceExercise(lesson, didactics, patternB, pool, 1),
buildGapExercise(lesson.title, patternA),
buildSentenceExercise(lesson.title, patternB)
].filter(Boolean);
}
const GERMAN_EXERCISES = {
'Begrüßung & Vorstellung': [
{
exerciseTypeId: 2,
title: 'Vorstellung erkennen',
instruction: 'Wähle die passendste deutsche Vorstellung.',
questionData: {
type: 'multiple_choice',
question: 'Du triffst jemanden zum ersten Mal. Welche Formulierung passt?',
options: ['Hallo, ich heiße Maria.', 'Ich habe Maria.', 'Wo Maria?', 'Nicht Maria.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Hallo, ich heiße Maria." ist eine natürliche deutsche Selbstvorstellung.'
},
{
exerciseTypeId: 1,
title: 'Herkunft ergänzen',
instruction: 'Fülle die Lücke mit dem passenden Ausdruck.',
questionData: {
type: 'gap_fill',
text: 'Ich komme {gap} Cebu.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['aus']
},
explanation: 'Im Deutschen sagt man "Ich komme aus Cebu."'
},
withTypeName('situational_response', {
title: 'Kurz reagieren bei der Begrüßung',
instruction: 'Antworte kurz und passend auf Deutsch.',
questionData: {
type: 'situational_response',
question: 'Jemand sagt: "Hallo, ich heiße Anna." Wie reagierst du mit deiner eigenen Vorstellung?',
keywords: ['hallo', 'heiße', 'ich']
},
answerData: {
modelAnswer: 'Hallo, ich heiße Maria.',
keywords: ['hallo', 'heiße', 'ich']
},
explanation: 'Die sicherste frühe Reaktion ist eine eigene kurze Vorstellung mit "ich heiße".'
})
],
'der / die / das - Einstieg': [
{
exerciseTypeId: 2,
title: 'Artikel als Chunk erkennen',
instruction: 'Wähle die Form mit dem richtigen Artikel.',
questionData: {
type: 'multiple_choice',
question: 'Welche Form ist als Lern-Chunk richtig?',
options: ['der Tisch', 'die Tisch', 'das Tasche', 'der Kind']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Nomen sollen möglichst zusammen mit dem Artikel gelernt werden.'
},
{
exerciseTypeId: 1,
title: 'Artikel ergänzen',
instruction: 'Fülle den passenden Artikel ein.',
questionData: {
type: 'gap_fill',
text: '{gap} Tasche',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['die']
},
explanation: 'Es heißt "die Tasche".'
},
{
exerciseTypeId: 3,
title: 'Chunk richtig bauen',
instruction: 'Ordne die Wörter zu einem korrekten Lern-Chunk.',
questionData: {
type: 'sentence_building',
question: 'Baue den richtigen Chunk für "child".',
tokens: ['das', 'Kind']
},
answerData: {
correct: ['das Kind']
},
explanation: 'Auch sehr kurze Strukturen sollen als vollständiger Chunk gelernt werden.'
}
],
'nicht / kein - Einstieg': [
{
exerciseTypeId: 2,
title: 'Negation unterscheiden',
instruction: 'Wähle die passende deutsche Negation.',
questionData: {
type: 'multiple_choice',
question: 'Welche Formulierung ist richtig?',
options: ['Ich habe kein Geld.', 'Ich habe nicht Geld.', 'Ich bin kein müde.', 'Ich kein habe Geld.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Kein" steht hier vor dem Nomen, deshalb ist "Ich habe kein Geld." richtig.'
},
{
exerciseTypeId: 2,
title: 'Adjektiv negieren',
instruction: 'Wähle die richtige Form für eine Aussage mit Adjektiv.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagst du korrekt: "Ich bin nicht müde"?',
options: ['Ich bin nicht müde.', 'Ich bin kein müde.', 'Ich nicht bin müde.', 'Ich bin müde kein.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Adjektive und Aussagen werden im Deutschen oft mit "nicht" negiert.'
},
{
exerciseTypeId: 1,
title: 'Kein ergänzen',
instruction: 'Ergänze die passende Negation.',
questionData: {
type: 'gap_fill',
text: 'Ich habe {gap} Zeit.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['keine']
},
explanation: 'Bei "Zeit" braucht man hier "keine".'
}
],
'wo / wohin - Kontrast': [
{
exerciseTypeId: 2,
title: 'Ort oder Richtung?',
instruction: 'Wähle die Frage nach einem Ziel.',
questionData: {
type: 'multiple_choice',
question: 'Welche Frage passt, wenn du nach dem Ziel fragst?',
options: ['Wohin gehst du?', 'Wo gehst du?', 'Wie gehst du?', 'Wann gehst du?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Wohin" fragt nach einer Richtung oder einem Ziel.'
},
{
exerciseTypeId: 1,
title: 'Fragewort ergänzen',
instruction: 'Setze das passende Fragewort ein.',
questionData: {
type: 'gap_fill',
text: '{gap} bist du? Ich bin zu Hause.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['Wo']
},
explanation: 'Bei einem Ort fragt man im Deutschen mit "Wo".'
}
],
'du / Sie - Einstieg': [
{
exerciseTypeId: 2,
title: 'Höfliche Anrede erkennen',
instruction: 'Wähle die höfliche deutsche Form.',
questionData: {
type: 'multiple_choice',
question: 'Du sprichst eine fremde erwachsene Person höflich an. Welche Frage passt?',
options: ['Wie heißen Sie?', 'Wie heißt du?', 'Wo bist du?', 'Wie bist du?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Sie" markiert im Deutschen die höfliche Anrede.'
},
{
exerciseTypeId: 1,
title: 'Anredeform ergänzen',
instruction: 'Fülle die passende Anredeform ein.',
questionData: {
type: 'gap_fill',
text: 'Können {gap} mir helfen?',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['Sie']
},
explanation: 'In einer höflichen Bitte heißt es "Können Sie mir helfen?".'
}
],
'Perfekt - Einstieg': [
{
exerciseTypeId: 2,
title: 'Perfektform erkennen',
instruction: 'Wähle die passende deutsche Vergangenheitsform.',
questionData: {
type: 'multiple_choice',
question: 'Welche Form ist ein korrektes frühes Perfekt?',
options: ['Ich habe gearbeitet.', 'Ich habe arbeiten.', 'Ich bin gearbeitet.', 'Ich arbeitete habe.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Viele frühe Alltagsverben bilden das Perfekt mit "haben" + Partizip II.'
},
{
exerciseTypeId: 1,
title: 'Hilfsverb ergänzen',
instruction: 'Ergänze das passende Hilfsverb.',
questionData: {
type: 'gap_fill',
text: 'Ich {gap} gestern gearbeitet.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['habe']
},
explanation: 'Im Perfekt braucht man hier das Hilfsverb "habe".'
}
],
'Wohnung & Nachbarn': [
{
exerciseTypeId: 2,
title: 'Wohnen passend ausdrücken',
instruction: 'Wähle die natürlichste Aussage zur Wohnsituation.',
questionData: {
type: 'multiple_choice',
question: 'Du willst sagen, dass du hier wohnst und den Nachbarn kennst. Welche Form passt?',
options: ['Ich wohne hier. Das ist mein Nachbar.', 'Ich bin hier wohnen. Das mein Nachbar.', 'Ich wohne dort Nachbar.', 'Hier wohne ich mein Nachbar.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Im Deutschen sind kurze klare Hauptsätze am Anfang am sichersten.'
},
{
exerciseTypeId: 1,
title: 'Wohnort ergänzen',
instruction: 'Fülle die Lücke im Satz.',
questionData: {
type: 'gap_fill',
text: 'Ich {gap} hier.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['wohne']
},
explanation: 'Das Kernmuster lautet "Ich wohne hier."'
},
withTypeName('situational_response', {
title: 'Nachbarn kurz vorstellen',
instruction: 'Antworte mit zwei kurzen Sätzen auf Deutsch.',
questionData: {
type: 'situational_response',
question: 'Jemand fragt dich: "Wo wohnst du? Und wer ist das?"',
keywords: ['wohne', 'hier', 'nachbar']
},
answerData: {
modelAnswer: 'Ich wohne hier. Das ist mein Nachbar.',
keywords: ['wohne', 'hier', 'nachbar']
},
explanation: 'Beide Informationen lassen sich mit zwei sehr einfachen Sätzen sauber ausdrücken.'
})
],
'Besuch empfangen': [
{
exerciseTypeId: 2,
title: 'Gast freundlich begrüßen',
instruction: 'Wähle die passende deutsche Reaktion.',
questionData: {
type: 'multiple_choice',
question: 'Ein Gast steht an der Tür. Welche Formulierung passt am besten?',
options: ['Komm doch rein. Setz dich bitte.', 'Du kommst rein sitzen bitte.', 'Ich rein bitte du.', 'Setz doch komm rein.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Komm doch rein. Setz dich bitte." klingt freundlich und natürlich.'
},
{
exerciseTypeId: 3,
title: 'Einladung richtig bauen',
instruction: 'Ordne die Wörter zu einer höflichen Einladung.',
questionData: {
type: 'sentence_building',
question: 'Baue eine freundliche Aufforderung für einen Gast.',
tokens: ['Setz', 'dich', 'bitte']
},
answerData: {
correct: ['Setz dich bitte']
},
explanation: 'Kurze Aufforderungen sind im Alltag sehr häufig.'
},
withTypeName('speaking_from_memory', {
title: 'Begrüßung frei sprechen',
instruction: 'Sprich eine kurze Begrüßung für einen Gast frei nach.',
questionData: {
type: 'speaking_from_memory',
question: 'Ein Freund besucht dich. Was sagst du an der Tür?',
expectedText: 'Komm doch rein. Setz dich bitte.',
keywords: ['komm', 'rein', 'setz', 'bitte']
},
answerData: {
type: 'speaking_from_memory'
},
explanation: 'Die Übung trainiert einen natürlichen Mini-Dialog beim Empfang von Besuch.'
})
],
'Arzt, Apotheke, Termin': [
{
exerciseTypeId: 2,
title: 'Arzttermin richtig sagen',
instruction: 'Wähle die passende Aussage.',
questionData: {
type: 'multiple_choice',
question: 'Du bist beim Arzt und brauchst Medikamente. Welche Form passt?',
options: ['Ich habe einen Termin. Ich brauche Medikamente.', 'Ich bin ein Termin. Ich brauche Medizin machen.', 'Ich habe Medikamente Termin.', 'Termin ich brauche.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Beide Aussagen sind typische frühe Standardsätze für Arzt und Apotheke.'
},
{
exerciseTypeId: 1,
title: 'Apotheke ergänzen',
instruction: 'Setze das passende Wort ein.',
questionData: {
type: 'gap_fill',
text: 'Wo ist die {gap}?',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['Apotheke']
},
explanation: 'Das ist eine zentrale Frage für Alltag und Gesundheit.'
},
withTypeName('situational_response', {
title: 'Beim Arzt knapp reagieren',
instruction: 'Antworte kurz und passend auf Deutsch.',
questionData: {
type: 'situational_response',
question: 'Die Sprechstundenhilfe fragt: "Haben Sie einen Termin?"',
keywords: ['habe', 'termin']
},
answerData: {
modelAnswer: 'Ja, ich habe einen Termin.',
keywords: ['habe', 'termin']
},
explanation: 'Kurze sichere Antworten helfen besonders in belastenden Situationen.'
})
],
'Amt, Dokumente, Anmeldung': [
{
exerciseTypeId: 2,
title: 'Anmeldung im Amt',
instruction: 'Wähle die passende deutsche Formulierung.',
questionData: {
type: 'multiple_choice',
question: 'Du willst dich anmelden. Welche Aussage passt?',
options: ['Ich möchte mich anmelden.', 'Ich will Anmeldung machen mich.', 'Ich melde ich.', 'Anmelden ich möchte Formular.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Ich möchte mich anmelden." ist ein zentraler Standardsatz für Ämter.'
},
{
exerciseTypeId: 1,
title: 'Dokument zeigen',
instruction: 'Fülle die Lücke mit dem passenden Wort.',
questionData: {
type: 'gap_fill',
text: 'Hier ist mein {gap}.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['Formular']
},
explanation: 'Das Formular ist ein zentrales Schlüsselwort dieser Lektion.'
},
{
exerciseTypeId: 3,
title: 'Adressfrage bauen',
instruction: 'Ordne die Wörter zu einer typischen Amtsfrage.',
questionData: {
type: 'sentence_building',
question: 'Baue die Frage nach der Adresse.',
tokens: ['Was', 'ist', 'Ihre', 'Adresse']
},
answerData: {
correct: ['Was ist Ihre Adresse']
},
explanation: 'Solche festen Fragen tauchen in Formular- und Anmeldungssituationen ständig auf.'
}
],
'Nebensätze mit weil - Einstieg': [
{
exerciseTypeId: 2,
title: 'Weil-Satz erkennen',
instruction: 'Wähle den grammatisch passenden Satz.',
questionData: {
type: 'multiple_choice',
question: 'Welche Form mit "weil" ist richtig?',
options: ['Ich bleibe zu Hause, weil ich krank bin.', 'Ich bleibe zu Hause, weil ich bin krank.', 'Weil ich krank, ich bleibe zu Hause.', 'Ich bleibe weil zu Hause ich krank bin.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Im Nebensatz mit "weil" steht das Verb am Ende.'
},
{
exerciseTypeId: 1,
title: 'Weil ergänzen',
instruction: 'Setze das passende Bindewort ein.',
questionData: {
type: 'gap_fill',
text: 'Ich komme später, {gap} ich arbeite.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['weil']
},
explanation: 'Mit "weil" gibst du im Deutschen einen Grund an.'
},
{
exerciseTypeId: 3,
title: 'Grund richtig ordnen',
instruction: 'Ordne die Wörter zu einem korrekten weil-Satz.',
questionData: {
type: 'sentence_building',
question: 'Baue den Grundsatz richtig.',
tokens: ['weil', 'ich', 'arbeite']
},
answerData: {
correct: ['weil ich arbeite']
},
explanation: 'Kleine korrekte Nebensätze sind ein wichtiger nächster Schritt.'
}
],
'Arbeitssuche & Termine': [
{
exerciseTypeId: 2,
title: 'Arbeit suchen',
instruction: 'Wähle die passende Aussage.',
questionData: {
type: 'multiple_choice',
question: 'Welche Formulierung passt zu Arbeitssuche und Termin?',
options: ['Ich suche Arbeit. Ich habe morgen einen Termin.', 'Ich suche arbeiten. Morgen Termin ich.', 'Ich bin Arbeit suchen.', 'Morgen ich suche Termin Arbeit.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Beide Sätze sind typische kurze Aussagen für Bewerbung und Alltag.'
},
{
exerciseTypeId: 1,
title: 'Termin ergänzen',
instruction: 'Fülle die Lücke aus.',
questionData: {
type: 'gap_fill',
text: 'Ich habe morgen einen {gap}.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['Termin']
},
explanation: 'Der Termin gehört zu den wichtigsten Alltagswörtern im Deutschen.'
}
],
'Trennbare Verben - Einstieg': [
{
exerciseTypeId: 2,
title: 'Trennbares Verb erkennen',
instruction: 'Wähle den richtigen Hauptsatz.',
questionData: {
type: 'multiple_choice',
question: 'Welche Form mit trennbarem Verb ist richtig?',
options: ['Ich rufe dich an.', 'Ich anrufe dich.', 'Ich dich rufe an.', 'Ich rufe an dich.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Im Hauptsatz trennt sich das Verb: "Ich rufe dich an."'
},
{
exerciseTypeId: 1,
title: 'Trennteil ergänzen',
instruction: 'Setze den fehlenden Verbteil ein.',
questionData: {
type: 'gap_fill',
text: 'Ich stehe um sechs Uhr {gap}.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['auf']
},
explanation: 'Bei "aufstehen" steht der Teil "auf" im Hauptsatz am Ende.'
}
],
'Einladungen & soziale Treffen': [
{
exerciseTypeId: 2,
title: 'Einladung formulieren',
instruction: 'Wähle die natürlichste Einladung.',
questionData: {
type: 'multiple_choice',
question: 'Du willst jemanden für morgen einladen. Welche Form passt?',
options: ['Hast du Zeit? Kommst du morgen?', 'Du Zeit morgen kommst?', 'Morgen du kommst Zeit?', 'Kommst du Zeit hat morgen?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Kurze direkte Fragen sind im Deutschen in dieser Situation sehr üblich.'
},
withTypeName('situational_response', {
title: 'Treffen ausmachen',
instruction: 'Antworte kurz und passend auf Deutsch.',
questionData: {
type: 'situational_response',
question: 'Du willst ein Treffen für morgen bestätigen.',
keywords: ['morgen', 'zeit']
},
answerData: {
modelAnswer: 'Ja, morgen habe ich Zeit.',
keywords: ['morgen', 'zeit']
},
explanation: 'Die Antwort bestätigt knapp und klar die Verfügbarkeit.'
})
],
'Einkauf, Reklamation, Rückgabe': [
{
exerciseTypeId: 2,
title: 'Rückgabe im Laden',
instruction: 'Wähle die passende Reklamation.',
questionData: {
type: 'multiple_choice',
question: 'Ein gekaufter Gegenstand ist kaputt. Was sagst du?',
options: ['Das ist kaputt. Ich möchte das zurückgeben.', 'Das kaputt ich zurückgeben.', 'Ich kaputt zurück das.', 'Zurückgeben ich kaputt das.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Das ist die klare und höfliche Standardsituation bei einer Reklamation.'
},
{
exerciseTypeId: 1,
title: 'Kassenbon ergänzen',
instruction: 'Fülle die Lücke mit dem passenden Wort.',
questionData: {
type: 'gap_fill',
text: 'Haben Sie einen {gap}?',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['Kassenbon']
},
explanation: 'Der Kassenbon ist im Rückgabe-Dialog ein sehr typisches Schlüsselwort.'
}
],
'würde / hätte gern - Einstieg': [
{
exerciseTypeId: 2,
title: 'Höflichen Wunsch wählen',
instruction: 'Wähle die höflichste Formulierung.',
questionData: {
type: 'multiple_choice',
question: 'Welche Form klingt in einem Servicegespräch höflich?',
options: ['Ich hätte gern einen Termin.', 'Ich will Termin.', 'Ich nehme Termin.', 'Termin ich gern habe.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Ich hätte gern ..." ist ein sehr nützliches höfliches Muster.'
},
{
exerciseTypeId: 1,
title: 'Wunschform ergänzen',
instruction: 'Setze den passenden Ausdruck ein.',
questionData: {
type: 'gap_fill',
text: 'Ich {gap} gern Hilfe.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['hätte']
},
explanation: 'Mit "hätte gern" formuliert man Wünsche höflicher.'
}
],
'Dialogtag - Alltag': [
{
exerciseTypeId: 2,
title: 'Alltagsschritte verbinden',
instruction: 'Wähle die natürlichste kleine Alltagsfolge.',
questionData: {
type: 'multiple_choice',
question: 'Welche Minifolge klingt als Alltagsbericht am besten?',
options: ['Heute muss ich arbeiten. Danach gehe ich einkaufen.', 'Heute muss ich arbeiten danach ich einkaufen gehe.', 'Ich heute arbeiten muss danach einkaufen.', 'Danach heute ich muss arbeiten.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Die Lektion trainiert verbundene Alltagsschritte in klaren deutschen Hauptsätzen.'
},
{
exerciseTypeId: 3,
title: 'Alltagsfolge bauen',
instruction: 'Ordne die Wörter zur ersten Aussage.',
questionData: {
type: 'sentence_building',
question: 'Baue einen typischen Alltagsaussagesatz.',
tokens: ['Heute', 'muss', 'ich', 'arbeiten']
},
answerData: {
correct: ['Heute muss ich arbeiten']
},
explanation: 'So trainierst du gleichzeitig Alltagssprache und Satzstellung.'
}
],
'Fehlertraining - Artikel & Kasus I': [
{
exerciseTypeId: 2,
title: 'Kasusform erkennen',
instruction: 'Wähle die richtige Form im Satz.',
questionData: {
type: 'multiple_choice',
question: 'Welche Form ist richtig?',
options: ['Ich brauche den Termin.', 'Ich brauche dem Termin.', 'Ich brauche der Termin.', 'Ich brauche das Termin.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Bei "brauchen" steht das Objekt hier im Akkusativ: "den Termin".'
},
{
exerciseTypeId: 2,
title: 'Präposition mit Dativ',
instruction: 'Wähle die korrekte Form mit Präposition.',
questionData: {
type: 'multiple_choice',
question: 'Welche Form ist richtig?',
options: ['Ich fahre mit dem Bus.', 'Ich fahre mit den Bus.', 'Ich fahre mit der Bus.', 'Ich fahre mit das Bus.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Nach "mit" steht hier der Dativ: "mit dem Bus".'
}
],
'Rollenspiel - Missverständnisse lösen': [
{
exerciseTypeId: 2,
title: 'Missverständnis klären',
instruction: 'Wähle die passendste Reaktion.',
questionData: {
type: 'multiple_choice',
question: 'Du hast etwas nicht verstanden. Was sagst du höflich?',
options: ['Ich verstehe das nicht. Können Sie das bitte wiederholen?', 'Ich nicht verstehe. Sie wiederholen.', 'Nicht verstehen ich das.', 'Wiederholen das bitte ich.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Die Kombination aus Klärung und höflicher Bitte ist hier besonders nützlich.'
},
withTypeName('situational_response', {
title: 'Ruhig nachfragen',
instruction: 'Antworte mit einer höflichen Nachfrage.',
questionData: {
type: 'situational_response',
question: 'Du hast etwas akustisch nicht verstanden.',
keywords: ['verstehe', 'bitte', 'wiederholen']
},
answerData: {
modelAnswer: 'Ich verstehe das nicht. Können Sie das bitte wiederholen?',
keywords: ['verstehe', 'bitte', 'wiederholen']
},
explanation: 'Das Muster lässt sich in vielen realen Situationen direkt verwenden.'
})
],
'Freies Sprechen - Alltag ohne Stütze': [
withTypeName('speaking_from_memory', {
title: 'Typischen Tag frei erzählen',
instruction: 'Sprich frei über einen normalen Tag.',
questionData: {
type: 'speaking_from_memory',
question: 'Erzähle frei: Was machst du zuerst, dann und später?',
expectedText: 'Zuerst arbeite ich. Dann gehe ich einkaufen. Später bin ich zu Hause.',
keywords: ['zuerst', 'dann', 'später', 'arbeite', 'zu']
},
answerData: {
type: 'speaking_from_memory'
},
explanation: 'Die Übung trainiert freien Ablauf statt einzelner isolierter Sätze.'
}),
withTypeName('situational_response', {
title: 'Freier Tagesbericht',
instruction: 'Antworte mit einem kurzen freien Mini-Bericht.',
questionData: {
type: 'situational_response',
question: 'Was machst du heute und später?',
keywords: ['heute', 'dann', 'später']
},
answerData: {
modelAnswer: 'Heute arbeite ich. Dann gehe ich einkaufen. Später bin ich zu Hause.',
keywords: ['heute', 'dann', 'später']
},
explanation: 'Wichtig ist eine klare kleine Struktur, nicht maximale Länge.'
})
],
'Abschlussprüfung - Gesamtpfad': [
{
exerciseTypeId: 2,
title: 'Gesamtpfad - Alltagssituation',
instruction: 'Wähle die komplett passendste Reaktion.',
questionData: {
type: 'multiple_choice',
question: 'Du bist neu in Deutschland, hast morgen einen Termin und brauchst heute Hilfe. Welche Aussage passt am besten?',
options: ['Ich brauche heute Hilfe. Morgen habe ich einen Termin.', 'Heute Hilfe ich brauche morgen Termin habe.', 'Ich morgen Hilfe heute Termin.', 'Termin morgen Hilfe heute ich.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Die Abschlussprüfung bündelt kurze, funktionale Alltagssprache über mehrere Themen.'
},
{
exerciseTypeId: 1,
title: 'Abschluss - wichtiges Kernwort',
instruction: 'Fülle das wichtige Alltagswort ein.',
questionData: {
type: 'gap_fill',
text: 'Können Sie mir bitte {gap}?',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['helfen']
},
explanation: '"Können Sie mir bitte helfen?" ist ein sehr nützliches Kernmuster für den Gesamtpfad.'
},
withTypeName('speaking_from_memory', {
title: 'Abschluss - frei reagieren',
instruction: 'Sprich eine kurze funktionale Antwort frei nach.',
questionData: {
type: 'speaking_from_memory',
question: 'Stell dich vor und nenne ein aktuelles Alltagsproblem.',
expectedText: 'Hallo, ich heiße Ana. Ich brauche Hilfe, weil ich morgen einen Termin habe.',
keywords: ['hallo', 'heiße', 'hilfe', 'weil', 'termin']
},
answerData: {
type: 'speaking_from_memory'
},
explanation: 'Die Abschlussübung verbindet Vorstellung, Bedarf und Begründung in einem kurzen freien Beitrag.'
})
],
'Kulturelle Orientierung in Deutschland vertieft': [
{
exerciseTypeId: 2,
title: 'Kulturelle Alltagserwartung',
instruction: 'Wähle die Aussage, die gut zum kulturellen Fokus passt.',
questionData: {
type: 'multiple_choice',
question: 'Welche Aussage passt besonders gut zum kulturellen Schwerpunkt dieser Lektion?',
options: ['Termine und Pünktlichkeit sind oft sehr wichtig.', 'In Deutschland sind Termine meistens egal.', 'Direktheit ist immer unhöflich.', 'Siezen spielt keine Rolle.']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: 'Pünktlichkeit und klare Absprachen sind in vielen Alltagssituationen zentral.'
},
withTypeName('situational_response', {
title: 'Kulturell angemessen reagieren',
instruction: 'Antworte mit einer kurzen kulturell passenden Aussage.',
questionData: {
type: 'situational_response',
question: 'Warum ist es wichtig, bei einem Termin pünktlich zu sein?',
keywords: ['termin', 'pünktlich', 'wichtig']
},
answerData: {
modelAnswer: 'Termine und Pünktlichkeit sind in Deutschland oft sehr wichtig.',
keywords: ['termin', 'pünktlich', 'wichtig']
},
explanation: 'Die Übung verbindet Sprache mit kultureller Orientierung.'
})
]
};
async function resolveExerciseTypeId(exercise) {
if (exercise.exerciseTypeId) {
return exercise.exerciseTypeId;
}
const trimmedName =
exercise.exerciseTypeName != null && exercise.exerciseTypeName !== ''
? String(exercise.exerciseTypeName).trim()
: '';
if (!trimmedName) {
throw new Error(`Kein exerciseTypeId oder exerciseTypeName für Übung "${exercise.title || 'unbenannt'}" definiert`);
}
const [type] = await sequelize.query(
`SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`,
{
replacements: { name: trimmedName },
type: sequelize.QueryTypes.SELECT
}
);
if (!type) {
throw new Error(`Übungstyp "${trimmedName}" nicht gefunden`);
}
return Number(type.id);
}
async function findOrCreateSystemUser() {
let systemUser = await User.findOne({ where: { username: 'system' } });
if (!systemUser) {
systemUser = await User.findOne({ where: { username: 'admin' } });
}
if (!systemUser) {
throw new Error('System user not found');
}
return systemUser;
}
function getExercisesForLesson(lesson) {
if (GERMAN_EXERCISES[lesson.title]) {
return GERMAN_EXERCISES[lesson.title];
}
for (const [key, exercises] of Object.entries(GERMAN_EXERCISES)) {
if (lesson.title.includes(key) || key.includes(lesson.title)) {
return exercises;
}
}
return generateExercisesFromDidactics(lesson);
}
async function createGermanForBisayaCourseContent() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
const systemUser = await findOrCreateSystemUser();
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
const [germanLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Deutsch' LIMIT 1`,
{
type: sequelize.QueryTypes.SELECT
}
);
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{
type: sequelize.QueryTypes.SELECT
}
);
if (!germanLanguage || !bisayaLanguage) {
throw new Error('Deutsch oder Bisaya als Sprache nicht gefunden');
}
const courses = await sequelize.query(
`SELECT id, title, owner_user_id AS "ownerUserId"
FROM community.vocab_course
WHERE language_id = :languageId
AND native_language_id = :nativeLanguageId
AND title LIKE 'Deutsch für Bisaya-Lernende%'`,
{
replacements: {
languageId: germanLanguage.id,
nativeLanguageId: bisayaLanguage.id
},
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${courses.length} Deutsch-für-Bisaya-Kurse\n`);
let totalExercisesAdded = 0;
let totalLessonsProcessed = 0;
const replaceLessons = new Set(Object.keys(GERMAN_EXERCISES));
for (const course of courses) {
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
const lessons = await VocabCourseLesson.findAll({
where: { courseId: course.id },
order: [['lessonNumber', 'ASC']]
});
console.log(` ${lessons.length} Lektionen gefunden\n`);
for (const lesson of lessons) {
const exercises = getExercisesForLesson(lesson);
if (exercises.length === 0) {
console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen definiert`);
continue;
}
const existingCount = await VocabGrammarExercise.count({ where: { lessonId: lesson.id } });
const replaceExisting = replaceLessons.has(lesson.title);
if (existingCount > 0 && !replaceExisting) {
console.log(` ⏭️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - bereits ${existingCount} Übung(en) vorhanden`);
continue;
}
if (replaceExisting && existingCount > 0) {
const deleted = await VocabGrammarExercise.destroy({ where: { lessonId: lesson.id } });
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deleted} bestehende Übung(en) entfernt`);
}
let exerciseNumber = 1;
for (const exerciseData of exercises) {
const exerciseTypeId = await resolveExerciseTypeId(exerciseData);
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId,
exerciseNumber: exerciseNumber++,
title: exerciseData.title,
instruction: exerciseData.instruction,
questionData: JSON.stringify(exerciseData.questionData),
answerData: JSON.stringify(exerciseData.answerData),
explanation: exerciseData.explanation,
createdByUserId: course.ownerUserId || systemUser.id
});
totalExercisesAdded++;
}
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} Übung(en) erstellt`);
totalLessonsProcessed++;
}
console.log('');
}
console.log('\n🎉 Zusammenfassung:');
console.log(` ${totalLessonsProcessed} Lektionen bearbeitet`);
console.log(` ${totalExercisesAdded} Übungen erstellt`);
}
createGermanForBisayaCourseContent()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});