From 973fcaaf9ec89a4e7138abbeab91b28b4b3c7fbe Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sat, 6 Jun 2026 13:16:04 +0200 Subject: [PATCH] Bisaya kurs korrekturen --- .../repair-bisaya-number-answer-pairs.js | 302 ++++++++++++++++++ .../socialnetwork/VocabPracticeDialog.vue | 20 +- frontend/src/utils/numberAnswerVariants.js | 110 +++++++ .../views/social/VocabLessonReviewView.vue | 10 +- frontend/src/views/social/VocabLessonView.vue | 20 +- 5 files changed, 443 insertions(+), 19 deletions(-) create mode 100644 backend/scripts/repair-bisaya-number-answer-pairs.js create mode 100644 frontend/src/utils/numberAnswerVariants.js diff --git a/backend/scripts/repair-bisaya-number-answer-pairs.js b/backend/scripts/repair-bisaya-number-answer-pairs.js new file mode 100644 index 0000000..d348431 --- /dev/null +++ b/backend/scripts/repair-bisaya-number-answer-pairs.js @@ -0,0 +1,302 @@ +#!/usr/bin/env node +/** + * Repairs stale Bisaya number pairs where the Bisaya side is correct, but the + * German answer side contains a currency phrase such as "20 peso". + * + * Default: dry-run. Use --apply to write changes. + */ + +import crypto from 'crypto'; +import { Op } from 'sequelize'; +import { sequelize } from '../utils/sequelize.js'; +import VocabCourseLesson from '../models/community/vocab_course_lesson.js'; +import VocabSrsItem from '../models/community/vocab_srs_item.js'; + +const BISAYA_TO_GERMAN_DIGIT = new Map([ + ['usa', '1'], + ['duha', '2'], + ['tulo', '3'], + ['upat', '4'], + ['lima', '5'], + ['unom', '6'], + ['pito', '7'], + ['walo', '8'], + ['siyam', '9'], + ['napulo', '10'], + ['onse', '11'], + ['dose', '12'], + ['trese', '13'], + ['katorse', '14'], + ['kinse', '15'], + ['disisais', '16'], + ['disisiete', '17'], + ['disiotso', '18'], + ['disinuybe', '19'], + ['baynte', '20'], + ['kawhaan', '20'], +]); + +const CURRENCY_WORDS = new Set(['peso', 'pesos', 'piso', 'pisos']); +const GERMAN_NUMBER_TO_DIGIT = new Map([ + ['ein', '1'], + ['eins', '1'], + ['zwei', '2'], + ['drei', '3'], + ['vier', '4'], + ['fuenf', '5'], + ['funf', '5'], + ['fünf', '5'], + ['sechs', '6'], + ['sieben', '7'], + ['acht', '8'], + ['neun', '9'], + ['zehn', '10'], + ['elf', '11'], + ['zwoelf', '12'], + ['zwolf', '12'], + ['zwölf', '12'], + ['dreizehn', '13'], + ['vierzehn', '14'], + ['fuenfzehn', '15'], + ['funfzehn', '15'], + ['fünfzehn', '15'], + ['sechzehn', '16'], + ['siebzehn', '17'], + ['achtzehn', '18'], + ['neunzehn', '19'], + ['zwanzig', '20'], +]); + +function parseArgs(argv) { + return { + apply: argv.includes('--apply'), + }; +} + +function normalizeText(value) { + return String(value || '') + .trim() + .toLowerCase() + .normalize('NFKC') + .replace(/[\p{P}\p{S}]+/gu, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function compact(value) { + return normalizeText(value).replace(/\s+/g, ''); +} + +function getBisayaDigit(value) { + return BISAYA_TO_GERMAN_DIGIT.get(compact(value)) || ''; +} + +function getCurrencyNumber(value) { + const text = normalizeText(value); + if (!text) return ''; + const parts = text.split(/\s+/).filter(Boolean); + if (parts.length < 2) return ''; + const last = parts[parts.length - 1]; + if (!CURRENCY_WORDS.has(last)) return ''; + const numberPart = parts.slice(0, -1).join(''); + if (/^\d+$/.test(numberPart)) return String(Number(numberPart)); + return GERMAN_NUMBER_TO_DIGIT.get(numberPart) || ''; +} + +function fixPair(left, right) { + const leftBisayaDigit = getBisayaDigit(left); + const rightCurrencyDigit = getCurrencyNumber(right); + if (leftBisayaDigit && rightCurrencyDigit && leftBisayaDigit === rightCurrencyDigit) { + return { left, right: leftBisayaDigit, changed: right !== leftBisayaDigit }; + } + + const rightBisayaDigit = getBisayaDigit(right); + const leftCurrencyDigit = getCurrencyNumber(left); + if (rightBisayaDigit && leftCurrencyDigit && rightBisayaDigit === leftCurrencyDigit) { + return { left: rightBisayaDigit, right, changed: left !== rightBisayaDigit }; + } + + return { left, right, changed: false }; +} + +function patchPattern(entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) { + return { value: entry, changed: false }; + } + + const next = { ...entry }; + let changed = false; + const pairKeys = [ + ['target', 'gloss'], + ['learning', 'reference'], + ['bisaya', 'native'], + ]; + + pairKeys.forEach(([leftKey, rightKey]) => { + if (!(leftKey in next) || !(rightKey in next)) return; + const pair = fixPair(next[leftKey], next[rightKey]); + if (!pair.changed) return; + next[leftKey] = pair.left; + next[rightKey] = pair.right; + changed = true; + }); + + return { value: next, changed }; +} + +function patchJson(value) { + if (Array.isArray(value)) { + let changed = false; + const next = value.map((entry) => { + const patched = patchJson(entry); + changed = changed || patched.changed; + return patched.value; + }); + return { value: next, changed }; + } + + if (value && typeof value === 'object') { + const direct = patchPattern(value); + let next = direct.value; + let changed = direct.changed; + + Object.entries(next).forEach(([key, child]) => { + if (!child || typeof child !== 'object') return; + const patched = patchJson(child); + if (!patched.changed) return; + next = { ...next, [key]: patched.value }; + changed = true; + }); + + return { value: next, changed }; + } + + return { value, changed: false }; +} + +function normalizeSrsText(value) { + return normalizeText(value); +} + +function buildSrsItemKey({ courseId, lessonId = null, learning, reference, direction = 'BOTH' }) { + const raw = [ + Number(courseId) || 0, + lessonId == null ? 'course' : Number(lessonId) || 0, + String(direction || 'BOTH').toUpperCase(), + normalizeSrsText(learning), + normalizeSrsText(reference), + ].join('|'); + return crypto.createHash('sha1').update(raw).digest('hex'); +} + +async function repairLessons({ apply }) { + const lessons = await VocabCourseLesson.findAll({ + where: { + corePatterns: { + [Op.ne]: null, + }, + }, + attributes: ['id', 'courseId', 'lessonNumber', 'title', 'corePatterns'], + order: [['courseId', 'ASC'], ['lessonNumber', 'ASC']], + }); + + let changedLessons = 0; + for (const lesson of lessons) { + const patched = patchJson(lesson.corePatterns); + if (!patched.changed) continue; + + changedLessons += 1; + console.log( + `Lesson ${lesson.id} (course ${lesson.courseId}, #${lesson.lessonNumber}, ${lesson.title}): corePatterns korrigiert` + ); + + if (apply) { + await lesson.update({ corePatterns: patched.value }); + } + } + + return changedLessons; +} + +async function repairSrsItems({ apply }) { + const items = await VocabSrsItem.findAll({ + where: { + [Op.or]: [ + { learning: { [Op.iLike]: '%peso%' } }, + { reference: { [Op.iLike]: '%peso%' } }, + { learning: { [Op.iLike]: '%piso%' } }, + { reference: { [Op.iLike]: '%piso%' } }, + ], + }, + order: [['id', 'ASC']], + }); + + let changedItems = 0; + let keyConflicts = 0; + for (const item of items) { + const pair = fixPair(item.learning, item.reference); + if (!pair.changed) continue; + + changedItems += 1; + const nextKey = buildSrsItemKey({ + courseId: item.courseId, + lessonId: item.lessonId, + learning: pair.left, + reference: pair.right, + direction: item.direction, + }); + + console.log( + `SRS ${item.id}: "${item.learning}" | "${item.reference}" -> "${pair.left}" | "${pair.right}"` + ); + + if (!apply) continue; + + try { + await item.update({ + learning: pair.left, + reference: pair.right, + itemKey: nextKey, + }); + } catch (error) { + keyConflicts += 1; + console.warn(` item_key-Konflikt bei SRS ${item.id}; aktualisiere Textwerte mit altem Key.`); + await item.update({ + learning: pair.left, + reference: pair.right, + }); + } + } + + return { changedItems, keyConflicts }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + console.log(`Modus: ${args.apply ? 'APPLY (schreibt Änderungen)' : 'DRY-RUN (keine Änderungen)'}`); + await sequelize.authenticate(); + + const changedLessons = await repairLessons(args); + const { changedItems, keyConflicts } = await repairSrsItems(args); + + console.log(''); + console.log(`Betroffene Lektionen: ${changedLessons}`); + console.log(`Betroffene SRS-Items: ${changedItems}`); + if (keyConflicts > 0) { + console.log(`SRS-Items mit behaltenem altem item_key wegen Unique-Konflikt: ${keyConflicts}`); + } + if (!args.apply) { + console.log('Zum Anwenden erneut mit --apply ausführen.'); + } +} + +main() + .then(async () => { + await sequelize.close(); + }) + .catch(async (error) => { + console.error('Fehler beim Reparieren der Bisaya-Zahlenpaare:', error); + await sequelize.close(); + process.exit(1); + }); diff --git a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue index ac4a3bb..e02ba85 100644 --- a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue +++ b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue @@ -156,6 +156,7 @@