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
@@ -250,10 +250,35 @@ if (root) {
|
||||
if (help) help.textContent = 'Ein E-Mail-Feld des Formulars. Dann lässt sich auf die Nachricht direkt antworten.';
|
||||
};
|
||||
|
||||
// Eine Feldkarte ist eine Zeile; die Details öffnet man bei Bedarf. Darunter steht kurz, was
|
||||
// in ihnen eingestellt ist, damit man nicht jede Karte aufklappen muss.
|
||||
const setOpen = (card, open) => {
|
||||
card.querySelector('[data-field-body]').hidden = !open;
|
||||
const toggle = card.querySelector('[data-field-toggle]');
|
||||
toggle.setAttribute('aria-expanded', String(open));
|
||||
card.classList.toggle('is-open', open);
|
||||
};
|
||||
const meta = card => {
|
||||
const type = card.querySelector('[data-field-type]').value;
|
||||
const parts = [card.querySelector('[data-field-name]').value.trim()];
|
||||
const options = card.querySelector('[data-field-options-input]').value.split(',').map(o => o.trim()).filter(Boolean);
|
||||
if (['select', 'radio', 'checkboxes'].includes(type)) parts.push(options.length ? options.join(', ') : 'noch keine Optionen');
|
||||
if (card.querySelector('[data-field-help]').value.trim()) parts.push('Hilfetext');
|
||||
if (card.querySelector('[data-field-showif]').value.trim()) parts.push('mit Bedingung');
|
||||
for (const translation of card.querySelectorAll('[data-field-translation]')) {
|
||||
if ([...translation.querySelectorAll('input')].some(input => input.value.trim())) parts.push(`${translation.dataset.fieldTranslation.toUpperCase()} ✓`);
|
||||
}
|
||||
return parts.filter(Boolean).join(' · ');
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
cards().forEach((card, index) => {
|
||||
card.querySelector('[data-field-index]').textContent = String(index + 1);
|
||||
const type = card.querySelector('[data-field-type]').value;
|
||||
const line = card.querySelector('[data-field-meta]');
|
||||
line.textContent = meta(card); line.hidden = !line.textContent || card.classList.contains('is-open');
|
||||
const label = card.querySelector('[data-field-label]').value.trim() || `Feld ${index + 1}`;
|
||||
card.querySelector('[data-field-toggle]').setAttribute('aria-label', `Details zu „${label}“`);
|
||||
card.querySelector('[data-field-options]').hidden = !['select', 'radio', 'checkboxes'].includes(type);
|
||||
card.querySelector('[data-field-images]').hidden = !['radio', 'checkboxes'].includes(type);
|
||||
if (['radio', 'checkboxes'].includes(type)) renderImagePickers(card);
|
||||
@@ -276,12 +301,19 @@ if (root) {
|
||||
const empty = cards().filter(card => card.classList.contains('field-card-new'));
|
||||
if (cards().length > empty.length) empty.forEach(card => card.remove());
|
||||
add.hidden = false;
|
||||
for (const card of cards()) {
|
||||
card.querySelector('[data-field-toggle]').hidden = false;
|
||||
setOpen(card, card.classList.contains('field-card-new'));
|
||||
}
|
||||
add.addEventListener('click', () => {
|
||||
list.insertAdjacentHTML('beforeend', template.innerHTML.replaceAll('__n__', String(next++)));
|
||||
const card = list.lastElementChild; card.classList.remove('field-card-new');
|
||||
card.querySelector('[data-field-toggle]').hidden = false; setOpen(card, true);
|
||||
refresh(); card.querySelector('[data-field-label]').focus();
|
||||
});
|
||||
list.addEventListener('click', event => {
|
||||
const toggle = event.target.closest('[data-field-toggle]');
|
||||
if (toggle) { const card = toggle.closest('[data-field]'); setOpen(card, !card.classList.contains('is-open')); refresh(); return; }
|
||||
const button = event.target.closest('[data-remove-field]');
|
||||
if (!button) return;
|
||||
button.closest('[data-field]').remove(); refresh(); add.focus();
|
||||
|
||||
Reference in new issue
Block a user