{"id":4094,"date":"2026-04-21T19:58:07","date_gmt":"2026-04-21T19:58:07","guid":{"rendered":"https:\/\/sofapiano.com\/?page_id=4094"},"modified":"2026-06-06T09:07:28","modified_gmt":"2026-06-06T09:07:28","slug":"level-test","status":"publish","type":"page","link":"https:\/\/sofapiano.com\/es\/level-test\/","title":{"rendered":"Evaluaci\u00f3n del nivel"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"4094\" class=\"elementor elementor-4094\">\n\t\t\t\t<div class=\"elementor-element elementor-element-18856d0 e-con-full e-flex e-con e-parent\" data-id=\"18856d0\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;none&quot;}\">\n\t\t<div class=\"elementor-element elementor-element-55da29b e-con-full animated-slow e-flex elementor-invisible e-con e-child\" data-id=\"55da29b\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;slideInLeft&quot;,&quot;animation_delay&quot;:500}\">\n\t\t\t\t<div class=\"elementor-element elementor-element-13e8418 elementor-widget elementor-widget-heading\" data-id=\"13e8418\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Level Assessment<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-6fd95ba elementor-widget__width-auto premium-modal-dismissible-yes elementor-widget elementor-widget-premium-addon-modal-box\" data-id=\"6fd95ba\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;premium_modal_box_animation&quot;:&quot;slideInRight&quot;}\" data-widget_type=\"premium-addon-modal-box.default\">\n\t\t\t\t\t\n\t\t<div class=\"premium-modal-box-container\" data-settings=\"{&quot;trigger&quot;:&quot;button&quot;,&quot;show_on_exit&quot;:false}\">\n\t\t\t<div class=\"premium-modal-trigger-container\">\n\t\t\t\t\t\t\t\t\t<button data-toggle=\"premium-modal\" data-target=\"#premium-modal-6fd95ba\" type=\"button\" class=\"premium-modal-trigger-btn premium-btn-lg premium-button-none \" data-text=\"\">\n\n\t\t\t\t\t\t<svg class=\"svg-inline--fas-fa-info-circle premium-svg-nodraw\" aria-hidden=\"true\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewbox=\"0 0 512 512\"><path d=\"M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z\"><\/path><\/svg>\n\t\t\t\t\t\t<div class=\"premium-button-text-icon-wrapper\">\n\t\t\t\t\t\t\t<span><\/span>\n\t\t\t\t\t\t<\/div>\n\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t<\/button>\n\t\t\t\t\t\t\t<\/div>\n\n\t\t\t<div id=\"premium-modal-6fd95ba\" class=\"premium-modal-box-modal\"\n\t\t\trole=\"dialog\"\n\t\t\tstyle=\"display: none\"\n\t\t\t>\n\t\t\t\t<div class=\"premium-modal-box-modal-dialog\" data-delay-animation=\"\" data-modal-animation=\"slideInRight animated-\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"premium-modal-box-modal-header\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"premium-modal-box-close-button-container\">\n\t\t\t\t\t\t\t\t\t<button type=\"button\" class=\"premium-modal-box-modal-close\" data-dismiss=\"premium-modal\">&times;<\/button>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t\t\t\t\t\t<div class=\"premium-modal-box-modal-body\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"container\"><div id=\"model-response-message-contentr_e74f4aa553c38906\" class=\"markdown markdown-main-panel stronger enable-updated-hr-color\" dir=\"ltr\" aria-live=\"polite\" aria-busy=\"false\"><h3 data-path-to-node=\"0\"><b data-path-to-node=\"0\" data-index-in-node=\"0\">German Level Test<\/b><\/h3><p data-path-to-node=\"1\">This test is a structured <b data-path-to-node=\"1\" data-index-in-node=\"26\">Level Assessment<\/b> that measures your German grammar, writing and listening skills.<\/p><ol start=\"1\" data-path-to-node=\"2\"><li><p data-path-to-node=\"2,0,0\"><b data-path-to-node=\"2,0,0\" data-index-in-node=\"0\">Level Selection:<\/b>\u00a0 Choose the level you want to test(A1\u2013B2).\u00a0<\/p><\/li><li><p data-path-to-node=\"2,1,0\"><b data-path-to-node=\"2,1,0\" data-index-in-node=\"0\">The Challenge:<\/b> There will be 10 rounds each with 3 sentences. Translation and listening tasks based on selected level.<\/p><\/li><li><p data-path-to-node=\"2,1,0\"><b data-path-to-node=\"2,1,1,0,0\" data-index-in-node=\"0\">Translation:<\/b><span style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\"> The app provides hints (the first letter of words) to help you structure your response.<\/span><\/p><\/li><li><p data-path-to-node=\"2,2,0\"><b data-path-to-node=\"2,2,0\" data-index-in-node=\"0\">Grading:<\/b>\u00a0 Every typo will be an error! Capitalization, umlauts &amp; punctuation marks are auto-corrected<\/p><\/li><\/ol><p data-path-to-node=\"3\"><b data-path-to-node=\"3\" data-index-in-node=\"0\">The Result:<\/b> You receive a final Score and a Level Recommendation telling you whether to keep practicing or move to the next level.<\/p><\/div><\/div>\n\t\t\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-01bd9ce e-con-full e-flex e-con e-child\" data-id=\"01bd9ce\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-f91e26d elementor-widget elementor-widget-html\" data-id=\"f91e26d\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<div class=\"trainer-app no-sidebar\">\n    <style>\n        @import url('https:\/\/fonts.googleapis.com\/css2?family=Plus+Jakarta+Sans:wght=400;500;600;700;800&display=swap');\n\n        .trainer-app {\n            max-width: 600px;\n            margin: 20px auto;\n            font-family: 'Plus Jakarta Sans', sans-serif;\n            color: #1a1d1e;\n            --brand: #6EC1E4;\n            --bg: transparent; \n            --card-bg: #ffffff;\n            --correct: #27ae60;\n            --missing: #eb4d4b;\n            --tip: #f39c12;\n            --text-main: #2d3436;\n            --border-color: #e6e9ec;\n            --shadow-sm: 0 4px 12px rgba(0,0,0,0.05);\n            background-color: var(--bg);\n            padding: 15px;\n        }\n\n        .trainer-app * { box-sizing: border-box; }\n        .trainer-app .setup-screen { padding: 40px 20px; text-align: center; }\n        .trainer-app .level-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 0 auto 30px auto; max-width: 450px; }\n        .trainer-app .level-card { background: var(--card-bg); border: 2px solid #edf2f7; border-radius: 20px; padding: 20px; cursor: pointer; transition: all 0.3s ease; display: flex; flex-direction: column; align-items: center; }\n        .trainer-app .level-card:hover { border-color: var(--brand); transform: translateY(-3px); }\n        .trainer-app .level-card.active { border-color: var(--brand); background: #f0f9ff; box-shadow: 0 0 0 1px var(--brand); }\n        .trainer-app .lvl-tag { font-size: 24px; font-weight: 800; color: var(--brand); }\n        \n        .trainer-app .exam-header { \n            display: grid; \n            grid-template-columns: 40px 1fr 40px; \n            align-items: center; \n            margin-bottom: 15px; \n            padding: 12px 15px; \n            background: var(--card-bg); \n            border-radius: 16px; \n            box-shadow: var(--shadow-sm); \n            border: 1px solid var(--border-color); \n        }\n\n        .trainer-app .icon-btn { \n            background: #ffffff; \n            border: 1px solid var(--border-color); \n            border-radius: 10px; \n            width: 40px; \n            height: 40px; \n            display: flex; \n            align-items: center; \n            justify-content: center; \n            cursor: pointer; \n            color: #1a1d1e;\n            padding: 0;\n            line-height: 1;\n        }\n        \n        .trainer-app .round-indicator { font-size: 15px; text-transform: uppercase; font-weight: 700; color: var(--brand); letter-spacing: 1px; display: block; text-align: center; margin-bottom: 6px; }\n        .trainer-app .progress-bar { height: 8px; background: #edf2f7; border-radius: 10px; overflow: hidden; width: 160px; margin: 0 auto; }\n        .trainer-app .progress-fill { height: 100%; background: var(--brand); width: 0%; transition: width 0.6s ease; }\n        \n        .trainer-app .sentence-block { position: relative; margin-bottom: 12px; padding: 20px; border: 1px solid var(--border-color); border-radius: 20px; background: var(--card-bg); box-shadow: var(--shadow-sm); }\n        .trainer-app .sentence-block.is-confirmed { opacity: 0.6; }\n        .trainer-app .source { font-size: 17px; font-weight: 700; color: var(--text-main); margin-bottom: 18px; text-align: center; line-height: 1.4; }\n        \n        .trainer-app textarea { flex: 1; min-height: 50px; height: 50px; font-size: 16px; font-weight: 600; padding: 12px 15px; border: 2px solid #edf2f7; border-radius: 14px; background: #fcfcfc; font-family: inherit; resize: none; overflow: hidden; }\n        .trainer-app textarea:focus { outline: none; border-color: var(--brand); background: #fff; }\n        \n        .trainer-app .btn-confirm-sentence { height: 50px; width: 50px; min-width: 50px; border-radius: 14px; border: none; background: var(--brand); color: white !important; font-size: 22px; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0.5; transition: 0.3s; }\n        .trainer-app .btn-confirm-sentence.active { opacity: 1; transform: scale(1.05); }\n        .trainer-app .btn-confirm-sentence.confirmed { background: var(--correct) !important; opacity: 1; }\n        \n        .trainer-app .btn-audio-center { background: #ffffff; border: 2px solid #1a1d1e; color: #1a1d1e; border-radius: 30%; width: 50px; height: 50px; cursor: pointer; display: flex; align-items: center; justify-content: center; }\n        .trainer-app .letter-hints { text-align: center; font-size: 12px; color: #94a3b8; margin-top: 12px; font-weight: 700; letter-spacing: 1px; }\n        .trainer-app .btn-nav { background: var(--brand); color: white !important; padding: 16px 45px; border: none; border-radius: 16px; font-weight: 700; cursor: pointer; font-size: 16px; text-transform: uppercase; }\n        \n        .trainer-app .score-display { font-size: 32px; font-weight: 800; color: var(--brand); text-align: center; margin: 30px 0; }\n        .trainer-app .status-icon { position: absolute; top: 15px; right: 20px; font-size: 24px; }\n        .trainer-app .feedback span.correct { color: var(--correct); font-weight: 700; }\n        .trainer-app .feedback span.tip { color: var(--tip); font-weight: 700; border-bottom: 2px dashed var(--tip); }\n        .trainer-app .feedback span.missing { color: var(--missing); font-weight: 700; }\n    <\/style>\n\n    <div class=\"setup-screen\">\n        <div class=\"level-grid\"><\/div>\n        <button class=\"btn-nav start-btn\">START<\/button>\n    <\/div>\n\n    <div class=\"main-app\" style=\"display: none;\">\n        <div class=\"exam-header\">\n            <div><\/div>\n            <div class=\"header-center\">\n                <span class=\"round-indicator\">Lade...<\/span>\n                <div class=\"progress-bar\"><div class=\"progress-fill\"><\/div><\/div>\n            <\/div>\n            <button class=\"icon-btn flag-icon\"><span class=\"flag\" style=\"font-size: 20px;\">\ud83c\uddea\ud83c\uddf8<\/span><\/button>\n        <\/div>\n        <div class=\"sentence-area\"><\/div>\n        <div class=\"control-area\" style=\"text-align: center; margin-top: 15px;\">\n            <button class=\"btn-nav next-btn\" disabled>N\u00e4chste Runde \u279c<\/button>\n        <\/div>\n    <\/div>\n\n    <script>\n    (function(){\n        const scripts = document.getElementsByTagName('script');\n        const currentScript = scripts[scripts.length - 1];\n        const container = currentScript.closest('.trainer-app');\n\n        const LEVEL_CONFIG = {\n            \"A1\": { topics: [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\",\"9\", \"10\"], voiceRate: 0.85 },\n            \"A2\": { topics: [\"11\", \"12\", \"13\", \"14\", \"15\", \"16\", \"17\", \"18\", \"19\", \"20\"], voiceRate: 0.95 },\n            \"B1\": { topics: [\"21\", \"22\", \"23\", \"24\", \"25\", \"26\", \"27\", \"28\", \"29\", \"30\"], voiceRate: 1.05 },\n            \"B2\": { topics: [\"31\", \"32\", \"33\", \"34\", \"35\", \"36\", \"37\", \"38\", \"39\", \"40\"], voiceRate: 1.15 }\n        };\n\n        const CSV_URL = `https:\/\/docs.google.com\/spreadsheets\/d\/1jV4htReTIOu9qlcb_B-E9kDSU6o-igJLvfDfiChBucU\/export?format=csv&gid=839931735`;\n        \n        let currentLevel = \"A1\";\n        let selectedTopicIds = [];\n        let currentLang = \"en\"; \n        let allData = [];\n        let currentTopicIndex = 0;\n        let examResults = []; \n        let confirmedCount = 0;\n        let globalQuestionCounter = 0;\n        let totalAccumulatedPoints = 0;\n\n        const translations = {\n            en: { \n                round: \"ROUND\", next: \"Next \u279c\", eval: \"Evaluation \ud83d\udcca\", score: \"SCORE\", flag: \"\ud83c\uddfa\ud83c\uddf8\", nextLang: \"es\",\n                feedbackHigh: \"Excellent! You are ready for the next level.\", \n                feedbackLow: \"Keep practicing! You should stay on this level a bit longer.\", \n                recommendation: \"Recommendation\"\n            },\n            es: { \n                round: \"RONDA\", next: \"Siguiente \u279c\", eval: \"Evaluaci\u00f3n \ud83d\udcca\", score: \"PUNTUACI\u00d3N\", flag: \"\ud83c\uddea\ud83c\uddf8\", nextLang: \"en\",\n                feedbackHigh: \"\u00a1Excelente! Est\u00e1s listo para demas.\", \n                feedbackLow: \"\u00a1Sigue practicando! Deber\u00edas quedarte en este nivel un poco m\u00e1s.\", \n                recommendation: \"Recomendaci\u00f3n\"\n            }\n        };\n\n        const normalizeClean = (t) => {\n            if(!t) return \"\";\n            return t.toLowerCase().trim()\n                    .replace(\/[.,!?]\/g, '')\n                    .replace(\/\u00e4\/g,'ae').replace(\/\u00f6\/g,'oe').replace(\/\u00fc\/g,'ue').replace(\/\u00df\/g,'ss')\n                    .replace(\/\\s+\/g, ' ');\n        };\n\n        const getLevenshteinDistance = (a, b) => {\n            const matrix = [];\n            for (let i = 0; i <= b.length; i++) matrix[i] = [i];\n            for (let j = 0; j <= a.length; j++) matrix[0][j] = j;\n            for (let i = 1; i <= b.length; i++) {\n                for (let j = 1; j <= a.length; j++) {\n                    if (b.charAt(i - 1) === a.charAt(j - 1)) matrix[i][j] = matrix[i - 1][j - 1];\n                    else matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);\n                }\n            }\n            return matrix[b.length][a.length];\n        };\n\n        const grid = container.querySelector('.level-grid');\n        Object.keys(LEVEL_CONFIG).forEach((lvl, idx) => {\n            const card = document.createElement('div');\n            card.className = `level-card ${idx === 0 ? 'active' : ''}`;\n            card.dataset.level = lvl;\n            card.innerHTML = `<span class=\"lvl-tag\">${lvl}<\/span>`;\n            card.onclick = () => {\n                container.querySelectorAll('.level-card').forEach(c => c.classList.remove('active'));\n                card.classList.add('active');\n                currentLevel = lvl;\n            };\n            grid.appendChild(card);\n        });\n\n        async function startTraining() {\n            selectedTopicIds = LEVEL_CONFIG[currentLevel].topics;\n            try {\n                sendLogStart(currentLevel);\n\n                const res = await fetch(CSV_URL);\n                const text = await res.text();\n                const rows = text.split(\/\\r?\\n\/).slice(1);\n                allData = rows.map(row => {\n                    const cols = row.split(\/,(?=(?:(?:[^\"]*\"){2})*[^\"]*$)\/);\n                    const clean = (v) => v ? v.trim().replace(\/^\"|\"$\/g, '').replace(\/\"\"\/g, '\"') : \"\";\n                    return { level: clean(cols[0]), topicId: clean(cols[1]), de: clean(cols[2]), en: clean(cols[3]), es: clean(cols[4]) };\n                }).filter(r => r.de && r.level === currentLevel);\n\n                if(allData.length > 0) {\n                    container.querySelector('.setup-screen').style.display = 'none';\n                    container.querySelector('.main-app').style.display = 'block';\n                    showNextRound();\n                }\n            } catch (e) { console.error(e); }\n        }\n\n        function showNextRound() {\n            const area = container.querySelector('.sentence-area');\n            const nextBtn = container.querySelector('.next-btn');\n            confirmedCount = 0; nextBtn.disabled = true;\n            \n            const topicId = selectedTopicIds[currentTopicIndex];\n            const pool = allData.filter(d => d.topicId === topicId);\n            const roundData = pool.sort(() => 0.5 - Math.random()).slice(0, 3);\n            \n            currentTopicIndex++;\n            area.innerHTML = \"\";\n            container.querySelector('.progress-fill').style.width = (((currentTopicIndex-1) \/ selectedTopicIds.length) * 100) + \"%\";\n            roundData.forEach((data, index) => { area.appendChild(createBlock(data, index === 2)); });\n            updateUILanguage();\n        }\n\n        function evaluateSentence(correctStr, inputVal) {\n            const targetWords = correctStr.split(\/\\s+\/);\n            const userWordsClean = inputVal.split(\/\\s+\/).map(w => normalizeClean(w));\n            \n            let sentenceScore = 5;\n            let usedUserIndices = new Set();\n            const spansData = [];\n\n            targetWords.forEach((word) => {\n                const cleanT = normalizeClean(word);\n                let foundIndex = -1;\n                let isLight = false;\n\n                for(let i = 0; i < userWordsClean.length; i++) {\n                    if(usedUserIndices.has(i)) continue;\n                    if(userWordsClean[i] === cleanT) {\n                        foundIndex = i; isLight = false; break;\n                    }\n                    const dist = getLevenshteinDistance(cleanT, userWordsClean[i]);\n                    if(dist === 1 && cleanT.length > 3) {\n                        foundIndex = i; isLight = true; break;\n                    }\n                }\n\n                if(foundIndex !== -1) {\n                    usedUserIndices.add(foundIndex);\n                    spansData.push({ text: word + \" \", className: isLight ? \"tip\" : \"correct\" });\n                    if(isLight) sentenceScore -= 1;\n                } else {\n                    spansData.push({ text: word + \" \", className: \"missing\" });\n                    sentenceScore -= 2;\n                }\n            });\n\n            return {\n                score: Math.max(0, sentenceScore),\n                spans: spansData\n            };\n        }\n\n        function createBlock(data, isAudio) {\n            globalQuestionCounter++;\n            const thisQuestionNum = globalQuestionCounter;\n\n            const block = document.createElement(\"div\");\n            block.className = \"sentence-block\";\n            block.dataset.correctSentence = data.de;\n            block.dataset.isAudioTask = isAudio;\n            block.dataset.enSource = data.en;\n            block.dataset.esSource = data.es;\n            block.dataset.questionNum = thisQuestionNum;\n            \n            const hints = isAudio ? \"\" : data.de.split(\/\\s+\/).map((w, i) => (i % 2 === 0) ? (w[0] || \"\") + \"...\" : \"...\").join(\" \");\n\n            block.innerHTML = `\n                <div style=\"display:${isAudio ? 'flex' : 'none'}; justify-content:center; gap:12px; margin-bottom:15px;\">\n                    <button class=\"btn-audio-center\">\ud83d\udd0a<\/button>\n                    <span class=\"audio-count\" style=\"font-size:13px; font-weight:800;\">2 \/ 2<\/span>\n                <\/div>\n                <div class=\"source\">${isAudio ? '' : (currentLang === \"en\" ? data.en : data.es)}<\/div>\n                <div class=\"input-group\" style=\"display:flex; gap:10px;\">\n                    <textarea rows=\"1\"><\/textarea>\n                    <button class=\"btn-confirm-sentence\">\u2713<\/button>\n                <\/div>\n                <div class=\"letter-hints\">${hints}<\/div>\n                <div class=\"feedback\" style=\"display:none; margin-top:15px; text-align:center; padding:10px; background:#f8fafb; border-radius:10px;\"><\/div>\n            `;\n\n            const btnConf = block.querySelector(\".btn-confirm-sentence\");\n            const input = block.querySelector(\"textarea\");\n            const btnAudio = block.querySelector(\".btn-audio-center\");\n\n            input.oninput = function() {\n                if (this.value.trim().length > 0) btnConf.classList.add('active');\n            };\n\n            let playCount = 0;\n            btnAudio.onclick = () => {\n                if(playCount < 2) {\n                    const u = new SpeechSynthesisUtterance(data.de);\n                    u.lang = \"de-DE\"; u.rate = LEVEL_CONFIG[currentLevel].voiceRate;\n                    speechSynthesis.speak(u);\n                    playCount++; \n                    block.querySelector(\".audio-count\").textContent = `${2-playCount} \/ 2`;\n                    if(playCount >= 2) btnAudio.disabled = true;\n                }\n            };\n            \n            btnConf.onclick = () => {\n                if(btnConf.classList.contains(\"confirmed\") || input.value.trim() === \"\") return;\n                btnConf.classList.add(\"confirmed\"); block.classList.add(\"is-confirmed\"); input.disabled = true;\n                \n                const inputVal = input.value;\n                block.dataset.userRawInput = inputVal;\n                \n                const evaluation = evaluateSentence(data.de, inputVal);\n                block.dataset.calculatedScore = evaluation.score;\n                totalAccumulatedPoints += evaluation.score;\n\n                const clone = block.cloneNode(true);\n                clone.querySelector('textarea').value = inputVal;\n                examResults.push(clone); \n\n                \/\/ Einzelne Frage wird direkt an das neue Apps Script geschickt\n                sendSingleQuestionToGoogleSheets(currentLevel, data, isAudio, inputVal, evaluation.score);\n\n                \/\/ Wenn dies die absolut letzte Frage des Tests war, starten wir den 15-Sekunden-Timer f\u00fcr die Auswertung\n                const isLastQuestionOfTest = (currentTopicIndex >= selectedTopicIds.length && confirmedCount === 2);\n                if (isLastQuestionOfTest) {\n                    console.log(\"Letzte Frage beantwortet. Starte 15-Sekunden-Puffer f\u00fcr die Test-Auswertung...\");\n                    setTimeout(() => {\n                        const maxPoints = examResults.length * 5;\n                        sendLogEvaluation(currentLevel, totalAccumulatedPoints, maxPoints);\n                    }, 15000);\n                }\n                \n                confirmedCount++;\n                if(confirmedCount === 3) {\n                    container.querySelector('.next-btn').disabled = false;\n                    container.querySelector('.next-btn').onclick = (currentTopicIndex >= selectedTopicIds.length) ? finalEvaluation : showNextRound;\n                }\n            };\n            return block;\n        }\n\n        function finalEvaluation() {\n            const t = translations[currentLang];\n            const area = container.querySelector('.sentence-area');\n            area.innerHTML = \"\";\n            container.querySelector('.next-btn').style.display = \"none\";\n\n            examResults.forEach((block) => {\n                const inputVal = block.dataset.userRawInput || \"\";\n                const correctStr = block.dataset.correctSentence;\n                const fb = block.querySelector(\".feedback\");\n                const sentenceScore = parseInt(block.dataset.calculatedScore || \"0\");\n                \n                fb.innerHTML = \"\";\n\n                const evaluation = evaluateSentence(correctStr, inputVal);\n                evaluation.spans.forEach(spanInfo => {\n                    const span = document.createElement(\"span\");\n                    span.textContent = spanInfo.text;\n                    span.className = spanInfo.className;\n                    fb.appendChild(span);\n                });\n\n                const status = document.createElement(\"div\");\n                status.className = \"status-icon\";\n                status.innerHTML = sentenceScore === 5 ? \"\u2714\ufe0f\" : (sentenceScore >= 3 ? \"\u26a0\ufe0f\" : \"\u274c\");\n                block.appendChild(status);\n                block.querySelector(\".btn-confirm-sentence\").style.display = \"none\";\n                block.querySelector(\".letter-hints\").style.display = \"none\";\n                fb.style.display = \"block\";\n                area.appendChild(block);\n            });\n\n            const maxPoints = examResults.length * 5;\n            const percentage = totalAccumulatedPoints \/ maxPoints;\n\n            const scoreEl = document.createElement(\"div\");\n            scoreEl.className = \"score-display\";\n            scoreEl.innerHTML = `\n                <div style=\"font-size:14px; color:#718096; text-transform:uppercase;\">${t.score}<\/div>\n                ${totalAccumulatedPoints} \/ ${maxPoints}\n                <div class=\"recommendation-box\" style=\"margin-top:20px; padding:20px; background:white; border-radius:16px; border:1px solid var(--border-color); font-size:15px; font-weight:500; color:var(--text-main); line-height:1.5; text-align:center;\">\n                    <div style=\"color:var(--brand); font-weight:800; margin-bottom:8px; font-size:12px; text-transform:uppercase;\">${t.recommendation}<\/div>\n                    <span class=\"recommendation-text\">${percentage >= 0.8 ? t.feedbackHigh : t.feedbackLow}<\/span>\n                <\/div>\n            `;\n            area.prepend(scoreEl);\n            window.scrollTo({ top: container.offsetTop, behavior: 'smooth' });\n        }\n\n        function getUserIdentity() {\n            let userId = \"Gast\";\n            let userName = \"Anonym\";\n            try {\n                if (window.wpTrainerUserInfo) {\n                    if (typeof window.wpTrainerUserInfo === 'object') {\n                        userId = window.wpTrainerUserInfo.id || window.wpTrainerUserInfo.user_id || \"Eingeloggt\";\n                        userName = window.wpTrainerUserInfo.name || window.wpTrainerUserInfo.user_name || window.wpTrainerUserInfo.display_name || \"Mitglied\";\n                    } else if (typeof window.wpTrainerUserInfo === 'string') {\n                        const parsed = JSON.parse(window.wpTrainerUserInfo);\n                        userId = parsed.id || parsed.user_id || \"Eingeloggt\";\n                        userName = parsed.name || parsed.user_name || \"Mitglied\";\n                    }\n                }\n            } catch (e) { console.log(\"Fehler beim Sichern der WP-Userdaten:\", e); }\n\n            if (userName === \"Anonym\") {\n                const wpAdminBarUser = document.querySelector('#wp-admin-bar-my-account .avatar + span') || \n                                      document.querySelector('#wp-admin-bar-my-account .display-name');\n                if (wpAdminBarUser) {\n                    userName = wpAdminBarUser.textContent.trim();\n                    userId = userName;\n                }\n            }\n            return { userId, userName };\n        }\n\n        function sendLogStart(level) {\n            const identity = getUserIdentity();\n            const payload = {\n                user_id: String(identity.userId),\n                user_name: String(identity.userName),\n                sentence_id: \"N\/A\",\n                level: String(level),\n                topic: \"-\",\n                german: \"-\",\n                translation: \"-\",\n                action: \"Test-Start\",\n                status: \"-\",\n                user_input: \"-\"\n            };\n            executeJsonSubmit(payload);\n        }\n\n        function sendLogEvaluation(level, totalPoints, maxPoints) {\n            const identity = getUserIdentity();\n            const payload = {\n                user_id: String(identity.userId),\n                user_name: String(identity.userName),\n                sentence_id: \"N\/A\",\n                level: String(level),\n                topic: \"-\",\n                german: \"Maximal erreichbar: \" + maxPoints,\n                translation: \"-\",\n                action: \"Test-Auswertung\",\n                status: totalPoints + \" \/ \" + maxPoints,\n                user_input: \"Endergebnis des Users\"\n            };\n            executeJsonSubmit(payload);\n        }\n\n        function sendSingleQuestionToGoogleSheets(level, data, isAudio, inputVal, score) {\n            const identity = getUserIdentity();\n            const payload = {\n                user_id: String(identity.userId),\n                user_name: String(identity.userName),\n                sentence_id: \"N\/A\", \/\/ Da in den CSV-Spalten aktuell keine separate Satz-ID ausgelesen wird\n                level: String(level),\n                topic: String(data.topicId || \"\"),\n                german: String(data.de || \"\"),\n                translation: String(currentLang === \"en\" ? data.en : data.es),\n                action: isAudio ? \"H\u00f6r\u00fcbung (Audio)\" : \"\u00dcbersetzung (Text)\",\n                status: score + \" \/ 5\",\n                user_input: String(inputVal)\n            };\n            executeJsonSubmit(payload);\n        }\n\n        function executeJsonSubmit(payload) {\n            const targetUrl = 'https:\/\/script.google.com\/macros\/s\/AKfycbz7ViqxDaSi1l2M0o4rsPHqS09qmgosQy34eIJmBhfd1hs2SBbRAuc3nrJC4D4KfBoPPQ\/exec';\n            try {\n                fetch(targetUrl, {\n                    method: 'POST',\n                    mode: 'no-cors',\n                    headers: {\n                        'Content-Type': 'application\/json'\n                    },\n                    body: JSON.stringify(payload)\n                });\n                console.log(`Log f\u00fcr '${payload.action}' erfolgreich abgesetzt.`);\n            } catch (err) {\n                console.error('Fehler beim Absenden des JSON-Logs:', err);\n            }\n        }\n\n        function updateUILanguage() {\n            const t = translations[currentLang];\n            \n            container.querySelector('.flag-icon .flag').textContent = translations[t.nextLang].flag;\n            container.querySelector('.round-indicator').innerHTML = `${t.round} ${currentTopicIndex} \/ ${selectedTopicIds.length}`;\n            \n            const nextBtn = container.querySelector('.next-btn');\n            if(nextBtn) {\n                nextBtn.textContent = (currentTopicIndex >= selectedTopicIds.length) ? t.eval : t.next;\n            }\n\n            container.querySelectorAll('.sentence-block').forEach(block => {\n                const sourceEl = block.querySelector('.source');\n                if(sourceEl && block.dataset.isAudioTask === \"false\") {\n                    sourceEl.textContent = (currentLang === \"en\") ? block.dataset.enSource : block.dataset.esSource;\n                }\n            });\n\n            const recBox = container.querySelector('.recommendation-box');\n            if(recBox) {\n                recBox.querySelector('div').textContent = t.recommendation;\n                const max = examResults.length * 5;\n                recBox.querySelector('.recommendation-text').textContent = (totalAccumulatedPoints\/max >= 0.8) ? t.feedbackHigh : t.feedbackLow;\n            }\n        }\n\n        container.querySelector('.start-btn').onclick = startTraining;\n        container.querySelector('.flag-icon').onclick = () => { \n            currentLang = translations[currentLang].nextLang; \n            updateUILanguage(); \n        };\n    })();\n    <\/script>\n<\/div>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Level Assessment &times; German Level Test This test is a structured Level Assessment that measures your German grammar, writing and listening skills. Level Selection:\u00a0 Choose the level you want to test(A1\u2013B2).\u00a0 The Challenge: There will be 10 rounds each with 3 sentences. Translation and listening tasks based on selected level. Translation: The app provides hints [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-4094","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Level Assessment - sofapiano<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/sofapiano.com\/es\/level-test\/\" \/>\n<meta property=\"og:locale\" content=\"es_ES\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Level Assessment - sofapiano\" \/>\n<meta property=\"og:description\" content=\"Level Assessment &times; German Level Test This test is a structured Level Assessment that measures your German grammar, writing and listening skills. Level Selection:\u00a0 Choose the level you want to test(A1\u2013B2).\u00a0 The Challenge: There will be 10 rounds each with 3 sentences. Translation and listening tasks based on selected level. Translation: The app provides hints [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/sofapiano.com\/es\/level-test\/\" \/>\n<meta property=\"og:site_name\" content=\"sofapiano\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-06T09:07:28+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Tiempo de lectura\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minuto\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/sofapiano.com\\\/level-test\\\/\",\"url\":\"https:\\\/\\\/sofapiano.com\\\/level-test\\\/\",\"name\":\"Level Assessment - sofapiano\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/sofapiano.com\\\/#website\"},\"datePublished\":\"2026-04-21T19:58:07+00:00\",\"dateModified\":\"2026-06-06T09:07:28+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/sofapiano.com\\\/level-test\\\/#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/sofapiano.com\\\/level-test\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/sofapiano.com\\\/level-test\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Start\",\"item\":\"https:\\\/\\\/sofapiano.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Level Assessment\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/sofapiano.com\\\/#website\",\"url\":\"https:\\\/\\\/sofapiano.com\\\/\",\"name\":\"sofapiano\",\"description\":\"Learn German through translation\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/sofapiano.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Level Assessment - sofapiano","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/sofapiano.com\/es\/level-test\/","og_locale":"es_ES","og_type":"article","og_title":"Level Assessment - sofapiano","og_description":"Level Assessment &times; German Level Test This test is a structured Level Assessment that measures your German grammar, writing and listening skills. Level Selection:\u00a0 Choose the level you want to test(A1\u2013B2).\u00a0 The Challenge: There will be 10 rounds each with 3 sentences. Translation and listening tasks based on selected level. Translation: The app provides hints [&hellip;]","og_url":"https:\/\/sofapiano.com\/es\/level-test\/","og_site_name":"sofapiano","article_modified_time":"2026-06-06T09:07:28+00:00","twitter_card":"summary_large_image","twitter_misc":{"Tiempo de lectura":"1 minuto"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/sofapiano.com\/level-test\/","url":"https:\/\/sofapiano.com\/level-test\/","name":"Level Assessment - sofapiano","isPartOf":{"@id":"https:\/\/sofapiano.com\/#website"},"datePublished":"2026-04-21T19:58:07+00:00","dateModified":"2026-06-06T09:07:28+00:00","breadcrumb":{"@id":"https:\/\/sofapiano.com\/level-test\/#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/sofapiano.com\/level-test\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/sofapiano.com\/level-test\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Start","item":"https:\/\/sofapiano.com\/"},{"@type":"ListItem","position":2,"name":"Level Assessment"}]},{"@type":"WebSite","@id":"https:\/\/sofapiano.com\/#website","url":"https:\/\/sofapiano.com\/","name":"sofapiano","description":"Aprende alem\u00e1n a trav\u00e9s de la traducci\u00f3n","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/sofapiano.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"}]}},"_links":{"self":[{"href":"https:\/\/sofapiano.com\/es\/wp-json\/wp\/v2\/pages\/4094","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sofapiano.com\/es\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/sofapiano.com\/es\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/sofapiano.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/sofapiano.com\/es\/wp-json\/wp\/v2\/comments?post=4094"}],"version-history":[{"count":15,"href":"https:\/\/sofapiano.com\/es\/wp-json\/wp\/v2\/pages\/4094\/revisions"}],"predecessor-version":[{"id":16384,"href":"https:\/\/sofapiano.com\/es\/wp-json\/wp\/v2\/pages\/4094\/revisions\/16384"}],"wp:attachment":[{"href":"https:\/\/sofapiano.com\/es\/wp-json\/wp\/v2\/media?parent=4094"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}