From 09e53244d90517078cec472ffa049927f1ccc6ad Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 19 Jan 2026 11:43:38 +0100 Subject: [PATCH] Add native language support in vocab course management - Introduced a new field for native language in the VocabCourse model to allow learners to specify their native language. - Updated the VocabService to handle native language during course creation and retrieval, including filtering options. - Enhanced the database schema to include foreign key constraints for native language. - Updated frontend components to support native language selection and display in course listings. - Added internationalization strings for native language features in both German and English. --- backend/models/community/vocab_course.js | 6 + backend/scripts/create-language-courses.js | 452 ++++++++++++++++++ backend/services/vocabService.js | 21 +- .../sql/add-native-language-to-courses.sql | 24 + backend/utils/syncDatabase.js | 7 + .../src/i18n/locales/de/socialnetwork.json | 5 + .../src/i18n/locales/en/socialnetwork.json | 5 + .../src/views/social/VocabCourseListView.vue | 16 +- 8 files changed, 531 insertions(+), 5 deletions(-) create mode 100755 backend/scripts/create-language-courses.js create mode 100644 backend/sql/add-native-language-to-courses.sql diff --git a/backend/models/community/vocab_course.js b/backend/models/community/vocab_course.js index f3c3543..68f1540 100644 --- a/backend/models/community/vocab_course.js +++ b/backend/models/community/vocab_course.js @@ -27,6 +27,12 @@ VocabCourse.init({ allowNull: false, field: 'language_id' }, + nativeLanguageId: { + type: DataTypes.INTEGER, + allowNull: true, + field: 'native_language_id', + comment: 'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".' + }, difficultyLevel: { type: DataTypes.INTEGER, allowNull: false, diff --git a/backend/scripts/create-language-courses.js b/backend/scripts/create-language-courses.js new file mode 100755 index 0000000..b2bc178 --- /dev/null +++ b/backend/scripts/create-language-courses.js @@ -0,0 +1,452 @@ +#!/usr/bin/env node +/** + * Script zum Erstellen von Sprachkursen für verschiedene Sprachen + * + * Verwendung: + * node backend/scripts/create-language-courses.js + * + * Erstellt Kurse für: Bisaya, Französisch, Spanisch, Latein, Italienisch, Portugiesisch, Tagalog + */ + +import { sequelize } from '../utils/sequelize.js'; +import VocabCourse from '../models/community/vocab_course.js'; +import VocabCourseLesson from '../models/community/vocab_course_lesson.js'; +import User from '../models/community/user.js'; +import crypto from 'crypto'; + +// Kursstruktur für alle Sprachen (4 Wochen, 40 Lektionen) +const LESSON_TEMPLATE = [ + // WOCHE 1: Grundlagen & Aussprache + { week: 1, day: 1, num: 1, type: 'conversation', title: 'Begrüßungen & Höflichkeit', + desc: 'Lerne die wichtigsten Begrüßungen und Höflichkeitsformeln', + targetMin: 15, targetScore: 80, review: false, + cultural: 'Höflichkeit ist wichtig. Lächeln hilft!' }, + + { week: 1, day: 1, num: 2, type: 'vocab', title: 'Überlebenssätze - Teil 1', + desc: 'Die 10 wichtigsten Sätze für den Alltag', + targetMin: 20, targetScore: 85, review: true, + cultural: 'Diese Sätze helfen dir sofort im Alltag weiter.' }, + + { week: 1, day: 2, num: 3, type: 'vocab', title: 'Familienwörter', + desc: 'Mama, Papa, Geschwister, Großeltern und mehr', + targetMin: 20, targetScore: 85, review: true, + cultural: 'Familienwörter sind wichtig für echte Gespräche.' }, + + { week: 1, day: 2, num: 4, type: 'conversation', title: 'Familien-Gespräche', + desc: 'Einfache Gespräche mit Familienmitgliedern', + targetMin: 15, targetScore: 80, review: false, + cultural: 'Familienkonversationen sind herzlicher als formelle Gespräche.' }, + + { week: 1, day: 3, num: 5, type: 'conversation', title: 'Gefühle & Zuneigung', + desc: 'Wie man Gefühle ausdrückt', + targetMin: 15, targetScore: 80, review: false, + cultural: 'Gefühle auszudrücken ist wichtig für echte Verbindung.' }, + + { week: 1, day: 3, num: 6, type: 'vocab', title: 'Überlebenssätze - Teil 2', + desc: 'Weitere wichtige Alltagssätze', + targetMin: 20, targetScore: 85, review: true, + cultural: null }, + + { week: 1, day: 4, num: 7, type: 'conversation', title: 'Essen & Fürsorge', + desc: 'Gespräche rund ums Essen', + targetMin: 15, targetScore: 80, review: false, + cultural: 'Essen verbindet Menschen überall auf der Welt.' }, + + { week: 1, day: 4, num: 8, type: 'vocab', title: 'Essen & Trinken', + desc: 'Wichtige Wörter rund ums Essen', + targetMin: 20, targetScore: 85, review: true, + cultural: null }, + + { week: 1, day: 5, num: 9, type: 'review', title: 'Woche 1 - Wiederholung', + desc: 'Wiederhole alle Inhalte der ersten Woche', + targetMin: 30, targetScore: 80, review: false, + cultural: 'Wiederholung ist der Schlüssel zum Erfolg!' }, + + { week: 1, day: 5, num: 10, type: 'vocab', title: 'Woche 1 - Vokabeltest', + desc: 'Teste dein Wissen aus Woche 1', + targetMin: 15, targetScore: 80, review: true, + cultural: null }, + + // WOCHE 2: Alltag & Familie + { week: 2, day: 1, num: 11, type: 'conversation', title: 'Alltagsgespräche - Teil 1', + desc: 'Wie war dein Tag? Was machst du?', + targetMin: 15, targetScore: 80, review: false, + cultural: 'Alltagsgespräche sind wichtig für echte Kommunikation.' }, + + { week: 2, day: 1, num: 12, type: 'vocab', title: 'Haus & Familie', + desc: 'Wörter für Haus, Zimmer, Familie', + targetMin: 20, targetScore: 85, review: true, + cultural: null }, + + { week: 2, day: 2, num: 13, type: 'conversation', title: 'Alltagsgespräche - Teil 2', + desc: 'Wohin gehst du? Was machst du heute?', + targetMin: 15, targetScore: 80, review: false, + cultural: null }, + + { week: 2, day: 2, num: 14, type: 'vocab', title: 'Ort & Richtung', + desc: 'Wo, hier, dort, gehen zu', + targetMin: 20, targetScore: 85, review: true, + cultural: null }, + + { week: 2, day: 3, num: 15, type: 'grammar', title: 'Zeitformen - Grundlagen', + desc: 'Vergangenheit, Gegenwart, Zukunft', + targetMin: 25, targetScore: 75, review: true, + cultural: 'Zeitformen sind wichtig für präzise Kommunikation.' }, + + { week: 2, day: 3, num: 16, type: 'vocab', title: 'Zeit & Datum', + desc: 'Jetzt, morgen, gestern, heute', + targetMin: 20, targetScore: 85, review: true, + cultural: null }, + + { week: 2, day: 4, num: 17, type: 'conversation', title: 'Einkaufen & Preise', + desc: 'Wie viel kostet das? Kann es billiger sein?', + targetMin: 15, targetScore: 80, review: false, + cultural: 'Einkaufen ist eine wichtige Alltagssituation.' }, + + { week: 2, day: 4, num: 18, type: 'vocab', title: 'Zahlen & Preise', + desc: 'Zahlen 1-100, Preise, Mengen', + targetMin: 25, targetScore: 85, review: true, + cultural: null }, + + { week: 2, day: 5, num: 19, type: 'review', title: 'Woche 2 - Wiederholung', + desc: 'Wiederhole alle Inhalte der zweiten Woche', + targetMin: 30, targetScore: 80, review: false, + cultural: null }, + + { week: 2, day: 5, num: 20, type: 'vocab', title: 'Woche 2 - Vokabeltest', + desc: 'Teste dein Wissen aus Woche 2', + targetMin: 15, targetScore: 80, review: true, + cultural: null }, + + // WOCHE 3: Vertiefung + { week: 3, day: 1, num: 21, type: 'conversation', title: 'Gefühle & Emotionen', + desc: 'Wie man verschiedene Gefühle ausdrückt', + targetMin: 15, targetScore: 80, review: false, + cultural: 'Emotionen auszudrücken ist wichtig für echte Verbindung.' }, + + { week: 3, day: 1, num: 22, type: 'vocab', title: 'Gefühle & Emotionen', + desc: 'Wörter für verschiedene Gefühle', + targetMin: 20, targetScore: 85, review: true, + cultural: null }, + + { week: 3, day: 2, num: 23, type: 'conversation', title: 'Gesundheit & Wohlbefinden', + desc: 'Gespräche über Gesundheit', + targetMin: 15, targetScore: 80, review: false, + cultural: null }, + + { week: 3, day: 2, num: 24, type: 'vocab', title: 'Körper & Gesundheit', + desc: 'Wörter rund um den Körper und Gesundheit', + targetMin: 20, targetScore: 85, review: true, + cultural: null }, + + { week: 3, day: 3, num: 25, type: 'grammar', title: 'Höflichkeitsformen', + desc: 'Wie man höflich spricht', + targetMin: 20, targetScore: 75, review: true, + cultural: 'Höflichkeit ist extrem wichtig in jeder Kultur.' }, + + { week: 3, day: 3, num: 26, type: 'conversation', title: 'Bitten & Fragen', + desc: 'Wie man höflich fragt und bittet', + targetMin: 15, targetScore: 80, review: false, + cultural: null }, + + { week: 3, day: 4, num: 27, type: 'conversation', title: 'Kinder & Familie', + desc: 'Gespräche mit und über Kinder', + targetMin: 15, targetScore: 80, review: false, + cultural: 'Kinder sind sehr wichtig in Familien.' }, + + { week: 3, day: 4, num: 28, type: 'vocab', title: 'Kinder & Spiel', + desc: 'Wörter für Kinder und Spielsachen', + targetMin: 20, targetScore: 85, review: true, + cultural: null }, + + { week: 3, day: 5, num: 29, type: 'review', title: 'Woche 3 - Wiederholung', + desc: 'Wiederhole alle Inhalte der dritten Woche', + targetMin: 30, targetScore: 80, review: false, + cultural: null }, + + { week: 3, day: 5, num: 30, type: 'vocab', title: 'Woche 3 - Vokabeltest', + desc: 'Teste dein Wissen aus Woche 3', + targetMin: 15, targetScore: 80, review: true, + cultural: null }, + + // WOCHE 4: Freies Sprechen + { week: 4, day: 1, num: 31, type: 'conversation', title: 'Freies Gespräch - Thema 1', + desc: 'Übe freies Sprechen zu verschiedenen Themen', + targetMin: 20, targetScore: 75, review: false, + cultural: 'Fehler sind okay! Muttersprachler schätzen das Bemühen.' }, + + { week: 4, day: 1, num: 32, type: 'vocab', title: 'Wiederholung - Woche 1 & 2', + desc: 'Wiederhole wichtige Vokabeln aus den ersten beiden Wochen', + targetMin: 25, targetScore: 85, review: true, + cultural: null }, + + { week: 4, day: 2, num: 33, type: 'conversation', title: 'Freies Gespräch - Thema 2', + desc: 'Weitere Übung im freien Sprechen', + targetMin: 20, targetScore: 75, review: false, + cultural: null }, + + { week: 4, day: 2, num: 34, type: 'vocab', title: 'Wiederholung - Woche 3', + desc: 'Wiederhole wichtige Vokabeln aus Woche 3', + targetMin: 25, targetScore: 85, review: true, + cultural: null }, + + { week: 4, day: 3, num: 35, type: 'conversation', title: 'Komplexere Gespräche', + desc: 'Längere Gespräche zu verschiedenen Themen', + targetMin: 25, targetScore: 75, review: false, + cultural: 'Je mehr du sprichst, desto besser wirst du!' }, + + { week: 4, day: 3, num: 36, type: 'review', title: 'Gesamtwiederholung', + desc: 'Wiederhole alle wichtigen Inhalte des Kurses', + targetMin: 30, targetScore: 80, review: false, + cultural: null }, + + { week: 4, day: 4, num: 37, type: 'conversation', title: 'Praktische Übung', + desc: 'Simuliere echte Gesprächssituationen', + targetMin: 25, targetScore: 75, review: false, + cultural: null }, + + { week: 4, day: 4, num: 38, type: 'vocab', title: 'Abschlusstest - Vokabeln', + desc: 'Finaler Vokabeltest über den gesamten Kurs', + targetMin: 20, targetScore: 80, review: true, + cultural: null }, + + { week: 4, day: 5, num: 39, type: 'review', title: 'Abschlussprüfung', + desc: 'Finale Prüfung über alle Kursinhalte', + targetMin: 30, targetScore: 80, review: false, + cultural: 'Gratulation zum Abschluss des Kurses!' }, + + { week: 4, day: 5, num: 40, type: 'culture', title: 'Kulturelle Tipps & Tricks', + desc: 'Wichtige kulturelle Hinweise für den Alltag', + targetMin: 15, targetScore: 0, review: false, + cultural: 'Kulturelles Verständnis ist genauso wichtig wie die Sprache selbst.' } +]; + +// Zielsprachen (die zu lernenden Sprachen) +const TARGET_LANGUAGES = [ + 'Bisaya', + 'Französisch', + 'Spanisch', + 'Latein', + 'Italienisch', + 'Portugiesisch', + 'Tagalog' +]; + +// Muttersprachen (für die Kurse erstellt werden) +const NATIVE_LANGUAGES = [ + 'Deutsch', + 'Englisch', + 'Spanisch', + 'Französisch', + 'Italienisch', + 'Portugiesisch' +]; + +// Generiere Kurskonfigurationen für alle Kombinationen +function generateCourseConfigs() { + const configs = []; + + for (const targetLang of TARGET_LANGUAGES) { + for (const nativeLang of NATIVE_LANGUAGES) { + // Überspringe, wenn Zielsprache = Muttersprache + if (targetLang === nativeLang) continue; + + const title = `${targetLang} für ${nativeLang}sprachige - Schnellstart in 4 Wochen`; + let description = `Lerne ${targetLang} schnell und praktisch für den Alltag. `; + + if (targetLang === 'Latein') { + description = `Lerne ${targetLang} systematisch mit Fokus auf Grammatik und Vokabular. `; + } else if (targetLang === 'Bisaya') { + description = `Lerne ${targetLang} (Cebuano) schnell und praktisch für den Familienalltag. `; + } + + description += `Fokus auf Sprechen & Hören mit strukturiertem 4-Wochen-Plan.`; + + configs.push({ + targetLanguageName: targetLang, + nativeLanguageName: nativeLang, + title, + description, + difficultyLevel: 1 + }); + } + } + + return configs; +} + +const LANGUAGE_COURSES = generateCourseConfigs(); + +async function findOrCreateLanguage(languageName, ownerUserId) { + // Suche zuerst nach vorhandener Sprache + const [existing] = await sequelize.query( + `SELECT id FROM community.vocab_language WHERE name = :name LIMIT 1`, + { + replacements: { name: languageName }, + type: sequelize.QueryTypes.SELECT + } + ); + + if (existing) { + console.log(` ✅ Sprache "${languageName}" bereits vorhanden (ID: ${existing.id})`); + return existing.id; + } + + // Erstelle neue Sprache + const shareCode = crypto.randomBytes(8).toString('hex'); + const [created] = await sequelize.query( + `INSERT INTO community.vocab_language (owner_user_id, name, share_code) + VALUES (:ownerUserId, :name, :shareCode) + RETURNING id`, + { + replacements: { ownerUserId, name: languageName, shareCode }, + type: sequelize.QueryTypes.SELECT + } + ); + + console.log(` ✅ Sprache "${languageName}" erstellt (ID: ${created.id})`); + return created.id; +} + +async function createCourseForLanguage(targetLanguageId, nativeLanguageId, languageConfig, ownerUserId) { + const shareCode = crypto.randomBytes(8).toString('hex'); + + const course = await VocabCourse.create({ + ownerUserId, + title: languageConfig.title, + description: languageConfig.description, + languageId: Number(targetLanguageId), + nativeLanguageId: nativeLanguageId ? Number(nativeLanguageId) : null, + difficultyLevel: languageConfig.difficultyLevel || 1, + isPublic: true, + shareCode + }); + + console.log(` ✅ Kurs erstellt: "${course.title}" (ID: ${course.id}, Share-Code: ${shareCode})`); + + // Erstelle Lektionen + for (const lessonData of LESSON_TEMPLATE) { + await VocabCourseLesson.create({ + courseId: course.id, + chapterId: null, + lessonNumber: lessonData.num, + title: lessonData.title, + description: lessonData.desc, + weekNumber: lessonData.week, + dayNumber: lessonData.day, + lessonType: lessonData.type, + culturalNotes: lessonData.cultural, + targetMinutes: lessonData.targetMin, + targetScorePercent: lessonData.targetScore, + requiresReview: lessonData.review + }); + } + + console.log(` ✅ ${LESSON_TEMPLATE.length} Lektionen erstellt`); + return course; +} + +async function createAllLanguageCourses(ownerHashedId) { + try { + // Finde User + const user = await User.findOne({ where: { hashedId: ownerHashedId } }); + if (!user) { + throw new Error(`User mit hashedId ${ownerHashedId} nicht gefunden`); + } + + console.log(`\n🚀 Erstelle Sprachkurse für Benutzer: ${user.id}\n`); + + const createdCourses = []; + + // Stelle sicher, dass alle benötigten Sprachen existieren + console.log(`\n🌍 Stelle sicher, dass alle Sprachen existieren...`); + const allLanguages = [...new Set([...TARGET_LANGUAGES, ...NATIVE_LANGUAGES])]; + const languageMap = new Map(); + + for (const langName of allLanguages) { + const langId = await findOrCreateLanguage(langName, user.id); + languageMap.set(langName, langId); + } + + for (const langConfig of LANGUAGE_COURSES) { + console.log(`\n📚 Verarbeite: ${langConfig.targetLanguageName} für ${langConfig.nativeLanguageName}sprachige`); + + const targetLanguageId = languageMap.get(langConfig.targetLanguageName); + const nativeLanguageId = languageMap.get(langConfig.nativeLanguageName); + + // Prüfe, ob Kurs bereits existiert + const existingCourse = await VocabCourse.findOne({ + where: { + languageId: targetLanguageId, + nativeLanguageId: nativeLanguageId, + ownerUserId: user.id + } + }); + + if (existingCourse) { + console.log(` ⚠️ Kurs "${langConfig.title}" existiert bereits (ID: ${existingCourse.id})`); + createdCourses.push({ + ...langConfig, + courseId: existingCourse.id, + targetLanguageId, + nativeLanguageId, + skipped: true + }); + continue; + } + + // Erstelle Kurs + const course = await createCourseForLanguage(targetLanguageId, nativeLanguageId, langConfig, user.id); + createdCourses.push({ + ...langConfig, + courseId: course.id, + targetLanguageId, + nativeLanguageId, + shareCode: course.shareCode + }); + } + + console.log(`\n\n🎉 Zusammenfassung:\n`); + console.log(` Gesamt: ${LANGUAGE_COURSES.length} Sprachen`); + console.log(` Erstellt: ${createdCourses.filter(c => !c.skipped).length} Kurse`); + console.log(` Übersprungen: ${createdCourses.filter(c => c.skipped).length} Kurse`); + + console.log(`\n📋 Erstellte Kurse:\n`); + for (const course of createdCourses) { + if (course.skipped) { + console.log(` ⚠️ ${course.languageName}: Bereits vorhanden (ID: ${course.courseId})`); + } else { + console.log(` ✅ ${course.languageName}: ${course.title}`); + console.log(` Share-Code: ${course.shareCode}`); + } + } + + console.log(`\n💡 Nächste Schritte:`); + console.log(` 1. Füge Vokabeln zu den Vokabel-Lektionen hinzu`); + console.log(` 2. Erstelle Grammatik-Übungen für die Grammatik-Lektionen`); + console.log(` 3. Teile die Kurse mit anderen (Share-Codes oben)`); + + return createdCourses; + } catch (error) { + console.error('❌ Fehler beim Erstellen der Kurse:', error); + throw error; + } +} + +// CLI-Aufruf +const ownerHashedId = process.argv[2]; + +if (!ownerHashedId) { + console.error('Verwendung: node create-language-courses.js '); + console.error('Beispiel: node create-language-courses.js abc123def456'); + process.exit(1); +} + +createAllLanguageCourses(ownerHashedId) + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index 1f773ec..bdddc32 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -538,7 +538,7 @@ export default class VocabService { // ========== COURSE METHODS ========== - async createCourse(hashedUserId, { title, description, languageId, difficultyLevel = 1, isPublic = false }) { + async createCourse(hashedUserId, { title, description, languageId, nativeLanguageId, difficultyLevel = 1, isPublic = false }) { const user = await this._getUserByHashedId(hashedUserId); // Prüfe Zugriff auf Sprache @@ -551,6 +551,7 @@ export default class VocabService { title, description, languageId: Number(languageId), + nativeLanguageId: nativeLanguageId ? Number(nativeLanguageId) : null, difficultyLevel: Number(difficultyLevel) || 1, isPublic: Boolean(isPublic), shareCode @@ -559,7 +560,7 @@ export default class VocabService { return course.get({ plain: true }); } - async getCourses(hashedUserId, { includePublic = true, includeOwn = true, languageId, search } = {}) { + async getCourses(hashedUserId, { includePublic = true, includeOwn = true, languageId, nativeLanguageId, search } = {}) { const user = await this._getUserByHashedId(hashedUserId); const where = {}; @@ -579,11 +580,21 @@ export default class VocabService { where.isPublic = true; } - // Filter nach Sprache + // Filter nach Zielsprache (die zu lernende Sprache) if (languageId) { where.languageId = Number(languageId); } + // Filter nach Muttersprache (die Sprache des Lerners) + if (nativeLanguageId !== undefined) { + if (nativeLanguageId === null) { + // NULL bedeutet "für alle Sprachen" - zeige Kurse ohne native_language_id + where.nativeLanguageId = null; + } else { + where.nativeLanguageId = Number(nativeLanguageId); + } + } + // Suche nach Titel oder Beschreibung if (search && search.trim()) { const searchTerm = `%${search.trim()}%`; @@ -698,7 +709,7 @@ export default class VocabService { return courseData; } - async updateCourse(hashedUserId, courseId, { title, description, difficultyLevel, isPublic }) { + async updateCourse(hashedUserId, courseId, { title, description, languageId, nativeLanguageId, difficultyLevel, isPublic }) { const user = await this._getUserByHashedId(hashedUserId); const course = await VocabCourse.findByPk(courseId); @@ -717,6 +728,8 @@ export default class VocabService { const updates = {}; if (title !== undefined) updates.title = title; if (description !== undefined) updates.description = description; + if (languageId !== undefined) updates.languageId = Number(languageId); + if (nativeLanguageId !== undefined) updates.nativeLanguageId = nativeLanguageId ? Number(nativeLanguageId) : null; if (difficultyLevel !== undefined) updates.difficultyLevel = Number(difficultyLevel); if (isPublic !== undefined) { updates.isPublic = Boolean(isPublic); diff --git a/backend/sql/add-native-language-to-courses.sql b/backend/sql/add-native-language-to-courses.sql new file mode 100644 index 0000000..3db82da --- /dev/null +++ b/backend/sql/add-native-language-to-courses.sql @@ -0,0 +1,24 @@ +-- ============================================ +-- Füge native_language_id zu vocab_course hinzu +-- ============================================ +-- Dieses Feld speichert die Muttersprache des Lerners +-- z.B. "Bisaya für Deutschsprachige" -> language_id = Bisaya, native_language_id = Deutsch + +-- Spalte hinzufügen +ALTER TABLE community.vocab_course +ADD COLUMN IF NOT EXISTS native_language_id INTEGER; + +-- Foreign Key Constraint hinzufügen +ALTER TABLE community.vocab_course +ADD CONSTRAINT vocab_course_native_language_fk +FOREIGN KEY (native_language_id) +REFERENCES community.vocab_language(id) +ON DELETE SET NULL; + +-- Index für bessere Performance +CREATE INDEX IF NOT EXISTS vocab_course_native_language_idx +ON community.vocab_course(native_language_id); + +-- Kommentar hinzufügen +COMMENT ON COLUMN community.vocab_course.native_language_id IS +'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".'; diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js index a2ed45b..16d8d18 100644 --- a/backend/utils/syncDatabase.js +++ b/backend/utils/syncDatabase.js @@ -152,6 +152,7 @@ const syncDatabase = async () => { title TEXT NOT NULL, description TEXT, language_id INTEGER NOT NULL, + native_language_id INTEGER, difficulty_level INTEGER DEFAULT 1, is_public BOOLEAN DEFAULT false, share_code TEXT, @@ -165,6 +166,10 @@ const syncDatabase = async () => { FOREIGN KEY (language_id) REFERENCES community.vocab_language(id) ON DELETE CASCADE, + CONSTRAINT vocab_course_native_language_fk + FOREIGN KEY (native_language_id) + REFERENCES community.vocab_language(id) + ON DELETE SET NULL, CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code) ); @@ -239,6 +244,8 @@ const syncDatabase = async () => { ON community.vocab_course(owner_user_id); CREATE INDEX IF NOT EXISTS vocab_course_language_idx ON community.vocab_course(language_id); + CREATE INDEX IF NOT EXISTS vocab_course_native_language_idx + ON community.vocab_course(native_language_id); CREATE INDEX IF NOT EXISTS vocab_course_public_idx ON community.vocab_course(is_public); CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json index 4f77f7f..e1a47db 100644 --- a/frontend/src/i18n/locales/de/socialnetwork.json +++ b/frontend/src/i18n/locales/de/socialnetwork.json @@ -352,6 +352,11 @@ "shareCode": "Share-Code", "searchPlaceholder": "Kurs suchen...", "allLanguages": "Alle Sprachen", + "targetLanguage": "Zielsprache", + "nativeLanguage": "Muttersprache", + "allNativeLanguages": "Alle Muttersprachen", + "forAllLanguages": "Für alle Sprachen", + "optional": "Optional", "invalidCode": "Ungültiger Code", "courseNotFound": "Kurs nicht gefunden" } diff --git a/frontend/src/i18n/locales/en/socialnetwork.json b/frontend/src/i18n/locales/en/socialnetwork.json index 228086c..d581063 100644 --- a/frontend/src/i18n/locales/en/socialnetwork.json +++ b/frontend/src/i18n/locales/en/socialnetwork.json @@ -352,6 +352,11 @@ "shareCode": "Share Code", "searchPlaceholder": "Search courses...", "allLanguages": "All Languages", + "targetLanguage": "Target Language", + "nativeLanguage": "Native Language", + "allNativeLanguages": "All Native Languages", + "forAllLanguages": "For All Languages", + "optional": "Optional", "invalidCode": "Invalid code", "courseNotFound": "Course not found" } diff --git a/frontend/src/views/social/VocabCourseListView.vue b/frontend/src/views/social/VocabCourseListView.vue index adc167d..1956f2e 100644 --- a/frontend/src/views/social/VocabCourseListView.vue +++ b/frontend/src/views/social/VocabCourseListView.vue @@ -20,11 +20,20 @@ />
+
+
+ + +
{{ $t('general.loading') }}
@@ -42,7 +51,8 @@

{{ course.description }}

- {{ $t('socialnetwork.vocab.courses.language') }}: {{ course.languageName }} + {{ $t('socialnetwork.vocab.courses.targetLanguage') }}: {{ course.languageName }} + {{ $t('socialnetwork.vocab.courses.nativeLanguage') }}: {{ course.nativeLanguageName }} {{ $t('socialnetwork.vocab.courses.difficulty') }}: {{ course.difficultyLevel }} {{ $t('socialnetwork.vocab.courses.lessons') }}: {{ course.lessons.length }}
@@ -162,6 +172,9 @@ export default { if (this.selectedLanguageId) { params.languageId = this.selectedLanguageId; } + if (this.selectedNativeLanguageId !== '') { + params.nativeLanguageId = this.selectedNativeLanguageId === 'null' ? null : this.selectedNativeLanguageId; + } if (this.searchTerm.trim()) { params.search = this.searchTerm.trim(); } @@ -230,6 +243,7 @@ export default { title: '', description: '', languageId: null, + nativeLanguageId: null, difficultyLevel: 1, isPublic: false };