diff --git a/AI_HANDOVER.md b/AI_HANDOVER.md index 7cb66aa..fab5979 100644 --- a/AI_HANDOVER.md +++ b/AI_HANDOVER.md @@ -109,10 +109,14 @@ Aktuelle Button- und Sichtbarkeitslogik pro Umfrage: - Nach Veröffentlichung disabled - `Öffentliche Ansicht` - Nur sichtbar, wenn `published = 1` +- `Link kopieren` + - Nur sichtbar, wenn `published = 1` + - Kopiert die **absolute** öffentliche Reader-URL (`copyUrl`, erzeugt mit `UrlGeneratorInterface::ABSOLUTE_URL`) über den `data-copy-text`-Handler in `_survey_assets.html.twig`. - `Entwurfs-Ansicht` - Nur sichtbar, wenn `published = 0` - `Ergebnisse anzeigen` - Immer verfügbar, sofern Results-Seite konfiguriert ist +- Aktionslinks (`Bearbeiten`, `Öffentliche Ansicht`, `Ergebnisse anzeigen`, `Entwurfs-Ansicht`) öffnen bewusst **im selben Tab** (kein `target="_blank"` mehr), weil der Kunde die vielen neuen Tabs als störend meldete. - `Umfrage veröffentlichen` - Eigene Aktion in der Liste - Nach Veröffentlichung disabled und textlich als veröffentlicht markiert @@ -120,9 +124,12 @@ Aktuelle Button- und Sichtbarkeitslogik pro Umfrage: - Legt eine Kopie der Umfrage für den Redakteur an - Titelvergabe ist fortlaufend: `Titel - Kopie`, danach `Titel - Kopie 2`, `Titel - Kopie 3` usw. - Bestehende `- Kopie`-Suffixe werden fachlich normalisiert, damit keine Titel wie `... - Kopie - Kopie` mehr neu entstehen -- Kreis grau/grün + - Wichtig: Die Titel-Kollisionsprüfung (`SurveyEditorService::buildDuplicateTitle`) prüft **ohne** Ausschluss der Quell-ID gegen `titleExists`. Sonst würde beim Duplizieren einer bereits `... - Kopie`-Umfrage erneut derselbe Titel vergeben statt `... - Kopie 2` (früherer Bug). +- Aktiv-Umschalter (grauer/grüner Punkt **mit Textlabel** „Aktiv"/„Deaktiviert") - Schaltet `isActive` - Ist bewusst kein Auge mehr + - Das Textlabel wurde ergänzt, weil der reine Punkt ohne Beschriftung für den Kunden unklar war (`survey-visibility-toggle` in `_survey_assets.html.twig`). + - Bei `isActive = 0` ist der öffentliche Reader-Link deaktiviert: `SurveyRepository::findByPublicAlias` verlangt `published = 1` UND `isActive = 1`, sonst liefert der Reader die Leer-/Hinweisseite. Wichtiger technischer Punkt (CSRF): @@ -229,9 +236,19 @@ Aktuelle Ergebnislogik: - Optionsstatistik plus Kreisdiagramm - `range` - Minimum, Maximum, Durchschnitt plus Verteilung und Linienchart + - Die Verteilung deckt die **komplette Skala** von `rangeMin` bis `rangeMax` in `rangeStep`-Schritten ab (inkl. Werte ohne Antworten), damit die X-Achse linear und vollständig ist. Antworten außerhalb der konfigurierten Grenzen erweitern die Skala. Siehe `SurveyResultsViewService::buildRangeScale`. - `text` - Kein Pie-Chart - - Stattdessen Response-Summary und Liste der Freitextantworten + - Gruppierte Freitextantworten: jede unterschiedliche Antwort **einmal** mit exakter Häufigkeit (`responseSummary`, häufigste zuerst, kein Top-5-Limit). Prozentangaben und die frühere Doppel-Auflistung wurden bewusst entfernt. + +Pro Frage werden statt einer reinen Prozent-Rücklaufquote absolute Zahlen ausgewiesen: + +- `totalAnswers` und `completedSubmissionCount` als „X von Y beantwortet". +- `skippedCount` (= `completedSubmissionCount - totalAnswers`) als „Z übersprungen", nur wenn > 0. So sind per Sprungregel übersprungene Fragen sichtbar, statt überall die volle Teilnehmerzahl zu zeigen. + +Reihenfolge: + +- Die Frageergebnisse werden **vor** dem Finalisieren nach `_sorting` sortiert. Wichtig: `finalizeQuestionResult` entfernt die internen `_sorting`-Werte – würde man erst danach sortieren (früherer Bug), fiele die Reihenfolge auf den alphabetischen Fallback zurück. Online-Ansicht, PDF und Excel folgen dadurch derselben Frage-Sortierung. Wichtig: @@ -296,6 +313,8 @@ Wichtige Eigenschaft: - Die Spalten orientieren sich an den aktuell vorhandenen Fragen im Survey. - Die Zeilen enthalten nur abgeschlossene Submissions. +- Über den Antwortspalten steht eine eigene **Fragetext-Zeile** (`F1: `, `F2: ...`). Je Frage werden die Zellen dieser Zeile über alle zugehörigen Antwortspalten zusammengeführt (`mergeCells`). Zuvor fehlte der Fragetext im Excel komplett – der Kunde sah nur `F1 ja`, `F1 nein` usw. Reihenfolge: Titel (Zeile 1), Fragetexte (Zeile 2), Spaltenköpfe (Zeile 3), Daten ab Zeile 4; Freeze-Pane auf `A4`. +- Diagramme gibt es im Excel bewusst nicht (dafür ist der PDF-Export da). ## PDF-Export diff --git a/contao/templates/frontend/_survey_assets.html.twig b/contao/templates/frontend/_survey_assets.html.twig index ce62454..545375b 100644 --- a/contao/templates/frontend/_survey_assets.html.twig +++ b/contao/templates/frontend/_survey_assets.html.twig @@ -440,15 +440,36 @@ transform: translateY(0); } + .survey-visibility-toggle { + display: inline-flex; + align-items: center; + gap: 0.55rem; + padding: 0.35rem 0.9rem 0.35rem 0.4rem; + border-radius: 999px; + border: 1px solid rgba(162, 168, 180, 0.5); + background: rgba(255, 255, 255, 0.88); + color: var(--survey-gray-dark); + font-weight: 700; + font-size: 0.9rem; + line-height: 1; + cursor: pointer; + transition: border-color 0.18s ease, color 0.18s ease; + } + + .survey-visibility-toggle.is-on { + border-color: rgba(10, 131, 46, 0.45); + color: #0a6e28; + } + .survey-visibility-dot { aspect-ratio: 1; - height: 42px; + height: 28px; border-radius: 999px; background: #a6adb7; display: inline-flex; align-items: center; justify-content: center; - cursor: pointer; + flex-shrink: 0; transition: background 0.18s ease; } @@ -456,6 +477,10 @@ background: #0a832e; } + .survey-visibility-label { + white-space: nowrap; + } + .survey-button-row { display: flex; flex-wrap: wrap; diff --git a/contao/templates/frontend/member_survey_list.html.twig b/contao/templates/frontend/member_survey_list.html.twig index 2f8a692..a82135a 100644 --- a/contao/templates/frontend/member_survey_list.html.twig +++ b/contao/templates/frontend/member_survey_list.html.twig @@ -70,13 +70,22 @@ {% endif %} {% endif %} {% if survey.publicUrl and survey.published %} - {{ 'survey.list.public_view'|trans }} + {{ 'survey.list.public_view'|trans }} + {% endif %} + {% if survey.copyUrl and survey.published %} + {% endif %} {% if survey.draftUrl and not survey.published %} - {{ 'survey.list.draft_view'|trans }} + {{ 'survey.list.draft_view'|trans }} {% endif %} {% if survey.resultsUrl %} - {{ 'survey.list.show_results'|trans }} + {{ 'survey.list.show_results'|trans }} {% endif %}
@@ -118,7 +127,10 @@ - +
diff --git a/contao/templates/frontend/show_survey_results.html.twig b/contao/templates/frontend/show_survey_results.html.twig index dbca0b6..c9184e0 100644 --- a/contao/templates/frontend/show_survey_results.html.twig +++ b/contao/templates/frontend/show_survey_results.html.twig @@ -41,8 +41,10 @@

{{ question.question|striptags|trim }}

- {{ 'survey.results.participations'|trans({'%count%': completedSubmissionCount}) }} - {{ 'survey.results.response_rate'|trans({'%rate%': question.responseRate}) }} + {{ 'survey.results.answered'|trans({'%count%': question.totalAnswers, '%total%': completedSubmissionCount}) }} + {% if question.skippedCount > 0 %} + {{ 'survey.results.skipped'|trans({'%count%': question.skippedCount}) }} + {% endif %}
{% if question.description|striptags|trim %} @@ -107,25 +109,13 @@ {% endfor %} {% else %} - {% if question.responseSummary is defined and question.responseSummary %} -
- {% for option in question.responseSummary %} -
- {{ option.label|striptags|trim }} -
- {{ option.count }} - {{ option.percentage }}% -
-
- {% endfor %} -
- {% endif %} - + {# Eine Liste statt Doppel-Auflistung: jede Antwort einmal, + mit exakter Anzahl statt Prozentwerten. #}
- {% for response in question.responses %} + {% for option in question.responseSummary|default([]) %}
- {{ loop.index }} -
{{ response|striptags|trim|nl2br }}
+ {{ 'survey.results.answer_count'|trans({'%count%': option.count}) }} +
{{ option.label|striptags|trim|nl2br }}
{% else %}

{{ 'survey.results.no_text_responses'|trans }}

diff --git a/src/Controller/FrontendModule/MemberSurveyListController.php b/src/Controller/FrontendModule/MemberSurveyListController.php index b337137..fadd444 100644 --- a/src/Controller/FrontendModule/MemberSurveyListController.php +++ b/src/Controller/FrontendModule/MemberSurveyListController.php @@ -18,6 +18,7 @@ use Mummert\SurveyBundle\Service\SurveyEditorService; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; #[AsFrontendModule(type: 'member_survey_list', category: 'survey', template: 'member_survey_list')] #[AsFrontendModule(type: 'member_survey_template_list', category: 'survey', template: 'member_survey_list')] @@ -69,6 +70,9 @@ final class MemberSurveyListController extends AbstractFrontendModuleController 'participationCount' => (int) ($survey['participationCount'] ?? 0), 'editUrl' => $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['survey' => $surveyId]) : null, 'publicUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['parameters' => '/'.$alias]) : null, + // Absolute URL für den "Link kopieren"-Button, damit der geteilte + // Link auch außerhalb der Seite funktioniert. + 'copyUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['parameters' => '/'.$alias], UrlGeneratorInterface::ABSOLUTE_URL) : null, 'draftUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['parameters' => '/'.$alias, 'preview' => '1']) : null, 'resultsUrl' => $resultsPage instanceof PageModel ? $this->generateContentUrl($resultsPage, ['parameters' => '/'.$alias]) : null, ]; diff --git a/src/Resources/views/pdf/survey_results.html.twig b/src/Resources/views/pdf/survey_results.html.twig index df2fc8d..ae721e0 100644 --- a/src/Resources/views/pdf/survey_results.html.twig +++ b/src/Resources/views/pdf/survey_results.html.twig @@ -223,8 +223,10 @@ {% endif %}
- {{ 'survey.pdf.participations'|trans({'%count%': completedSubmissionCount}) }} - {{ 'survey.pdf.response_rate'|trans({'%rate%': question.responseRate}) }} + {{ 'survey.pdf.answered'|trans({'%count%': question.totalAnswers, '%total%': completedSubmissionCount}) }} + {% if question.skippedCount > 0 %} + {{ 'survey.pdf.skipped'|trans({'%count%': question.skippedCount}) }} + {% endif %}
{% if question.pdfChartDataUri %} @@ -302,36 +304,28 @@ {% else %} - {% if question.pdfChartLegend %} + {# Eine Tabelle statt Doppel-Auflistung: jede Antwort einmal, mit + exakter Anzahl statt Prozentwerten. #} + {% if question.responseSummary|default([]) %} - - - {% for option in question.pdfChartLegend %} + {% for option in question.responseSummary %} - - {% endfor %}
{{ 'survey.pdf.color'|trans }} {{ 'survey.pdf.answer_group'|trans }} {{ 'survey.pdf.count'|trans }}{{ 'survey.pdf.share'|trans }}
{{ option.label|striptags|trim }} {{ option.count }}{{ option.percentage }}%
+ {% else %} +

{{ 'survey.pdf.no_text_responses'|trans }}

{% endif %} - -
    - {% for response in question.responses %} -
  1. {{ response|striptags|trim }}
  2. - {% else %} -
  3. {{ 'survey.pdf.no_text_responses'|trans }}
  4. - {% endfor %} -
{% endif %} {% else %} diff --git a/src/Service/SurveyEditorService.php b/src/Service/SurveyEditorService.php index db83f25..3a6ad45 100644 --- a/src/Service/SurveyEditorService.php +++ b/src/Service/SurveyEditorService.php @@ -251,7 +251,10 @@ final class SurveyEditorService $suffix = 1 === $copyNumber ? ' - Kopie' : sprintf(' - Kopie %d', $copyNumber); $candidate = $this->buildDuplicateTitleCandidate($baseTitle, $suffix); - if (!$this->surveyRepository->titleExists($candidate, (int) $survey->id)) { + // Bewusst ohne Ausschluss der Quelle: Beim Duplizieren einer bereits + // "... - Kopie"-Umfrage muss der Quelltitel selbst als vergeben gelten, + // sonst entsteht erneut derselbe Titel statt "... - Kopie 2". + if (!$this->surveyRepository->titleExists($candidate)) { return $candidate; } } diff --git a/src/Service/SurveyResultsExportService.php b/src/Service/SurveyResultsExportService.php index d5d9d00..29b3da4 100644 --- a/src/Service/SurveyResultsExportService.php +++ b/src/Service/SurveyResultsExportService.php @@ -29,9 +29,19 @@ final class SurveyResultsExportService */ public function createExcelExportResponse(SurveyModel $survey, array $finishedSubmissions): Response { - [$headers, $columns] = $this->buildExportColumns($this->surveyQuestionRepository->findAllBySurvey((int) $survey->id)); + [$questionSpans, $headers, $columns] = $this->buildExportColumns($this->surveyQuestionRepository->findAllBySurvey((int) $survey->id)); + + // Eigene Zeile mit dem Fragetext oberhalb der Antwortspalten; die Zellen + // werden je Frage über alle zugehörigen Spalten zusammengeführt. + $questionRow = array_fill(0, max(1, count($headers)), ''); + + foreach ($questionSpans as $span) { + $questionRow[$span['start'] - 1] = $span['label']; + } + $rows = [ [(string) $survey->title], + $questionRow, $headers, ]; @@ -66,7 +76,7 @@ final class SurveyResultsExportService $rows[] = $row; } - $response = new Response($this->buildSpreadsheetContent($rows)); + $response = new Response($this->buildSpreadsheetContent($rows, $questionSpans)); $disposition = $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, $this->buildExportFilename((string) $survey->title) @@ -84,16 +94,18 @@ final class SurveyResultsExportService /** * @param list $questions * - * @return array{0:list,1:list} + * @return array{0:list,1:list,2:list} */ private function buildExportColumns(array $questions): array { + $questionSpans = []; $headers = []; $columns = []; foreach (array_values($questions) as $index => $question) { $questionNumber = $index + 1; $prefix = 'F'.$questionNumber; + $firstColumnIndex = count($headers) + 1; switch ((string) $question->type) { case 'yes_no_maybe': @@ -137,9 +149,17 @@ final class SurveyResultsExportService ]; break; } + + $questionLabel = trim((string) $question->question); + + $questionSpans[] = [ + 'label' => sprintf('%s: %s', $prefix, '' !== $questionLabel ? $questionLabel : 'Frage #'.(int) $question->id), + 'start' => $firstColumnIndex, + 'end' => count($headers), + ]; } - return [$headers, $columns]; + return [$questionSpans, $headers, $columns]; } /** @@ -178,9 +198,10 @@ final class SurveyResultsExportService } /** - * @param list> $rows + * @param list> $rows + * @param list $questionSpans */ - private function buildSpreadsheetContent(array $rows): string + private function buildSpreadsheetContent(array $rows, array $questionSpans): string { $spreadsheet = new Spreadsheet(); $worksheet = $spreadsheet->getActiveSheet(); @@ -200,12 +221,27 @@ final class SurveyResultsExportService } } - $lastColumn = Coordinate::stringFromColumnIndex(max(1, count($rows[1] ?? $rows[0] ?? []))); + // Fragetext-Zellen je Frage über die zugehörigen Antwortspalten zusammenführen. + foreach ($questionSpans as $span) { + if ($span['end'] > $span['start']) { + $worksheet->mergeCells(sprintf( + '%s2:%s2', + Coordinate::stringFromColumnIndex($span['start']), + Coordinate::stringFromColumnIndex($span['end']), + )); + } + } + + $columnCount = max(1, count($rows[2] ?? $rows[0] ?? [])); + $lastColumn = Coordinate::stringFromColumnIndex($columnCount); $worksheet->getStyle('A1')->getFont()->setBold(true)->setSize(14); $worksheet->getStyle('A2:'.$lastColumn.'2')->getFont()->setBold(true); - $worksheet->freezePane('A3'); + $worksheet->getStyle('A2:'.$lastColumn.'2')->getAlignment()->setWrapText(true)->setVertical(\PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_TOP); + $worksheet->getRowDimension(2)->setRowHeight(42); + $worksheet->getStyle('A3:'.$lastColumn.'3')->getFont()->setBold(true); + $worksheet->freezePane('A4'); - for ($column = 1; $column <= max(1, count($rows[1] ?? $rows[0] ?? [])); ++$column) { + for ($column = 1; $column <= $columnCount; ++$column) { $worksheet->getColumnDimension(Coordinate::stringFromColumnIndex($column))->setAutoSize(true); } diff --git a/src/Service/SurveyResultsViewService.php b/src/Service/SurveyResultsViewService.php index 2f4f4af..228cb29 100644 --- a/src/Service/SurveyResultsViewService.php +++ b/src/Service/SurveyResultsViewService.php @@ -61,6 +61,11 @@ final class SurveyResultsViewService ); $results[$key]['_choiceOptions'] = $this->getChoiceOptions($question); $results[$key]['_allowMaybe'] = '1' === (string) $question->allowMaybe; + $results[$key]['_rangeConfig'] = 'range' === (string) $question->type ? [ + 'min' => (int) $question->rangeMin, + 'max' => (int) $question->rangeMax, + 'step' => max(1, (int) $question->rangeStep), + ] : null; } foreach ($answers as $answer) { @@ -83,10 +88,9 @@ final class SurveyResultsViewService $results[$key]['_values'][] = (string) ($answer['value'] ?? ''); } - foreach ($results as $key => $result) { - $results[$key] = $this->finalizeQuestionResult($result, $completedSubmissionCount); - } - + // Erst sortieren, dann finalisieren: finalizeQuestionResult entfernt die + // internen _sorting-Werte – ein späteres Sortieren fiele sonst immer auf + // den alphabetischen Fallback zurück. uasort($results, static function (array $left, array $right): int { $sortingComparison = ((int) ($left['_sorting'] ?? 0)) <=> ((int) ($right['_sorting'] ?? 0)); @@ -97,6 +101,10 @@ final class SurveyResultsViewService return strcasecmp((string) ($left['question'] ?? ''), (string) ($right['question'] ?? '')); }); + foreach ($results as $key => $result) { + $results[$key] = $this->finalizeQuestionResult($result, $completedSubmissionCount); + } + return array_values($results); } @@ -124,6 +132,7 @@ final class SurveyResultsViewService '_values' => [], '_choiceOptions' => [], '_allowMaybe' => false, + '_rangeConfig' => null, ]; } @@ -139,6 +148,7 @@ final class SurveyResultsViewService $totalAnswers = count($values); $result['totalAnswers'] = $totalAnswers; + $result['skippedCount'] = max(0, $completedSubmissionCount - $totalAnswers); $result['responseRate'] = $completedSubmissionCount > 0 ? (int) round(($totalAnswers / $completedSubmissionCount) * 100) : 0; switch ((string) ($result['type'] ?? 'text')) { @@ -186,14 +196,20 @@ final class SurveyResultsViewService case 'range': $numericValues = array_values(array_map(static fn (string $value): int => (int) $value, $values)); - $distribution = []; + $valueCounts = []; foreach ($numericValues as $numericValue) { - $label = (string) $numericValue; - $distribution[$label] = ($distribution[$label] ?? 0) + 1; + $valueCounts[$numericValue] = ($valueCounts[$numericValue] ?? 0) + 1; } - ksort($distribution, SORT_NATURAL); + // Die komplette Skala abbilden (inklusive Werte ohne Antworten), damit + // die X-Achse linear und vollständig ist – nicht nur die Werte, die + // tatsächlich geantwortet wurden. + $distribution = []; + + foreach ($this->buildRangeScale($result['_rangeConfig'] ?? null, $valueCounts) as $scalePoint) { + $distribution[(string) $scalePoint] = $valueCounts[$scalePoint] ?? 0; + } $result['minimum'] = [] !== $numericValues ? min($numericValues) : null; $result['maximum'] = [] !== $numericValues ? max($numericValues) : null; @@ -210,11 +226,71 @@ final class SurveyResultsViewService break; } - unset($result['_sorting'], $result['_values'], $result['_choiceOptions'], $result['_allowMaybe']); + unset($result['_sorting'], $result['_values'], $result['_choiceOptions'], $result['_allowMaybe'], $result['_rangeConfig']); return $result; } + /** + * Liefert alle Skalenpunkte einer Bewertungsfrage. Ohne Skalen-Konfiguration + * (z. B. bei gelöschten Fragen, die nur noch als Antwort-Snapshot existieren) + * wird auf die tatsächlich vorkommenden Werte zurückgegriffen. + * + * @param array{min:int,max:int,step:int}|null $rangeConfig + * @param array $valueCounts + * + * @return list + */ + private function buildRangeScale(?array $rangeConfig, array $valueCounts): array + { + $observedValues = array_keys($valueCounts); + $min = $rangeConfig['min'] ?? ([] !== $observedValues ? min($observedValues) : null); + $max = $rangeConfig['max'] ?? ([] !== $observedValues ? max($observedValues) : null); + $step = max(1, (int) ($rangeConfig['step'] ?? 1)); + + if (null === $min || null === $max) { + return []; + } + + // Antworten außerhalb der konfigurierten Grenzen (nachträglich geänderte + // Skala) nicht verschlucken, sondern die Skala entsprechend erweitern. + if ([] !== $observedValues) { + $min = min($min, min($observedValues)); + $max = max($max, max($observedValues)); + } + + if ($max < $min) { + [$min, $max] = [$max, $min]; + } + + // Schutz vor unbrauchbar dichten Achsen bei extremen Skalen. + if (($max - $min) / $step > 100) { + sort($observedValues); + + return array_values($observedValues); + } + + $scale = []; + + for ($point = $min; $point <= $max; $point += $step) { + $scale[] = $point; + } + + if ([] === $scale || $max !== end($scale)) { + $scale[] = $max; + } + + foreach ($observedValues as $observedValue) { + if (!\in_array($observedValue, $scale, true)) { + $scale[] = $observedValue; + } + } + + sort($scale); + + return array_values($scale); + } + /** * @return list */ @@ -424,26 +500,11 @@ final class SurveyResultsViewService $counts[$label] = ($counts[$label] ?? 0) + 1; } + // Alle unterschiedlichen Antworten mit exakter Häufigkeit zeigen (häufigste + // zuerst) – kein Top-5-Limit mit Sammelposten mehr. arsort($counts); - $summary = []; - $remainingCount = 0; - $index = 0; - foreach ($counts as $label => $count) { - if ($index < 5) { - $summary[$label] = $count; - } else { - $remainingCount += $count; - } - - ++$index; - } - - if ($remainingCount > 0) { - $summary['Weitere Antworten'] = $remainingCount; - } - - return $this->buildOptionStats($summary); + return $this->buildOptionStats($counts); } /** diff --git a/translations/messages.de.yaml b/translations/messages.de.yaml index 586b523..6e8aac8 100644 --- a/translations/messages.de.yaml +++ b/translations/messages.de.yaml @@ -16,6 +16,10 @@ survey: participations: "%count% Teilnahmen" edit: "Bearbeiten" public_view: "Öffentliche Ansicht" + copy_link: "Link kopieren" + copy_link_done: "Link kopiert!" + active_state: "Aktiv" + inactive_state: "Deaktiviert" draft_view: "Entwurfs-Ansicht" show_results: "Ergebnisse anzeigen" publish: "Umfrage veröffentlichen" @@ -123,6 +127,9 @@ survey: download_excel: "Ergebnisse als Excel herunterladen" download_pdf: "Ergebnisse als PDF herunterladen" answers: "%count% Antworten" + answered: "%count% von %total% beantwortet" + skipped: "%count% übersprungen" + answer_count: "%count%×" response_rate: "%rate%% Rücklauf" minimum: "Minimum" maximum: "Maximum" @@ -137,6 +144,8 @@ survey: questions: "%count% Fragen" export_prefix: "Export %date%" answers: "%count% Antworten" + answered: "%count% von %total% beantwortet" + skipped: "%count% übersprungen" response_rate: "%rate%% Rücklauf" minimum: "Minimum" maximum: "Maximum" diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 33def9b..b3289a9 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -16,6 +16,10 @@ survey: participations: "%count% participations" edit: "Edit" public_view: "Public view" + copy_link: "Copy link" + copy_link_done: "Link copied!" + active_state: "Active" + inactive_state: "Deactivated" draft_view: "Draft view" show_results: "Show results" publish: "Publish survey" @@ -123,6 +127,9 @@ survey: download_excel: "Download results as Excel" download_pdf: "Download results as PDF" answers: "%count% answers" + answered: "%count% of %total% answered" + skipped: "%count% skipped" + answer_count: "%count%×" response_rate: "%rate%% response rate" minimum: "Minimum" maximum: "Maximum" @@ -137,6 +144,8 @@ survey: questions: "%count% questions" export_prefix: "Export %date%" answers: "%count% answers" + answered: "%count% of %total% answered" + skipped: "%count% skipped" response_rate: "%rate%% response rate" minimum: "Minimum" maximum: "Maximum"