Translate pages, areas, menus, forms and posts field by field instead of rebuilding them
A site can have further languages (Settings → Languages). A page is built once; each language adds only its texts, keyed by the stable block IDs, plus its own address under /en/… (ADR 111, document schema v27, migration 20261009000100 for page_translation). - Editor: DE | EN switch, a list of all texts with original, state (missing, translated, original changed) and filter; structure stays with the main language. Undo, drafts, publishing and revisions cover all languages at once. - Website: /en and translated slugs, lang, canonical, hreflang with x-default, og:locale, sitemap; links to translated pages follow the language; a language switch block. - Header, footer and linked sections translate the same way; menu entries carry labels per language; fixed template texts come from a small dictionary. - Forms: labels, help, options, button and success message per language; messages and the step script speak the language, the submission and the email stay in the main language. - Posts, archives, category names, consent text and the 404 page follow the language. - Single-language sites see none of it and never touch the new table. The forms administration becomes an overview with one row per form and its own edit page; fields are one line each with details collapsed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c7705b7083
commit
e15b2517b1
51 files changed
+3045
-219
No files matched your search
@@ -18,7 +18,7 @@
|
||||
Importzeile sie kennen muss. Sie steht vor jedem Modul-Skript; spaeter geladen wirkt sie
|
||||
nicht mehr. #}
|
||||
<script type="importmap" nonce="{{ csp_nonce() }}">{"imports":{
|
||||
{%- for modul in ['editor-model', 'media', 'design-panel', 'button-design', 'color-control', 'rich-text', 'canvas-overlay', 'library-previews', 'preview-containers', 'page-picker', 'motion', 'slider', 'live-search', 'icons'] %}
|
||||
{%- for modul in ['editor-model', 'media', 'design-panel', 'button-design', 'color-control', 'rich-text', 'canvas-overlay', 'library-previews', 'preview-containers', 'page-picker', 'motion', 'slider', 'live-search', 'icons', 'translate'] %}
|
||||
"/assets/{{ modul }}.js": "/assets/{{ modul }}.js?v={{ kant_version() }}"{{ loop.last ? '' : ',' }}
|
||||
{%- endfor %}
|
||||
}}</script>
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
<button type="button" data-viewport="tablet" aria-pressed="false" title="Tablet"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="2" width="16" height="20" rx="2"/><path d="M11 18h2"/></svg><span>Tablet</span></button>
|
||||
<button type="button" data-viewport="mobile" aria-pressed="false" title="Mobil"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="2" width="10" height="20" rx="2"/><path d="M11 18h2"/></svg><span>Mobil</span></button>
|
||||
</div>
|
||||
{# Der Sprachumschalter (ADR 111). Er erscheint nur, wenn die Website weitere Sprachen hat. #}
|
||||
<div id="language-switch" class="language-switch" role="group" aria-label="Sprache der Bearbeitung" hidden></div>
|
||||
<div class="history-actions">
|
||||
<button id="undo" class="icon-button" title="Rückgängig (Strg+Z)" aria-label="Rückgängig" disabled><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 14 4 9l5-5"/><path d="M4 9h10a6 6 0 0 1 0 12h-3"/></svg></button>
|
||||
<button id="redo" class="icon-button" title="Wiederholen (Strg+Umschalt+Z)" aria-label="Wiederholen" disabled><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 14 5-5-5-5"/><path d="M20 9H10a6 6 0 0 0 0 12h3"/></svg></button>
|
||||
@@ -71,6 +73,7 @@
|
||||
<div class="tree-tools"><button id="expand-all" class="text-button" type="button">Alle öffnen</button><button id="collapse-all" class="text-button" type="button">Einklappen</button><button id="nest" class="text-button" type="button" title="In vorherigen Container verschachteln">⇥ Einrücken</button><button id="outdent" class="text-button" type="button" title="Eine Ebene herausheben">⇤ Ausrücken</button></div>
|
||||
<nav id="structure" aria-label="Blöcke auswählen"></nav>
|
||||
</section>
|
||||
<section class="sidebar-view" data-view="translate" id="translate-panel" aria-label="Übersetzung" hidden></section>
|
||||
<section class="sidebar-view" data-view="element" hidden>
|
||||
<div id="properties"><p class="muted">Inhalte werden geladen …</p></div>
|
||||
</section>
|
||||
@@ -90,7 +93,7 @@
|
||||
</main>
|
||||
<div id="context-menu" class="context-menu" role="menu" aria-label="Elementmenü" hidden></div>
|
||||
<noscript><p class="notice error">Zum Bearbeiten muss JavaScript aktiviert sein. Die öffentliche Website funktioniert ohne JavaScript.</p></noscript>
|
||||
<dialog id="publish-dialog" aria-labelledby="publish-title"><h2 id="publish-title">Seite veröffentlichen?</h2><p>Der gespeicherte Entwurf ersetzt den bisher sichtbaren Inhalt dieser Seite.</p><div class="dialog-actions"><button id="cancel-publish" class="secondary">Abbrechen</button><button id="confirm-publish" class="primary">Jetzt veröffentlichen</button></div></dialog>
|
||||
<dialog id="publish-dialog" aria-labelledby="publish-title"><h2 id="publish-title">Seite veröffentlichen?</h2><p>Der gespeicherte Entwurf ersetzt den bisher sichtbaren Inhalt dieser Seite.</p><p id="publish-languages" hidden>Alle Sprachen dieser Seite gehen gemeinsam online – auch die Übersetzungen im gespeicherten Stand.</p><div class="dialog-actions"><button id="cancel-publish" class="secondary">Abbrechen</button><button id="confirm-publish" class="primary">Jetzt veröffentlichen</button></div></dialog>
|
||||
<dialog id="preset-dialog" aria-labelledby="preset-title"><h2 id="preset-title">Gestaltung als Vorlage speichern</h2><p>Die Vorlage steht danach für jeden Block auf jeder Seite zur Verfügung.</p><label for="preset-name">Name der Vorlage</label><input id="preset-name" maxlength="60" placeholder="Heller Abschnitt"><p id="preset-error" class="field-error" hidden></p><div class="dialog-actions"><button id="cancel-preset" class="secondary">Abbrechen</button><button id="confirm-preset" class="primary">Vorlage speichern</button></div></dialog>
|
||||
<dialog id="reload-dialog" aria-labelledby="reload-title"><h2 id="reload-title">Serverstand laden?</h2><p>Lokale Änderungen und dein Undo-Verlauf werden ersetzt. Sichere deine Änderungen vorher als JSON.</p><div class="dialog-actions"><button id="cancel-reload" class="secondary">Abbrechen</button><button id="confirm-reload" class="primary">Serverstand übernehmen</button></div></dialog>
|
||||
</div>
|
||||
|
||||
@@ -4,35 +4,41 @@
|
||||
Seite dessen Stylesheet, genau wie die Designseite. #}
|
||||
{% block styles %}<link rel="stylesheet" href="/assets/buttons.css?v={{ kant_version() }}"><link rel="stylesheet" href="/assets/color-control.css?v={{ kant_version() }}"><link rel="stylesheet" href="/assets/media-dialog.css?v={{ kant_version() }}">{% endblock %}
|
||||
{% block scripts %}<script type="module" src="/assets/form-editor.js?v={{ kant_version() }}"></script>{% endblock %}
|
||||
{% block title %}Formulare{% endblock %}
|
||||
{% block heading %}Formulare{% endblock %}
|
||||
{% block lead %}Felder, Empfänger, Einwilligungstext und Aussehen werden hier gepflegt. Auf einer Seite steht nur ein Formularblock mit der Adresse – so kann ein gespeicherter Seiteninhalt niemals ändern, wohin eine Anfrage geht.{% endblock %}
|
||||
{% block actions %}<a class="primary" href="#neues-formular">Neues Formular</a>{% endblock %}
|
||||
{% set editing = editing|default(false) %}
|
||||
{% block title %}{{ editing ? (current ? current.name : 'Neues Formular') ~ ' · ' : '' }}Formulare{% endblock %}
|
||||
{% block heading %}{{ editing ? (current ? current.name : 'Neues Formular') : 'Formulare' }}{% endblock %}
|
||||
{% block lead %}{% if editing %}<a class="text-button back-to-list" href="{{ path('admin_forms') }}">← Alle Formulare</a>{% else %}Felder, Empfänger und Aussehen deiner Formulare. Auf einer Seite setzt du sie mit dem Baustein „Formular“ ein.{% endif %}{% endblock %}
|
||||
{% block actions %}{% if not editing %}<a class="primary" href="{{ path('admin_form_edit', {slug: 'neu'}) }}">Neues Formular</a>{% endif %}{% endblock %}
|
||||
|
||||
{# Eine Feldkarte. Der Index steht in den Feldnamen; die Vorlage fuer neue Felder traegt an seiner
|
||||
Stelle den Platzhalter __n__, den das Skript beim Einfuegen ersetzt. #}
|
||||
{% macro field(f, n, row, types) %}
|
||||
{% macro field(f, n, row, types, languages = [], translations = {}) %}
|
||||
{% set id = 'f' ~ f ~ '-' ~ n %}
|
||||
<li class="field-card{{ row.label == '' ? ' field-card-new' : '' }}" data-field>
|
||||
{# Eine Zeile je Feld: Beschriftung, Typ, Pflicht. Alles Weitere steht in den Details, die das
|
||||
Skript zuklappt - ohne Skript ist die Karte offen und vollständig bedienbar. #}
|
||||
<div class="field-card-head">
|
||||
<span class="field-card-index" aria-hidden="true" data-field-index></span>
|
||||
<div class="field-card-title">
|
||||
<label for="{{ id }}-label">Beschriftung</label>
|
||||
<input id="{{ id }}-label" name="fields[{{ n }}][label]" maxlength="120" value="{{ row.label }}" placeholder="{{ row.label == '' ? 'Neues Feld – Beschriftung eingeben' : '' }}" data-field-label>
|
||||
<label class="visually-hidden" for="{{ id }}-label">Beschriftung</label>
|
||||
<input id="{{ id }}-label" name="fields[{{ n }}][label]" maxlength="120" value="{{ row.label }}" placeholder="{{ row.label == '' ? 'Neues Feld – Beschriftung eingeben' : 'Beschriftung' }}" data-field-label>
|
||||
</div>
|
||||
<div class="field-card-type">
|
||||
<label for="{{ id }}-type">Typ</label>
|
||||
<label class="visually-hidden" for="{{ id }}-type">Typ</label>
|
||||
<select id="{{ id }}-type" name="fields[{{ n }}][type]" data-field-type>
|
||||
{% for value, label in types %}<option value="{{ value }}"{{ row.type == value ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label class="check-label field-card-required" for="{{ id }}-required"><input id="{{ id }}-required" type="checkbox" name="fields[{{ n }}][required]" value="1"{{ row.required ? ' checked' : '' }} data-field-required> Pflicht</label>
|
||||
<button type="button" class="field-toggle" data-field-toggle hidden aria-expanded="true" aria-controls="{{ id }}-body">Details<svg class="form-chevron" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg></button>
|
||||
<button type="button" class="field-remove" data-remove-field hidden aria-label="Feld entfernen" title="Feld entfernen">{% include 'admin/icon.html.twig' with {name: 'remove'} only %}</button>
|
||||
</div>
|
||||
<div class="field-card-body">
|
||||
<p class="field-card-meta" data-field-meta hidden></p>
|
||||
<div class="field-card-body" id="{{ id }}-body" data-field-body>
|
||||
<div>
|
||||
<label for="{{ id }}-name">Technischer Name</label>
|
||||
<input id="{{ id }}-name" name="fields[{{ n }}][name]" maxlength="32" value="{{ row.name }}" pattern="[a-z][a-z0-9_]*" placeholder="vorname" data-field-name>
|
||||
<p class="field-help">Kleinbuchstaben, Ziffern, Unterstrich. Steht in der E-Mail vor der Antwort.</p>
|
||||
<p class="field-help">Steht in der E-Mail vor der Antwort.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="{{ id }}-help">Hilfetext</label>
|
||||
@@ -46,12 +52,9 @@
|
||||
<label for="{{ id }}-images">Bild je Option</label>
|
||||
<textarea id="{{ id }}-images" name="fields[{{ n }}][images]" rows="2" data-field-images-input>{% for option, kennung in row.images|default({}) %}{{ option }} = {{ kennung }}
|
||||
{% endfor %}</textarea>
|
||||
<p class="field-help">Eine Zeile je Option: <code>Option = Kennung aus der Mediathek</code>. Sobald eine Option ein Bild hat, wird die Auswahl auf der Website zur Bildauswahl – anklickbare Karten statt Kästchen.</p>
|
||||
<p class="field-help">Mit Bild wird die Auswahl zu anklickbaren Karten. Ohne Skript: je Zeile <code>Option = Kennung</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-card-flags">
|
||||
<label class="check-label" for="{{ id }}-required"><input id="{{ id }}-required" type="checkbox" name="fields[{{ n }}][required]" value="1"{{ row.required ? ' checked' : '' }} data-field-required> Pflichtfeld</label>
|
||||
</div>
|
||||
<div class="field-card-number" data-field-number>
|
||||
<div>
|
||||
<label for="{{ id }}-min">Untergrenze</label>
|
||||
@@ -75,25 +78,41 @@
|
||||
<label for="{{ id }}-showvalues">… einen dieser Werte hat</label>
|
||||
<input id="{{ id }}-showvalues" name="fields[{{ n }}][showIfValues]" maxlength="500" value="{{ row.showIf.values|default([])|join(', ') }}" placeholder="Wohnung, Haus" data-field-showvalues>
|
||||
</div>
|
||||
<p class="field-help">Technischer Name eines Auswahlfeldes weiter oben, Werte durch Komma. Bei einem Schritt gilt die Bedingung für alle seine Felder. Leer: das Feld gilt immer.</p>
|
||||
<p class="field-help">Technischer Name eines Auswahlfeldes weiter oben, Werte durch Komma. Leer: das Feld gilt immer.</p>
|
||||
</div>
|
||||
{# Je weitere Sprache, was Besucher lesen (ADR 111). Leer heißt: wie in der Hauptsprache. #}
|
||||
{% for sprache in languages %}
|
||||
{% set tr = translations[sprache.code].fields[row.name]|default({}) %}
|
||||
<details class="field-translation" data-field-translation="{{ sprache.code }}">
|
||||
<summary>{{ sprache.name }}{% if tr.label|default('') %} <span class="muted">· {{ tr.label }}</span>{% endif %}</summary>
|
||||
<div class="fields">
|
||||
<div><label for="{{ id }}-tr-{{ sprache.code }}-label">Beschriftung {{ sprache.name }}</label><input id="{{ id }}-tr-{{ sprache.code }}-label" name="fields[{{ n }}][tr_{{ sprache.code }}_label]" maxlength="120" lang="{{ sprache.code }}" value="{{ tr.label|default('') }}" placeholder="{{ row.label }}"></div>
|
||||
<div><label for="{{ id }}-tr-{{ sprache.code }}-help">Hilfetext {{ sprache.name }}</label><input id="{{ id }}-tr-{{ sprache.code }}-help" name="fields[{{ n }}][tr_{{ sprache.code }}_help]" maxlength="200" lang="{{ sprache.code }}" value="{{ tr.help|default('') }}" placeholder="{{ row.help|default('') }}"></div>
|
||||
<div><label for="{{ id }}-tr-{{ sprache.code }}-options">Optionen {{ sprache.name }}</label><input id="{{ id }}-tr-{{ sprache.code }}-options" name="fields[{{ n }}][tr_{{ sprache.code }}_options]" maxlength="500" lang="{{ sprache.code }}" value="{% for option in row.options|default([]) %}{{ tr.options[option]|default('') }}{{ loop.last ? '' : ', ' }}{% endfor %}" placeholder="{{ row.options|default([])|join(', ') }}"><p class="field-help">Nur bei Auswahlfeldern: in derselben Reihenfolge, durch Komma.</p></div>
|
||||
<div><label for="{{ id }}-tr-{{ sprache.code }}-unit">Einheit {{ sprache.name }}</label><input id="{{ id }}-tr-{{ sprache.code }}-unit" name="fields[{{ n }}][tr_{{ sprache.code }}_unit]" maxlength="12" lang="{{ sprache.code }}" value="{{ tr.unit|default('') }}" placeholder="{{ row.unit|default('') }}"></div>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
{% import _self as karte %}
|
||||
{% if editing %}
|
||||
<div class="form-screens" data-form-schema="{{ schema|json_encode|e('html_attr') }}" data-form-images="{{ images|json_encode|e('html_attr') }}">
|
||||
{% for form in forms|merge([null]) %}
|
||||
{% for form in [current] %}
|
||||
{% set new = form is null %}
|
||||
{% set f = loop.index %}
|
||||
{% set rows = new ? [] : form.fields %}
|
||||
{% set settings = new ? {} : form.settings %}
|
||||
{% if new %}
|
||||
<details class="card form-new" id="neues-formular"{{ forms is empty ? ' open' : '' }}>
|
||||
<summary>Neues Formular anlegen</summary>
|
||||
<section class="card form-new" id="neues-formular">
|
||||
{% else %}
|
||||
<section class="card">
|
||||
<section class="card form-card" id="formular-{{ form.slug }}">
|
||||
<div class="form-card-summary">
|
||||
<span class="form-card-facts">{{ form.fields|length }} {{ form.fields|length == 1 ? 'Feld' : 'Felder' }} · an {{ settings.recipient|default('–') }} · Adresse <code>{{ form.slug }}</code>{% for sprache in languages %}{% if settings.translations[sprache.code]|default(null) %} · {{ sprache.code|upper }}{% endif %}{% endfor %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="{{ path('admin_form_save', {slug: new ? 'neu' : form.slug}) }}" method="post" class="form-def" data-form-editor>
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('forms') }}">
|
||||
@@ -102,15 +121,11 @@
|
||||
{# Die Zusammenfassung des Details ist keine Ueberschrift; ohne diese springt die
|
||||
Gliederung von der Seitenueberschrift direkt zu den Abschnittstiteln. #}
|
||||
<h2 class="visually-hidden">Neues Formular</h2>
|
||||
{% else %}
|
||||
<div class="card-head">
|
||||
<h2>{{ form.name }} <span class="count">{{ form.fields|length }} {{ form.fields|length == 1 ? 'Feld' : 'Felder' }}</span></h2>
|
||||
<span class="muted">Im Editor über den Formularblock wählbar · Adresse <code>{{ form.slug }}</code></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body form-editor">
|
||||
<div class="form-editor-main">
|
||||
<h3 class="form-part">Zustellung</h3>
|
||||
<details class="form-section"{{ new ? ' open' : '' }}>
|
||||
<summary><h3 class="form-part">Zustellung</h3><span class="form-section-hint">{{ new ? '' : 'an ' ~ settings.recipient|default('') ~ ' · Betreff „' ~ settings.subject|default('') ~ '“' }}</span><svg class="form-chevron" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg></summary>
|
||||
<div class="fields">
|
||||
<div>
|
||||
<label for="name-{{ f }}">Name</label>
|
||||
@@ -130,7 +145,7 @@
|
||||
<div>
|
||||
<label for="sender-{{ f }}">Absender</label>
|
||||
<input id="sender-{{ f }}" name="sender" type="email" value="{{ settings.sender|default('') }}" placeholder="formular@beispiel.de">
|
||||
<p class="field-help">Leer: die Empfängeradresse. Viele Anbieter stellen nur zu, was von einer Adresse der eigenen Domain kommt.</p>
|
||||
<p class="field-help">Leer: die Empfängeradresse. Am besten eine Adresse der eigenen Domain.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="subject-{{ f }}">Betreff der E-Mail</label>
|
||||
@@ -147,22 +162,24 @@
|
||||
</div>
|
||||
<div class="field-wide">
|
||||
<label class="check-label" for="summary-{{ f }}"><input id="summary-{{ f }}" type="checkbox" name="summary" value="1"{{ settings.summary|default(false) ? ' checked' : '' }}> Zusammenfassung vor dem Absenden zeigen</label>
|
||||
<p class="field-help">Wirkt bei Formularen mit Schritten: der letzte Schritt zeigt alle Angaben mit „Ändern“. Ohne Skript im Browser steht das Formular als Ganzes auf der Seite.</p>
|
||||
<p class="field-help">Nur bei Formularen mit Schritten.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="form-fields-head">
|
||||
<h3 class="form-part">Felder</h3>
|
||||
<button type="button" class="secondary" data-add-field hidden>{% include 'admin/icon.html.twig' with {name: 'plus'} only %}Feld hinzufügen</button>
|
||||
</div>
|
||||
<ol class="field-list" data-field-list>
|
||||
{% for row in rows %}{{ karte.field(f, loop.index0, row, types) }}{% endfor %}
|
||||
{{ karte.field(f, rows|length, {name: '', type: 'text', label: '', required: false, help: '', options: []}, types) }}
|
||||
{% for row in rows %}{{ karte.field(f, loop.index0, row, types, languages, settings.translations|default({})) }}{% endfor %}
|
||||
{{ karte.field(f, rows|length, {name: '', type: 'text', label: '', required: false, help: '', options: []}, types, languages) }}
|
||||
</ol>
|
||||
<template data-field-template>{{ karte.field(f, '__n__', {name: '', type: 'text', label: '', required: false, help: '', options: []}, types) }}</template>
|
||||
<p class="field-help" data-fields-help>Ein Feld ohne Beschriftung wird nicht gespeichert – so lässt sich ein Feld auch ohne Skript entfernen. Ein Einwilligungsfeld ist immer ein Pflichtfeld; der Zeitpunkt der Einwilligung wird mitgespeichert. Ein Feld vom Typ „Neuer Schritt“ eröffnet einen Abschnitt: seine Beschriftung ist der Titel, sein Hilfetext die Einleitung; alle Felder bis zum nächsten Schritt gehören dazu. Ein Feld oder ein Schritt kann von einer früheren Auswahl abhängen – dann verlangt der Server es nur, wenn die Bedingung zutrifft.</p>
|
||||
<template data-field-template>{{ karte.field(f, '__n__', {name: '', type: 'text', label: '', required: false, help: '', options: []}, types, languages) }}</template>
|
||||
<p class="field-help" data-fields-help>Hilfetext, Optionen, Bedingungen und Übersetzungen stehen unter „Details“. Ein „Neuer Schritt“ beginnt einen Abschnitt; ein Feld ohne Beschriftung wird nicht gespeichert.</p>
|
||||
|
||||
<h3 class="form-part">Aussehen</h3>
|
||||
<details class="form-section">
|
||||
<summary><h3 class="form-part">Aussehen</h3><span class="form-section-hint">Button „{{ settings.buttonLabel|default('Absenden') }}“</span><svg class="form-chevron" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg></summary>
|
||||
<div class="fields form-look">
|
||||
<div>
|
||||
<label for="buttonLabel-{{ f }}">Beschriftung des Buttons</label>
|
||||
@@ -175,7 +192,19 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="field-help">Leer heißt: wie die Website. Sonst eine Farbe aus der Palette oder ein Wert wie <code>#1a2b3c</code>. Form und Größe des Buttons kommen aus dem Design.</p>
|
||||
<p class="field-help">Leer heißt: wie die Website. Form und Größe des Buttons kommen aus dem Design.</p>
|
||||
</details>
|
||||
{% for sprache in languages %}
|
||||
{% set tr = settings.translations[sprache.code]|default({}) %}
|
||||
<details class="form-section">
|
||||
<summary><h3 class="form-part">{{ sprache.name }}</h3><span class="form-section-hint">{{ tr ? (tr.fields|default({})|length ~ ' von ' ~ rows|length ~ ' Feldern übersetzt') : 'noch nicht übersetzt' }}</span><svg class="form-chevron" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg></summary>
|
||||
<p class="field-help">Button und Meldung auf {{ sprache.name }}; die Felder übersetzt du unter „Details“ am Feld. Die E-Mail an dich bleibt deutsch.</p>
|
||||
<div class="fields">
|
||||
<div class="field-wide"><label for="success-{{ f }}-{{ sprache.code }}">Meldung nach dem Absenden ({{ sprache.name }})</label><textarea id="success-{{ f }}-{{ sprache.code }}" name="success_{{ sprache.code }}" maxlength="300" rows="2" lang="{{ sprache.code }}" placeholder="{{ settings.success|default('') }}">{{ tr.success|default('') }}</textarea></div>
|
||||
<div><label for="buttonLabel-{{ f }}-{{ sprache.code }}">Beschriftung des Buttons ({{ sprache.name }})</label><input id="buttonLabel-{{ f }}-{{ sprache.code }}" name="buttonLabel_{{ sprache.code }}" maxlength="40" lang="{{ sprache.code }}" value="{{ tr.buttonLabel|default('') }}" placeholder="{{ settings.buttonLabel|default('Absenden') }}"></div>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Die Vorschau: dieselben Felder, dieselben Farben, der Standardbutton der Website.
|
||||
@@ -212,13 +241,27 @@
|
||||
{% if not new %}<span class="muted">Änderungen gelten sofort auf jeder Seite, die dieses Formular zeigt.</span>{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% if new %}
|
||||
</details>
|
||||
{% else %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{# Die Übersicht: eine Zeile je Formular, ohne Editor und ohne Vorschau. #}
|
||||
{% if forms is empty %}
|
||||
<div class="empty"><h2>Noch kein Formular.</h2><p>Lege ein Formular an und setze es mit dem Baustein „Formular“ auf eine Seite.</p><p><a class="primary" href="{{ path('admin_form_edit', {slug: 'neu'}) }}">Neues Formular</a></p></div>
|
||||
{% else %}
|
||||
<div class="rows form-list">
|
||||
{% for form in forms %}
|
||||
<article class="row">
|
||||
<div class="row-main">
|
||||
<h2 class="form-list-name"><a href="{{ path('admin_form_edit', {slug: form.slug}) }}">{{ form.name }}</a></h2>
|
||||
<p>{{ form.fields|length }} {{ form.fields|length == 1 ? 'Feld' : 'Felder' }} · an {{ form.settings.recipient|default('–') }} · Adresse <code>{{ form.slug }}</code>{% for sprache in languages %}{% if form.settings.translations[sprache.code]|default(null) %} · {{ sprache.code|upper }} übersetzt{% endif %}{% endfor %}</p>
|
||||
</div>
|
||||
<span class="pill">{{ counts[form.id]|default(0) }} {{ counts[form.id]|default(0) == 1 ? 'Einsendung' : 'Einsendungen' }}</span>
|
||||
<a class="secondary" href="{{ path('admin_form_edit', {slug: form.slug}) }}" aria-label="{{ form.name }} bearbeiten">Bearbeiten</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="section-head"><h2>Einsendungen <span class="count">{{ submissions.total }}</span></h2><span class="muted">Neueste zuerst.</span></div>
|
||||
{% if submissions.items is empty %}
|
||||
@@ -238,5 +281,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'admin/pager.html.twig' with {result: submissions, route: 'admin_forms', label: 'Seiten der Einsendungen'} only %}
|
||||
{% endif %}
|
||||
<p class="footnote">KantSites · Arbeitsstand</p>
|
||||
{% endblock %}
|
||||
@@ -9,6 +9,9 @@
|
||||
<summary><strong class="menu-entry-title">{{ row.label }}</strong><span class="menu-entry-kind">{{ row.type == 'page' ? 'Seitenlink' : 'Eigener Link' }}</span><svg class="menu-chevron" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg></summary>
|
||||
<div class="menu-entry-fields">
|
||||
<label for="menu-label-{{ n }}">Beschriftung</label><input id="menu-label-{{ n }}" data-key="label" name="items[{{ n }}][label]" maxlength="80" required value="{{ row.label }}">
|
||||
{% for sprache in languages|default([]) %}
|
||||
<label for="menu-label_{{ sprache.code }}-{{ n }}">Beschriftung {{ sprache.name }}</label><input id="menu-label_{{ sprache.code }}-{{ n }}" data-key="label_{{ sprache.code }}" name="items[{{ n }}][label_{{ sprache.code }}]" maxlength="80" lang="{{ sprache.code }}"{{ sprache.rtl ? ' dir="rtl"' : '' }} value="{{ row.labels[sprache.code]|default('') }}" placeholder="Leer: wie „{{ row.label }}“">
|
||||
{% endfor %}
|
||||
{% if row.type == 'page' %}
|
||||
{% include 'admin/content-picker.html.twig' with {id: 'menu-target-' ~ n, name: 'items[' ~ n ~ '][pageId]', label: 'Verknüpfter Inhalt', selected: row.page} %}
|
||||
<label for="menu-anchor-{{ n }}">Sprungziel (optional)</label><input id="menu-anchor-{{ n }}" data-key="anchor" name="items[{{ n }}][anchor]" maxlength="65" value="{{ row.anchor }}" placeholder="kontakt">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% block content %}
|
||||
{% include 'admin/menu-icons.html.twig' %}
|
||||
<nav class="menu-slots" aria-label="Menü auswählen">{% for key, label in slots %}<a href="{{ path('admin_menus', {slot: key}) }}"{{ key == slot ? ' aria-current="page"' : '' }}>{{ label }}</a>{% endfor %}</nav>
|
||||
<form action="{{ path('admin_menu_save', {slot: slot}) }}" method="post" id="menu-editor" class="menu-workspace menu-form" data-icons="{{ menuIcons|json_encode|e('html_attr') }}" data-button-schema="{{ buttonSchema|json_encode|e('html_attr') }}">
|
||||
<form action="{{ path('admin_menu_save', {slot: slot}) }}" method="post" id="menu-editor" class="menu-workspace menu-form" data-icons="{{ menuIcons|json_encode|e('html_attr') }}" data-button-schema="{{ buttonSchema|json_encode|e('html_attr') }}" data-languages="{{ languages|json_encode|e('html_attr') }}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('menus') }}"><input type="hidden" name="expectedVersion" value="{{ menu ? menu.version : 0 }}">
|
||||
<aside class="menu-add" aria-label="Menüeinträge hinzufügen">
|
||||
<section class="card card-body"><h2>Seiten und Beiträge</h2>
|
||||
|
||||
@@ -52,6 +52,53 @@
|
||||
{% else %}<p class="field-help">Die Zuordnung kann von einem Benutzer mit Veröffentlichungsrechten geändert werden.</p>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
<section class="stack-section" id="sprachen" aria-labelledby="sprachen-title">
|
||||
<div class="section-head"><h2 id="sprachen-title">Sprachen</h2><span class="muted">Hauptsprache: {{ mainLanguage.name }}</span></div>
|
||||
<div class="card card-body">
|
||||
<p>Jede weitere Sprache übersetzt dieselben Seiten: Aufbau und Gestaltung bleiben, je Sprache kommen nur die Texte und eine eigene Adresse dazu – zum Beispiel <code>/en/services</code>. Übersetzt wird im Editor über den Sprachumschalter. Eine Seite erscheint in einer Sprache erst, wenn ihre Übersetzung veröffentlicht ist.</p>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<form action="{{ path('admin_languages_save') }}" method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
|
||||
<fieldset class="check-grid">
|
||||
<legend>Weitere Sprachen</legend>
|
||||
{% for sprache in languageCatalog %}
|
||||
<label class="check-label"><input type="checkbox" name="languages[]" value="{{ sprache.code }}"{{ sprache.code in extraLanguages ? ' checked' : '' }}> {{ sprache.name }} <span class="muted" lang="{{ sprache.code }}">{{ sprache.native }}</span></label>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<p class="field-help">Ohne Häkchen bleibt die Website einsprachig, und nirgends erscheint etwas davon. Wird eine Sprache wieder abgewählt, verschwinden ihre Seiten von der Website; die Übersetzungen bleiben gespeichert. Damit Besucher die Sprache wechseln können, setze den Baustein „Sprachwahl“ in den Kopfbereich.</p>
|
||||
<button class="primary" type="submit">Sprachen speichern</button>
|
||||
</form>
|
||||
{% if extraLanguages %}
|
||||
<form action="{{ path('admin_language_texts_save') }}" method="post" class="language-texts">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('settings') }}">
|
||||
<h3>Texte in den weiteren Sprachen</h3>
|
||||
<p class="field-help">Seiten, Kopf und Fuß übersetzt du im Editor, Menüs unter Menüs, Formulare unter Formulare. Hier stehen die Texte, die zur Website als Ganzes gehören. Leer heißt: wie in {{ mainLanguage.name }}.</p>
|
||||
{% for code in extraLanguages %}
|
||||
{% set name = languageCatalog|filter(s => s.code == code)|first.name %}
|
||||
<fieldset class="language-text-group">
|
||||
<legend>{{ name }}</legend>
|
||||
{% if settings.consentBanner %}
|
||||
<label for="consent-title-{{ code }}">Überschrift des Einwilligungshinweises</label>
|
||||
<input id="consent-title-{{ code }}" name="consentTexts[{{ code }}][title]" maxlength="80" lang="{{ code }}" value="{{ settings.consentTexts[code].title|default('') }}" placeholder="{{ settings.consentTitle ?: 'Externe Inhalte und Einwilligung' }}">
|
||||
<label for="consent-text-{{ code }}">Text des Einwilligungshinweises</label>
|
||||
<textarea id="consent-text-{{ code }}" name="consentTexts[{{ code }}][text]" maxlength="600" rows="3" lang="{{ code }}" placeholder="{{ settings.consentText }}">{{ settings.consentTexts[code].text|default('') }}</textarea>
|
||||
{% endif %}
|
||||
{% for term in terms %}
|
||||
{% set key = term.kind ~ ':' ~ term.slug %}
|
||||
<label for="term-{{ code }}-{{ term.id }}">{{ term.kind == 'category' ? 'Kategorie' : 'Schlagwort' }} „{{ term.name }}“</label>
|
||||
<input id="term-{{ code }}-{{ term.id }}" name="termNames[{{ code }}][{{ key }}]" maxlength="120" lang="{{ code }}" value="{{ settings.termNames[code][key]|default('') }}" placeholder="{{ term.name }}">
|
||||
{% endfor %}
|
||||
{% if not settings.consentBanner and terms is empty %}<p class="field-help">Ohne Einwilligungshinweis und ohne Kategorien gibt es hier nichts zu übersetzen.</p>{% endif %}
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
<button class="primary" type="submit">Übersetzungen speichern</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="field-help">{{ extraLanguages ? 'Weitere Sprachen: ' ~ extraLanguages|map(c => c|upper)|join(', ') ~ '.' : 'Die Website ist einsprachig.' }} Ändern kann das ein Administrator.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
<section class="stack-section">
|
||||
<div class="section-head"><h2>Unternehmensdaten</h2><span class="muted">Grundlage für Organization und LocalBusiness</span></div>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
|
||||
Reference in new issue
Block a user