i18n(quiz): translate EditView via $_() — header, meta inputs, question list, new-question form

- Header: back-button aria + "Quiz" label, Spielen play button
- Empty: "Quiz nicht gefunden."
- Meta-section: Titel/Beschreibung/Kategorie/Tags placeholders, Sichtbarkeit row label, untitled fallback
- Question list: "Fragen ({n})" heading, empty state, type-pill, edit/delete title+aria, "Frage löschen?" confirm
- Question types routed through $_('quiz.question_types.' + q.type); QUESTION_TYPE_LABELS constant kept in types.ts for non-Svelte callers
- New-question section: edit/new heading, cancel button, type select (4 options), question/correct-answer/expected-input fields, options-label (multi/single variant), correct-toggle title+aria, "Antwort {n}" placeholder, remove aria, "Antwort hinzufügen", explanation field, save/add submit button
- truefalse default options ("Wahr"/"Falsch") now i18n'd

Baselines: hardcoded 1218 → 1205 (13 cleared); missing-keys baseline +1 (quiz.question_types.* dynamic key).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-27 12:42:11 +02:00
parent 0fbef25565
commit 84bc904775
3 changed files with 64 additions and 44 deletions

View file

@ -6,10 +6,10 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { useQuiz, useQuestions, blankOption } from './queries'; import { useQuiz, useQuestions, blankOption } from './queries';
import { quizzesStore } from './stores/quizzes.svelte'; import { quizzesStore } from './stores/quizzes.svelte';
import { QUESTION_TYPE_LABELS } from './types';
import type { QuestionType, QuestionOption, QuizQuestion } from './types'; import type { QuestionType, QuestionOption, QuizQuestion } from './types';
import { ArrowLeft, Plus, Trash, Check, Play, PencilSimple, X } from '@mana/shared-icons'; import { ArrowLeft, Plus, Trash, Check, Play, PencilSimple, X } from '@mana/shared-icons';
import { VisibilityPicker, type VisibilityLevel } from '@mana/shared-privacy'; import { VisibilityPicker, type VisibilityLevel } from '@mana/shared-privacy';
import { _ } from 'svelte-i18n';
interface Props { interface Props {
quizId: string; quizId: string;
@ -42,7 +42,7 @@
async function saveMeta() { async function saveMeta() {
if (!quiz) return; if (!quiz) return;
await quizzesStore.updateQuiz(quiz.id, { await quizzesStore.updateQuiz(quiz.id, {
title: metaTitle.trim() || 'Unbenannt', title: metaTitle.trim() || $_('quiz.edit_view.untitled_fallback'),
description: metaDescription.trim() || null, description: metaDescription.trim() || null,
category: metaCategory.trim() || null, category: metaCategory.trim() || null,
tags: metaTags tags: metaTags
@ -71,8 +71,8 @@
function defaultOptions(type: QuestionType): QuestionOption[] { function defaultOptions(type: QuestionType): QuestionOption[] {
if (type === 'truefalse') { if (type === 'truefalse') {
return [ return [
{ id: 't', text: 'Wahr', isCorrect: true }, { id: 't', text: $_('quiz.edit_view.truefalse_true'), isCorrect: true },
{ id: 'f', text: 'Falsch', isCorrect: false }, { id: 'f', text: $_('quiz.edit_view.truefalse_false'), isCorrect: false },
]; ];
} }
if (type === 'text') return []; if (type === 'text') return [];
@ -166,7 +166,7 @@
} }
async function deleteQuestion(id: string) { async function deleteQuestion(id: string) {
if (!confirm('Frage löschen?')) return; if (!confirm($_('quiz.edit_view.confirm_delete_question'))) return;
await quizzesStore.deleteQuestion(id); await quizzesStore.deleteQuestion(id);
} }
@ -181,8 +181,9 @@
<div class="wrap"> <div class="wrap">
<header class="header"> <header class="header">
<button class="back" onclick={() => goto('/quiz')} aria-label="Zurück"> <button class="back" onclick={() => goto('/quiz')} aria-label={$_('quiz.edit_view.back_aria')}>
<ArrowLeft size={18} /> Quiz <ArrowLeft size={18} />
{$_('quiz.edit_view.back_label')}
</button> </button>
{#if quiz} {#if quiz}
<button <button
@ -190,13 +191,14 @@
disabled={questions.length === 0} disabled={questions.length === 0}
onclick={() => goto(`/quiz/${quiz.id}/play`)} onclick={() => goto(`/quiz/${quiz.id}/play`)}
> >
<Play size={14} weight="fill" /> Spielen <Play size={14} weight="fill" />
{$_('quiz.edit_view.action_play')}
</button> </button>
{/if} {/if}
</header> </header>
{#if !quiz} {#if !quiz}
<p class="empty">Quiz nicht gefunden.</p> <p class="empty">{$_('quiz.edit_view.empty_quiz')}</p>
{:else} {:else}
<section class="meta-section"> <section class="meta-section">
<input <input
@ -204,13 +206,13 @@
type="text" type="text"
bind:value={metaTitle} bind:value={metaTitle}
onblur={saveMeta} onblur={saveMeta}
placeholder="Titel" placeholder={$_('quiz.edit_view.placeholder_title')}
/> />
<textarea <textarea
class="desc-input" class="desc-input"
bind:value={metaDescription} bind:value={metaDescription}
onblur={saveMeta} onblur={saveMeta}
placeholder="Beschreibung (optional)" placeholder={$_('quiz.edit_view.placeholder_description')}
rows="2" rows="2"
></textarea> ></textarea>
<div class="meta-row"> <div class="meta-row">
@ -219,18 +221,18 @@
type="text" type="text"
bind:value={metaCategory} bind:value={metaCategory}
onblur={saveMeta} onblur={saveMeta}
placeholder="Kategorie" placeholder={$_('quiz.edit_view.placeholder_category')}
/> />
<input <input
class="small-input" class="small-input"
type="text" type="text"
bind:value={metaTags} bind:value={metaTags}
onblur={saveMeta} onblur={saveMeta}
placeholder="Tags (Komma-getrennt)" placeholder={$_('quiz.edit_view.placeholder_tags')}
/> />
</div> </div>
<div class="visibility-row"> <div class="visibility-row">
<span class="visibility-label">Sichtbarkeit</span> <span class="visibility-label">{$_('quiz.edit_view.label_visibility')}</span>
<VisibilityPicker <VisibilityPicker
level={quiz.visibility ?? 'space'} level={quiz.visibility ?? 'space'}
onChange={handleVisibilityChange} onChange={handleVisibilityChange}
@ -240,28 +242,28 @@
</section> </section>
<section class="questions-section"> <section class="questions-section">
<h2>Fragen ({questions.length})</h2> <h2>{$_('quiz.edit_view.section_questions', { values: { n: questions.length } })}</h2>
{#if questions.length === 0} {#if questions.length === 0}
<p class="empty">Noch keine Fragen — füge unten eine hinzu.</p> <p class="empty">{$_('quiz.edit_view.empty_questions')}</p>
{:else} {:else}
<ol class="question-list"> <ol class="question-list">
{#each questions as q, i (q.id)} {#each questions as q, i (q.id)}
<li class="question-item" class:editing={editingId === q.id}> <li class="question-item" class:editing={editingId === q.id}>
<div class="q-header"> <div class="q-header">
<span class="q-num">{i + 1}</span> <span class="q-num">{i + 1}</span>
<span class="q-type">{QUESTION_TYPE_LABELS[q.type]}</span> <span class="q-type">{$_('quiz.question_types.' + q.type)}</span>
<button <button
class="icon-btn" class="icon-btn"
title="Bearbeiten" title={$_('quiz.edit_view.action_edit')}
aria-label="Bearbeiten" aria-label={$_('quiz.edit_view.action_edit')}
onclick={() => startEdit(q)} onclick={() => startEdit(q)}
> >
<PencilSimple size={14} /> <PencilSimple size={14} />
</button> </button>
<button <button
class="icon-btn" class="icon-btn"
title="Löschen" title={$_('quiz.edit_view.action_delete')}
aria-label="Löschen" aria-label={$_('quiz.edit_view.action_delete')}
onclick={() => deleteQuestion(q.id)} onclick={() => deleteQuestion(q.id)}
> >
<Trash size={14} /> <Trash size={14} />
@ -284,46 +286,61 @@
<div class="new-header"> <div class="new-header">
<h2> <h2>
{editingId {editingId
? `Frage ${questions.findIndex((x) => x.id === editingId) + 1} bearbeiten` ? $_('quiz.edit_view.new_section_edit', {
: 'Neue Frage'} values: { n: questions.findIndex((x) => x.id === editingId) + 1 },
})
: $_('quiz.edit_view.new_section_new')}
</h2> </h2>
{#if editingId} {#if editingId}
<button class="cancel-btn" onclick={cancelEdit}> <button class="cancel-btn" onclick={cancelEdit}>
<X size={12} /> Abbrechen <X size={12} />
{$_('quiz.edit_view.action_cancel')}
</button> </button>
{/if} {/if}
</div> </div>
<label class="field"> <label class="field">
<span>Typ</span> <span>{$_('quiz.edit_view.label_type')}</span>
<select bind:value={newType} onchange={onTypeChange}> <select bind:value={newType} onchange={onTypeChange}>
<option value="single">Single Choice</option> <option value="single">{$_('quiz.question_types.single')}</option>
<option value="multi">Multiple Choice</option> <option value="multi">{$_('quiz.question_types.multi')}</option>
<option value="truefalse">Wahr / Falsch</option> <option value="truefalse">{$_('quiz.question_types.truefalse')}</option>
<option value="text">Texteingabe</option> <option value="text">{$_('quiz.question_types.text')}</option>
</select> </select>
</label> </label>
<label class="field"> <label class="field">
<span>Frage</span> <span>{$_('quiz.edit_view.label_question')}</span>
<textarea bind:value={newText} rows="2" placeholder="Was möchtest du fragen?"></textarea> <textarea
bind:value={newText}
rows="2"
placeholder={$_('quiz.edit_view.placeholder_question')}
></textarea>
</label> </label>
{#if newType === 'text'} {#if newType === 'text'}
<label class="field"> <label class="field">
<span>Korrekte Antwort</span> <span>{$_('quiz.edit_view.label_correct_answer')}</span>
<input type="text" bind:value={newTextAnswer} placeholder="Erwartete Eingabe" /> <input
type="text"
bind:value={newTextAnswer}
placeholder={$_('quiz.edit_view.placeholder_expected')}
/>
</label> </label>
{:else} {:else}
<div class="options-block"> <div class="options-block">
<span class="options-label"> <span class="options-label">
Antworten {newType === 'multi' ? '(mehrere richtig möglich)' : '(eine richtig)'} {newType === 'multi'
? $_('quiz.edit_view.options_label_multi')
: $_('quiz.edit_view.options_label_single')}
</span> </span>
{#each newOptions as opt, i (opt.id)} {#each newOptions as opt, i (opt.id)}
<div class="option-row"> <div class="option-row">
<button <button
class="correct-toggle" class="correct-toggle"
class:on={opt.isCorrect} class:on={opt.isCorrect}
title={opt.isCorrect ? 'Richtig' : 'Als richtig markieren'} title={opt.isCorrect
aria-label="Richtig" ? $_('quiz.edit_view.correct_marked')
: $_('quiz.edit_view.correct_mark_action')}
aria-label={$_('quiz.edit_view.correct_marked')}
onclick={() => toggleNewCorrect(opt.id)} onclick={() => toggleNewCorrect(opt.id)}
> >
{#if opt.isCorrect}<Check size={14} weight="bold" />{/if} {#if opt.isCorrect}<Check size={14} weight="bold" />{/if}
@ -331,13 +348,15 @@
<input <input
type="text" type="text"
bind:value={newOptions[i].text} bind:value={newOptions[i].text}
placeholder={`Antwort ${i + 1}`} placeholder={$_('quiz.edit_view.placeholder_option', {
values: { n: i + 1 },
})}
disabled={newType === 'truefalse'} disabled={newType === 'truefalse'}
/> />
{#if newType !== 'truefalse' && newOptions.length > 2} {#if newType !== 'truefalse' && newOptions.length > 2}
<button <button
class="icon-btn" class="icon-btn"
aria-label="Entfernen" aria-label={$_('quiz.edit_view.action_remove')}
onclick={() => removeNewOption(opt.id)} onclick={() => removeNewOption(opt.id)}
> >
<Trash size={14} /> <Trash size={14} />
@ -347,26 +366,27 @@
{/each} {/each}
{#if newType !== 'truefalse' && newOptions.length < 6} {#if newType !== 'truefalse' && newOptions.length < 6}
<button class="add-option" onclick={addNewOption}> <button class="add-option" onclick={addNewOption}>
<Plus size={12} /> Antwort hinzufügen <Plus size={12} />
{$_('quiz.edit_view.action_add_option')}
</button> </button>
{/if} {/if}
</div> </div>
{/if} {/if}
<label class="field"> <label class="field">
<span>Erklärung (optional)</span> <span>{$_('quiz.edit_view.label_explanation')}</span>
<textarea <textarea
bind:value={newExplanation} bind:value={newExplanation}
rows="2" rows="2"
placeholder="Wird nach dem Antworten angezeigt" placeholder={$_('quiz.edit_view.placeholder_explanation')}
></textarea> ></textarea>
</label> </label>
<button class="submit-btn" onclick={submitQuestion}> <button class="submit-btn" onclick={submitQuestion}>
{#if editingId} {#if editingId}
<Check size={14} /> Änderungen speichern <Check size={14} /> {$_('quiz.edit_view.action_save_changes')}
{:else} {:else}
<Plus size={14} /> Frage hinzufügen <Plus size={14} /> {$_('quiz.edit_view.action_add_question')}
{/if} {/if}
</button> </button>
</section> </section>

View file

@ -170,7 +170,6 @@
"apps/mana/apps/web/src/lib/modules/profile/MeImagesView.svelte": 2, "apps/mana/apps/web/src/lib/modules/profile/MeImagesView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/questions/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/questions/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/questions/views/DetailView.svelte": 6, "apps/mana/apps/web/src/lib/modules/questions/views/DetailView.svelte": 6,
"apps/mana/apps/web/src/lib/modules/quiz/EditView.svelte": 13,
"apps/mana/apps/web/src/lib/modules/quiz/ListView.svelte": 5, "apps/mana/apps/web/src/lib/modules/quiz/ListView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/quiz/PlayView.svelte": 6, "apps/mana/apps/web/src/lib/modules/quiz/PlayView.svelte": 6,
"apps/mana/apps/web/src/lib/modules/quotes/views/DetailView.svelte": 2, "apps/mana/apps/web/src/lib/modules/quotes/views/DetailView.svelte": 2,

View file

@ -15,6 +15,7 @@
"apps/mana/apps/web/src/lib/modules/library/views/DetailView.svelte": 3, "apps/mana/apps/web/src/lib/modules/library/views/DetailView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/period/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/period/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/plants/ListView.svelte": 5, "apps/mana/apps/web/src/lib/modules/plants/ListView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/quiz/EditView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/quotes/components/QuoteCard.svelte": 4, "apps/mana/apps/web/src/lib/modules/quotes/components/QuoteCard.svelte": 4,
"apps/mana/apps/web/src/lib/modules/recipes/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/recipes/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/times/components/EntryForm.svelte": 6, "apps/mana/apps/web/src/lib/modules/times/components/EntryForm.svelte": 6,