feat: improved i18n and l10n

This commit is contained in:
2026-07-16 11:16:38 +02:00
parent e504971740
commit 55410d675b
6 changed files with 338 additions and 92 deletions
+183
View File
@@ -0,0 +1,183 @@
msgid ""
msgstr ""
"Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
# crochet/templates/crochet/pattern.html
msgid "No se ha podido guardar. Inténtalo de nuevo."
msgstr "Couldn't save. Please try again."
msgid "Guardar"
msgstr "Save"
msgid "Añadir sección"
msgstr "Add section"
msgid "¿Seguro que quieres cargar el patrón guardado? Se perderán los cambios que no hayas guardado."
msgstr "Are you sure you want to load the saved pattern? Any unsaved changes will be lost."
msgid "Cargar"
msgstr "Load"
msgid "Exportar a PDF"
msgstr "Export to PDF"
msgid "Guardado"
msgstr "Saved"
msgid "Personalización de página"
msgstr "Page customization"
msgid "Título del patrón"
msgstr "Pattern title"
msgid "Título del patrón..."
msgstr "Pattern title..."
msgid "Autor"
msgstr "Author"
msgid "Autor..."
msgstr "Author..."
msgid "Color del texto"
msgstr "Text color"
msgid "Color de acento (títulos)"
msgstr "Accent color (headings)"
msgid "Color de fondo"
msgstr "Background color"
msgid "Tipografía"
msgstr "Font"
msgid "Monoespaciada"
msgstr "Monospace"
msgid "Tamaño de letra"
msgstr "Font size"
msgid "Pequeño"
msgstr "Small"
msgid "Normal"
msgstr "Normal"
msgid "Grande"
msgstr "Large"
msgid "Alineación del texto"
msgstr "Text alignment"
msgid "Izquierda"
msgstr "Left"
msgid "Centrado"
msgstr "Center"
msgid "Justificado"
msgstr "Justify"
msgid "Tamaño de página"
msgstr "Page size"
msgid "Carta (Letter)"
msgstr "Letter"
msgid "Orientación"
msgstr "Orientation"
msgid "Vertical"
msgstr "Portrait"
msgid "Horizontal"
msgstr "Landscape"
msgid "Secciones"
msgstr "Sections"
msgid "Vista previa"
msgstr "Preview"
msgid "Colapsar todo"
msgstr "Collapse all"
msgid "Expandir todo"
msgstr "Expand all"
msgid "Título"
msgstr "Title"
msgid "Título..."
msgstr "Title..."
msgid "Subtítulo"
msgstr "Subtitle"
msgid "Subtítulo..."
msgstr "Subtitle..."
msgid "Texto"
msgstr "Text"
msgid "Escribe aquí..."
msgstr "Write here..."
msgid "Nota"
msgstr "Note"
msgid "Escribe una nota o consejo..."
msgstr "Write a note or tip..."
msgid "Materiales"
msgstr "Materials"
msgid "Añadir material..."
msgstr "Add material..."
msgid "Añadir línea"
msgstr "Add line"
msgid "Imagen"
msgstr "Image"
msgid "No se ha podido subir la imagen. Inténtalo de nuevo."
msgstr "Couldn't upload the image. Please try again."
msgid "Patrón"
msgstr "Pattern"
msgid "Elementos"
msgstr "Elements"
msgid "Grupo"
msgstr "Group"
msgid "Arrastrar para reordenar"
msgstr "Drag to reorder"
msgid "Colapsar / expandir"
msgstr "Collapse / expand"
msgid "Duplicar sección"
msgstr "Duplicate section"
msgid "Eliminar sección"
msgstr "Delete section"
msgid "¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las secciones que contiene."
msgstr "Are you sure you want to delete this group? All sections inside it will also be deleted."
msgid "Añade una sección para ver aquí el resultado."
msgstr "Add a section to see the result here."
# crochet/views.py (PatternDetailView.EMPTY_MESSAGE)
msgid "Este patrón todavía no tiene contenido."
msgstr "This pattern doesn't have any content yet."
+13
View File
@@ -5,9 +5,13 @@
import { renderOutput } from './render.js';
import { renderPageMeta, applyPageSettings } from './page-settings.js';
import { applyDefaultSkeleton, buildSectionsFrom } from './section-types.js';
import { setAllSectionsCollapsed } from './sections.js';
import './tabs.js';
import './storage.js';
// Mismo punto de corte que .panel-hidden-mobile (ver css/main.css).
const MOBILE_BREAKPOINT_QUERY = '(max-width: 767px)';
// El propio patrón (sections/pageSettings ya guardados) viaja embebido en la
// plantilla Django, ver {{ pattern_data|json_script:"pattern-data" }} en
// pattern.html. Si todavía no tiene ninguna sección (patrón recién creado),
@@ -25,5 +29,14 @@ function loadInitialPattern() {
}
loadInitialPattern();
// En pantallas estrechas arranca con todas las secciones colapsadas (solo
// se ve el badge de cada una) en vez de todos sus campos editables a la
// vez: con un patrón ya guardado (varias secciones o grupos) ese "ruido"
// inicial era mucho en móvil.
if (window.matchMedia(MOBILE_BREAKPOINT_QUERY).matches) {
setAllSectionsCollapsed(true);
}
renderPageMeta();
renderOutput();
+36 -7
View File
@@ -54,17 +54,38 @@ function addGroupSection(data, children = [], container = sectionsContainer) {
const groupContainer = document.createElement('div');
groupContainer.className = 'group-container flex flex-col gap-4';
const toolbar = document.createElement('div');
toolbar.className = 'flex flex-wrap gap-2 mb-4';
// Mismo menú desplegable que la barra superior (ver pattern.html), no una
// fila de botones sueltos: un grupo anidado multiplicaba ese ruido visual
// en vez de reducirlo.
const dropdown = document.createElement('div');
dropdown.className = 'dropdown mb-2';
const dropdownBtn = document.createElement('button');
dropdownBtn.type = 'button';
dropdownBtn.className = 'btn btn-sm';
dropdownBtn.tabIndex = 0;
dropdownBtn.textContent = `+ ${addSectionLabel}`;
const menu = document.createElement('ul');
menu.tabIndex = 0;
menu.className = 'dropdown-content menu menu-sm bg-base-100 border border-base-300 rounded-box z-10 w-52 p-2 shadow';
SECTION_TYPES.forEach(({ button, create }) => {
const btn = button.cloneNode(true);
btn.removeAttribute('id'); // Puede haber varios grupos, cada uno con su propia copia de estos botones.
btn.addEventListener('click', () => create(undefined, groupContainer));
toolbar.appendChild(btn);
btn.addEventListener('click', () => {
create(undefined, groupContainer);
btn.blur(); // Cierra el menú (daisyUI lo abre con :focus-within) tras elegir.
});
const item = document.createElement('li');
item.appendChild(btn);
menu.appendChild(item);
});
body.appendChild(toolbar);
dropdown.appendChild(dropdownBtn);
dropdown.appendChild(menu);
body.appendChild(dropdown);
body.appendChild(groupContainer);
container.appendChild(section);
@@ -73,6 +94,11 @@ function addGroupSection(data, children = [], container = sectionsContainer) {
buildSectionsFrom(children, groupContainer);
}
// Etiqueta de "+ Añadir sección" compartida por la barra superior y por el
// menú propio de cada "Grupo" (ver addGroupSection): un solo dato en el
// HTML (ver pattern.html), traducible por la plantilla Django.
const addSectionLabel = document.getElementById('add-section-toggle').dataset.label;
const addTitleBtn = document.getElementById('add-title-section');
const addSubtitleBtn = document.getElementById('add-subtitle-section');
const addTextBtn = document.getElementById('add-text-section');
@@ -432,9 +458,12 @@ sectionsContainer.addEventListener('section:duplicate', (e) => duplicateSection(
// addGroupSection() para el de dentro de un grupo).
setupSectionsReorder(sectionsContainer, panelTop);
// Botones de la barra superior para añadir cada tipo de sección. Los textos
// Botones del menú "+ Añadir sección" de la barra superior. Los textos
// (etiqueta de tipo, placeholders...) se leen de sus data-* en el HTML, no
// están escritos aquí, para que la plantilla Django pueda traducirlos.
SECTION_TYPES.forEach(({ button, create }) => {
button.addEventListener('click', () => create(undefined, undefined));
button.addEventListener('click', () => {
create(undefined, undefined);
button.blur(); // Cierra el menú (daisyUI lo abre con :focus-within) tras elegir.
});
});
+30 -24
View File
@@ -380,8 +380,24 @@ export function addImageSection(data, image = null, container = sectionsContaine
export function addDndSection(data, items = [], container = sectionsContainer) {
const { section, body } = createSectionShell(data.typeLabel);
const row = document.createElement('div');
row.className = 'flex flex-col sm:flex-row gap-4';
// Siempre en columna (antes cambiaba a fila a partir de "sm", con los
// elementos arrastrables a la derecha): con el catálogo ya grande, esa
// barra lateral quedaba demasiado estrecha para envolver los tipos de
// punto con comodidad, así que ahora va arriba a todo lo ancho.
const column = document.createElement('div');
column.className = 'flex flex-col gap-4';
const elementsPanel = document.createElement('div');
elementsPanel.className = 'w-full border border-base-300 rounded-box p-4';
const elementsTitle = document.createElement('h3');
elementsTitle.className = 'font-semibold mb-2';
elementsTitle.textContent = data.elementsLabel;
const list = createElementsList();
elementsPanel.appendChild(elementsTitle);
elementsPanel.appendChild(list);
const dndCanvas = document.createElement('div');
// content-start (align-content: flex-start): sin esto, un flex-wrap con
@@ -393,21 +409,9 @@ export function addDndSection(data, items = [], container = sectionsContainer) {
// debajo, no repartido.
dndCanvas.className = 'dnd-canvas flex-1 flex flex-wrap content-start items-center gap-2 min-h-24 py-2';
const elementsPanel = document.createElement('aside');
elementsPanel.className = 'w-full sm:w-48 sm:shrink-0 border border-base-300 rounded-box p-4';
const elementsTitle = document.createElement('h3');
elementsTitle.className = 'font-semibold mb-2';
elementsTitle.textContent = data.elementsLabel;
const list = createElementsList();
elementsPanel.appendChild(elementsTitle);
elementsPanel.appendChild(list);
row.appendChild(dndCanvas);
row.appendChild(elementsPanel);
body.appendChild(row);
column.appendChild(elementsPanel);
column.appendChild(dndCanvas);
body.appendChild(column);
container.appendChild(section);
setupDropZone(dndCanvas, undefined, section);
@@ -418,21 +422,26 @@ export function addDndSection(data, items = [], container = sectionsContainer) {
// Colapsar / expandir todas las secciones de una vez
// ---------------------------------------------------------------------
const toggleCollapseAllBtn = document.getElementById('toggle-collapse-all');
// Colapsa o expande a la vez todas las secciones que haya, sin importar su
// nivel de anidamiento (querySelectorAll recorre también las que están
// dentro de un "Grupo"), sincronizando tanto su cuerpo como el icono de su
// propio botón de colapsar individual.
function setAllSectionsCollapsed(collapsed) {
// propio botón de colapsar individual y la etiqueta de "Colapsar todo" /
// "Expandir todo". Exportada para poder arrancar ya colapsado en móvil (ver
// main.js), no solo desde el propio botón.
export function setAllSectionsCollapsed(collapsed) {
sectionsContainer.querySelectorAll('.section-body').forEach(body => {
body.classList.toggle('hidden', collapsed);
});
sectionsContainer.querySelectorAll('.collapse-btn').forEach(btn => {
btn.textContent = collapsed ? '▸' : '▾';
});
toggleCollapseAllBtn.textContent = collapsed
? toggleCollapseAllBtn.dataset.expandAllLabel
: toggleCollapseAllBtn.dataset.collapseAllLabel;
}
const toggleCollapseAllBtn = document.getElementById('toggle-collapse-all');
toggleCollapseAllBtn.addEventListener('click', () => {
// Se decide según el estado actual (en vez de recordar uno propio) para
// que el botón siga teniendo sentido aunque el usuario haya colapsado o
@@ -441,7 +450,4 @@ toggleCollapseAllBtn.addEventListener('click', () => {
const anyExpanded = Array.from(sectionsContainer.querySelectorAll('.section-body'))
.some(body => !body.classList.contains('hidden'));
setAllSectionsCollapsed(anyExpanded);
toggleCollapseAllBtn.textContent = anyExpanded
? toggleCollapseAllBtn.dataset.expandAllLabel
: toggleCollapseAllBtn.dataset.collapseAllLabel;
});
+69 -58
View File
@@ -1,4 +1,4 @@
{% load static %}
{% load static i18n %}
<!DOCTYPE html>
<html lang="{{ lang }}">
@@ -34,10 +34,10 @@
</select>
<button id="save-pattern" type="button" class="btn btn-sm btn-outline"
data-save-url="{% url 'crochet:pattern_save' pattern.uuid %}"
data-error-message="No se ha podido guardar. Inténtalo de nuevo.">Guardar</button>
data-error-message="{% trans 'No se ha podido guardar. Inténtalo de nuevo.' %}">{% trans 'Guardar' %}</button>
<button id="load-pattern" type="button" class="btn btn-sm btn-outline"
data-confirm-message="¿Seguro que quieres cargar el patrón guardado? Se perderán los cambios que no hayas guardado.">Cargar</button>
<button id="export-pdf" type="button" class="btn btn-sm">Exportar a PDF</button>
data-confirm-message="{% trans '¿Seguro que quieres cargar el patrón guardado? Se perderán los cambios que no hayas guardado.' %}">{% trans 'Cargar' %}</button>
<button id="export-pdf" type="button" class="btn btn-sm">{% trans 'Exportar a PDF' %}</button>
</div>
</header>
@@ -45,7 +45,7 @@
localStorage no da ninguna señal de que ha ocurrido algo. -->
<div id="save-toast" class="toast toast-top toast-end no-print hidden">
<div class="alert alert-success">
<span data-message="Guardado">Guardado</span>
<span data-message="{% trans 'Guardado' %}">{% trans 'Guardado' %}</span>
</div>
</div>
@@ -56,72 +56,72 @@
de #panel-top-sections. <details> nativo para no necesitar JS
solo para plegarla/desplegarla. -->
<details class="border border-base-300 rounded-box p-4 mb-4 no-print">
<summary class="text-lg font-semibold cursor-pointer select-none">Personalización de página</summary>
<summary class="text-lg font-semibold cursor-pointer select-none">{% trans 'Personalización de página' %}</summary>
<div class="flex flex-col gap-4 mt-4">
<div class="flex flex-col sm:flex-row gap-4">
<label class="flex flex-col gap-1 flex-1">
<span class="text-sm">Título del patrón</span>
<input id="page-title-input" type="text" class="input input-bordered" placeholder="Título del patrón...">
<span class="text-sm">{% trans 'Título del patrón' %}</span>
<input id="page-title-input" type="text" class="input input-bordered" placeholder="{% trans 'Título del patrón...' %}">
</label>
<label class="flex flex-col gap-1 flex-1">
<span class="text-sm">Autor</span>
<input id="page-author-input" type="text" class="input input-bordered" placeholder="Autor...">
<span class="text-sm">{% trans 'Autor' %}</span>
<input id="page-author-input" type="text" class="input input-bordered" placeholder="{% trans 'Autor...' %}">
</label>
</div>
<div class="flex flex-wrap items-end gap-4">
<label class="flex flex-col gap-1">
<span class="text-sm">Color del texto</span>
<span class="text-sm">{% trans 'Color del texto' %}</span>
<input id="page-text-color" type="color" value="#000000">
</label>
<label class="flex flex-col gap-1">
<span class="text-sm">Color de acento (títulos)</span>
<span class="text-sm">{% trans 'Color de acento (títulos)' %}</span>
<input id="page-accent-color" type="color" value="#1d4ed8">
</label>
<label class="flex flex-col gap-1">
<span class="text-sm">Color de fondo</span>
<span class="text-sm">{% trans 'Color de fondo' %}</span>
<input id="page-bg-color" type="color" value="#ffffff">
</label>
<label class="flex flex-col gap-1">
<span class="text-sm">Tipografía</span>
<span class="text-sm">{% trans 'Tipografía' %}</span>
<select id="page-font-select" class="select select-bordered select-sm">
<option value="sans-serif" selected>Sans-serif</option>
<option value="serif">Serif</option>
<option value="Georgia, serif">Georgia</option>
<option value="'Trebuchet MS', sans-serif">Trebuchet MS</option>
<option value="'Courier New', monospace">Monoespaciada</option>
<option value="'Courier New', monospace">{% trans 'Monoespaciada' %}</option>
</select>
</label>
<label class="flex flex-col gap-1">
<span class="text-sm">Tamaño de letra</span>
<span class="text-sm">{% trans 'Tamaño de letra' %}</span>
<select id="page-font-size-select" class="select select-bordered select-sm">
<option value="0.875rem">Pequeño</option>
<option value="1rem" selected>Normal</option>
<option value="1.25rem">Grande</option>
<option value="0.875rem">{% trans 'Pequeño' %}</option>
<option value="1rem" selected>{% trans 'Normal' %}</option>
<option value="1.25rem">{% trans 'Grande' %}</option>
</select>
</label>
<label class="flex flex-col gap-1">
<span class="text-sm">Alineación del texto</span>
<span class="text-sm">{% trans 'Alineación del texto' %}</span>
<select id="page-align-select" class="select select-bordered select-sm">
<option value="left" selected>Izquierda</option>
<option value="center">Centrado</option>
<option value="justify">Justificado</option>
<option value="left" selected>{% trans 'Izquierda' %}</option>
<option value="center">{% trans 'Centrado' %}</option>
<option value="justify">{% trans 'Justificado' %}</option>
</select>
</label>
<label class="flex flex-col gap-1">
<span class="text-sm">Tamaño de página</span>
<span class="text-sm">{% trans 'Tamaño de página' %}</span>
<select id="page-size-select" class="select select-bordered select-sm">
<option value="A4" selected>A4</option>
<option value="A3">A3</option>
<option value="A5">A5</option>
<option value="letter">Carta (Letter)</option>
<option value="letter">{% trans 'Carta (Letter)' %}</option>
<option value="legal">Legal</option>
</select>
</label>
<label class="flex flex-col gap-1">
<span class="text-sm">Orientación</span>
<span class="text-sm">{% trans 'Orientación' %}</span>
<select id="page-orientation-select" class="select select-bordered select-sm">
<option value="portrait" selected>Vertical</option>
<option value="landscape">Horizontal</option>
<option value="portrait" selected>{% trans 'Vertical' %}</option>
<option value="landscape">{% trans 'Horizontal' %}</option>
</select>
</label>
</div>
@@ -129,51 +129,62 @@
</details>
<div class="tabs tabs-boxed w-fit mb-4 md:hidden no-print">
<button id="tab-sections" type="button" class="tab tab-active">Secciones</button>
<button id="tab-output" type="button" class="tab">Vista previa</button>
<button id="tab-sections" type="button" class="tab tab-active">{% trans 'Secciones' %}</button>
<button id="tab-output" type="button" class="tab">{% trans 'Vista previa' %}</button>
</div>
<div class="flex flex-col md:flex-row gap-4">
<section id="panel-top" class="w-full md:w-1/2 border border-base-300 rounded-box p-4 no-print">
<div class="flex items-center justify-between mb-2">
<h2 class="text-lg font-semibold">Secciones</h2>
<h2 class="text-lg font-semibold">{% trans 'Secciones' %}</h2>
<button id="toggle-collapse-all" type="button" class="btn btn-xs btn-outline"
data-collapse-all-label="Colapsar todo" data-expand-all-label="Expandir todo">Colapsar todo</button>
data-collapse-all-label="{% trans 'Colapsar todo' %}" data-expand-all-label="{% trans 'Expandir todo' %}">{% trans 'Colapsar todo' %}</button>
</div>
<div class="flex flex-wrap gap-2 mb-4">
<button id="add-title-section" type="button" class="btn btn-sm"
data-type-label="Título" data-placeholder="Título...">+ Título</button>
<button id="add-subtitle-section" type="button" class="btn btn-sm"
data-type-label="Subtítulo" data-placeholder="Subtítulo...">+ Subtítulo</button>
<button id="add-text-section" type="button" class="btn btn-sm"
data-type-label="Texto" data-placeholder="Escribe aquí...">+ Texto</button>
<button id="add-note-section" type="button" class="btn btn-sm"
data-type-label="Nota" data-placeholder="Escribe una nota o consejo..."
data-text-color-label="Color del texto" data-bg-color-label="Color de fondo">+ Nota</button>
<button id="add-materials-section" type="button" class="btn btn-sm"
data-type-label="Materiales" data-placeholder="Añadir material..." data-add-line-label="+ Añadir línea">+ Materiales</button>
<button id="add-image-section" type="button" class="btn btn-sm"
data-type-label="Imagen"
<!-- Un único botón con menú (en vez de 7-8 botones sueltos que se
partían en varias filas en pantallas estrechas): menos ruido
visual antes de llegar al contenido real. daisyUI abre
.dropdown-content con :focus-within, sin JS propio; el mismo
botón (clonado) se reutiliza dentro de cada "Grupo" (ver
addGroupSection en section-types.js), así que su
data-label también viaja con él. -->
<div class="dropdown mb-4">
<button id="add-section-toggle" type="button" class="btn btn-sm" tabindex="0"
data-label="{% trans 'Añadir sección' %}">+ {% trans 'Añadir sección' %}</button>
<ul tabindex="0" class="dropdown-content menu menu-sm bg-base-100 border border-base-300 rounded-box z-10 w-52 p-2 shadow">
<li><button id="add-title-section" type="button"
data-type-label="{% trans 'Título' %}" data-placeholder="{% trans 'Título...' %}">{% trans 'Título' %}</button></li>
<li><button id="add-subtitle-section" type="button"
data-type-label="{% trans 'Subtítulo' %}" data-placeholder="{% trans 'Subtítulo...' %}">{% trans 'Subtítulo' %}</button></li>
<li><button id="add-text-section" type="button"
data-type-label="{% trans 'Texto' %}" data-placeholder="{% trans 'Escribe aquí...' %}">{% trans 'Texto' %}</button></li>
<li><button id="add-note-section" type="button"
data-type-label="{% trans 'Nota' %}" data-placeholder="{% trans 'Escribe una nota o consejo...' %}"
data-text-color-label="{% trans 'Color del texto' %}" data-bg-color-label="{% trans 'Color de fondo' %}">{% trans 'Nota' %}</button></li>
<li><button id="add-materials-section" type="button"
data-type-label="{% trans 'Materiales' %}" data-placeholder="{% trans 'Añadir material...' %}" data-add-line-label="+ {% trans 'Añadir línea' %}">{% trans 'Materiales' %}</button></li>
<li><button id="add-image-section" type="button"
data-type-label="{% trans 'Imagen' %}"
data-upload-url="{% url 'crochet:pattern_image_upload' pattern.uuid %}"
data-upload-error-message="No se ha podido subir la imagen. Inténtalo de nuevo.">+ Imagen</button>
<button id="add-dnd-section" type="button" class="btn btn-sm"
data-type-label="Patrón" data-elements-label="Elementos">+ Patrón</button>
<button id="add-group-section" type="button" class="btn btn-sm"
data-type-label="Grupo">+ Grupo</button>
data-upload-error-message="{% trans 'No se ha podido subir la imagen. Inténtalo de nuevo.' %}">{% trans 'Imagen' %}</button></li>
<li><button id="add-dnd-section" type="button"
data-type-label="{% trans 'Patrón' %}" data-elements-label="{% trans 'Elementos' %}">{% trans 'Patrón' %}</button></li>
<li><button id="add-group-section" type="button"
data-type-label="{% trans 'Grupo' %}">{% trans 'Grupo' %}</button></li>
</ul>
</div>
<div id="panel-top-sections" class="flex flex-col gap-4"
data-drag-title="Arrastrar para reordenar"
data-collapse-title="Colapsar / expandir"
data-duplicate-title="Duplicar sección"
data-remove-title="Eliminar sección"
data-remove-confirm="¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las secciones que contiene."></div>
data-drag-title="{% trans 'Arrastrar para reordenar' %}"
data-collapse-title="{% trans 'Colapsar / expandir' %}"
data-duplicate-title="{% trans 'Duplicar sección' %}"
data-remove-title="{% trans 'Eliminar sección' %}"
data-remove-confirm="{% trans '¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las secciones que contiene.' %}"></div>
</section>
<section id="panel-output" class="w-full md:w-1/2 border border-base-300 rounded-box p-4 panel-hidden-mobile">
<h2 id="preview-title" class="text-lg font-semibold mb-2 no-print">Vista previa</h2>
<h2 id="preview-title" class="text-lg font-semibold mb-2 no-print">{% trans 'Vista previa' %}</h2>
<div id="panel-output-text"
data-empty-message="Añade una sección para ver aquí el resultado."></div>
data-empty-message="{% trans 'Añade una sección para ver aquí el resultado.' %}"></div>
</section>
</div>
+6 -2
View File
@@ -2,7 +2,7 @@ import json
from django.http import HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import get_language
from django.utils.translation import get_language, gettext_lazy as _
from django.views import View
from django.views.generic import DetailView
@@ -57,7 +57,11 @@ class PatternDetailView(DetailView):
slug_field = 'uuid'
slug_url_kwarg = 'uuid'
EMPTY_MESSAGE = 'Este patrón todavía no tiene contenido.'
# gettext_lazy (no gettext): se evalúa al construir el HTML de salida
# (dentro de render_pattern_html, vía format_html), no aquí en la
# definición de la clase, así que respeta el idioma activo de cada
# petición aunque este atributo se cree una sola vez al importar el módulo.
EMPTY_MESSAGE = _('Este patrón todavía no tiene contenido.')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)