feat: support for multi language

This commit is contained in:
2026-07-13 12:09:18 +02:00
parent 50c07e83ac
commit bfb020531b
2 changed files with 85 additions and 14 deletions
+75 -13
View File
@@ -4,6 +4,39 @@ document.addEventListener('DOMContentLoaded', () => {
const panelTop = document.getElementById('panel-top'); // Todo el panel "Secciones" (incluye los botones "+ tipo"), usado como zona de detección al reordenar.
const sectionsContainer = document.getElementById('panel-top-sections'); // Contiene las secciones que el usuario va añadiendo.
const draggableItemsSource = document.getElementById('draggable-items-source'); // Lista oculta con los elementos disponibles para arrastrar: fuente de datos única, se clona (visible) en cada sección de tipo "arrastrar y soltar".
const languageSelect = document.getElementById('language-select'); // Idioma del contenido (no de la interfaz): controla qué versión de los textos de título/subtítulo/texto se muestra y edita.
// Idioma actualmente seleccionado para el contenido. La estructura de
// secciones (cuáles hay, su orden, su tipo) es la misma para todos los
// idiomas; lo único que cambia es qué texto se muestra en cada campo de
// título/subtítulo/texto libre.
let currentLanguage = languageSelect.value;
// ---------------------------------------------------------------------
// Multi-idioma del contenido: título, subtítulo y texto libre guardan un
// texto por idioma en vez de uno solo, para poder editarlos y asociarlos
// al idioma seleccionado sin perder lo ya escrito en los demás.
// ---------------------------------------------------------------------
// Cada input/textarea traducible guarda su propio mapa { idioma: texto }
// serializado en el propio elemento (data-translations), así viaja solo
// con guardar/cargar la sección (ver serializeSection/addTitleSection...),
// sin necesidad de un almacén aparte que haya que mantener sincronizado.
function getTranslations(el) {
try {
return JSON.parse(el.dataset.translations || '{}');
} catch {
return {};
}
}
// Actualiza, dentro del mapa ya guardado en el elemento, el texto del
// idioma indicado (por defecto el actual), sin tocar el resto de idiomas.
function setTranslation(el, text, lang = currentLanguage) {
const translations = getTranslations(el);
translations[lang] = text;
el.dataset.translations = JSON.stringify(translations);
}
// ---------------------------------------------------------------------
// Reordenar secciones (arrastrando toda la cabecera de cada una)
@@ -407,10 +440,31 @@ document.addEventListener('DOMContentLoaded', () => {
// añadir/quitar/reordenar secciones o elementos soltados (MutationObserver).
// Al estar en #panel-top-sections y usar subtree:true, esto ya cubre
// también los cambios dentro de las secciones anidadas en un "Grupo".
sectionsContainer.addEventListener('input', renderOutput);
// Si el campo es traducible (título/subtítulo/texto), cada pulsación de
// tecla también se guarda en su mapa de traducciones para el idioma
// actual, así no hace falta un paso aparte de "guardar" al cambiar de
// idioma o al serializar la sección.
sectionsContainer.addEventListener('input', (e) => {
if (e.target.matches('.title-input, .subtitle-input, textarea')) {
setTranslation(e.target, e.target.value);
}
renderOutput();
});
sectionsContainer.addEventListener('change', renderOutput);
new MutationObserver(renderOutput).observe(sectionsContainer, { childList: true, subtree: true });
// Al cambiar el idioma del contenido, se sustituye en cada campo
// traducible (a cualquier nivel de anidamiento) el texto mostrado por el
// que tenga guardado para el nuevo idioma (vacío si aún no se ha escrito
// nada en ese idioma), y se repinta la vista previa.
languageSelect.addEventListener('change', () => {
currentLanguage = languageSelect.value;
sectionsContainer.querySelectorAll('.title-input, .subtitle-input, textarea').forEach(el => {
el.value = getTranslations(el)[currentLanguage] || '';
});
renderOutput();
});
// ---------------------------------------------------------------------
// Creación de secciones (título, texto, imagen, drag & drop, grupo)
// ---------------------------------------------------------------------
@@ -544,17 +598,20 @@ document.addEventListener('DOMContentLoaded', () => {
// Sección de título: un input de una línea que se muestra como <h3> en el
// resultado. `data` es el dataset del botón "+ Título" (data-type-label,
// data-placeholder). `text`, si se pasa, precarga el valor (se usa al
// restaurar un patrón guardado). `container` es dónde se añade la
// sección: #panel-top-sections por defecto, o el de un grupo.
function addTitleSection(data, text = '', container = sectionsContainer) {
// data-placeholder). `translations`, si se pasa, es el mapa { idioma:
// texto } completo (se usa al restaurar un patrón guardado o al
// duplicar una sección); se muestra el que corresponda al idioma actual.
// `container` es dónde se añade la sección: #panel-top-sections por
// defecto, o el de un grupo.
function addTitleSection(data, translations = {}, container = sectionsContainer) {
const { section, body } = createSectionShell(data.typeLabel);
const input = document.createElement('input');
input.type = 'text';
input.className = 'title-input input input-bordered w-full';
input.placeholder = data.placeholder;
input.value = text;
input.dataset.translations = JSON.stringify(translations);
input.value = translations[currentLanguage] || '';
body.appendChild(input);
container.appendChild(section);
@@ -562,27 +619,29 @@ document.addEventListener('DOMContentLoaded', () => {
// Sección de subtítulo: igual que la de título pero se muestra como <h4>
// (más pequeño) en el resultado, para marcar un encabezado secundario.
function addSubtitleSection(data, text = '', container = sectionsContainer) {
function addSubtitleSection(data, translations = {}, container = sectionsContainer) {
const { section, body } = createSectionShell(data.typeLabel);
const input = document.createElement('input');
input.type = 'text';
input.className = 'subtitle-input input input-bordered w-full';
input.placeholder = data.placeholder;
input.value = text;
input.dataset.translations = JSON.stringify(translations);
input.value = translations[currentLanguage] || '';
body.appendChild(input);
container.appendChild(section);
}
// Sección de texto libre.
function addTextSection(data, text = '', container = sectionsContainer) {
function addTextSection(data, translations = {}, container = sectionsContainer) {
const { section, body } = createSectionShell(data.typeLabel);
const textarea = document.createElement('textarea');
textarea.className = 'textarea textarea-bordered w-full';
textarea.placeholder = data.placeholder;
textarea.value = text;
textarea.dataset.translations = JSON.stringify(translations);
textarea.value = translations[currentLanguage] || '';
body.appendChild(textarea);
container.appendChild(section);
@@ -809,14 +868,17 @@ document.addEventListener('DOMContentLoaded', () => {
return { type: 'group', children: Array.from(groupContainer.children).map(serializeSection).filter(Boolean) };
}
// Título/subtítulo/texto libre: se serializa el mapa de traducciones
// completo (todos los idiomas ya escritos), no solo el texto que se ve
// ahora mismo, para no perder lo escrito en otros idiomas al guardar.
const titleInput = section.querySelector('.title-input');
if (titleInput) return { type: 'title', text: titleInput.value };
if (titleInput) return { type: 'title', text: getTranslations(titleInput) };
const subtitleInput = section.querySelector('.subtitle-input');
if (subtitleInput) return { type: 'subtitle', text: subtitleInput.value };
if (subtitleInput) return { type: 'subtitle', text: getTranslations(subtitleInput) };
const textarea = section.querySelector('textarea');
if (textarea) return { type: 'text', text: textarea.value };
if (textarea) return { type: 'text', text: getTranslations(textarea) };
const dndCanvas = section.querySelector('.dnd-canvas');
if (dndCanvas) {