feat: support for multi language
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user