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