feat: support for multi language
This commit is contained in:
+10
-1
@@ -15,7 +15,16 @@
|
|||||||
|
|
||||||
<header class="flex flex-wrap items-center justify-between gap-2 p-4 no-print">
|
<header class="flex flex-wrap items-center justify-between gap-2 p-4 no-print">
|
||||||
<h1 class="text-xl font-semibold">Crochet</h1>
|
<h1 class="text-xl font-semibold">Crochet</h1>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2 items-center">
|
||||||
|
<!-- Idioma del contenido (título/subtítulo/texto de cada sección), no
|
||||||
|
de la interfaz: la estructura de secciones es la misma para
|
||||||
|
todos los idiomas, solo cambian los textos que el usuario ha
|
||||||
|
escrito para el idioma seleccionado. Añadir un idioma nuevo es
|
||||||
|
solo cuestión de añadir una <option> aquí. -->
|
||||||
|
<select id="language-select" class="select select-sm select-bordered">
|
||||||
|
<option value="es" selected>Español</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
</select>
|
||||||
<button id="save-pattern" type="button" class="btn btn-sm btn-outline">Guardar</button>
|
<button id="save-pattern" type="button" class="btn btn-sm btn-outline">Guardar</button>
|
||||||
<button id="load-pattern" type="button" class="btn btn-sm btn-outline">Cargar</button>
|
<button id="load-pattern" type="button" class="btn btn-sm btn-outline">Cargar</button>
|
||||||
<button id="export-pdf" type="button" class="btn btn-sm">Exportar a PDF</button>
|
<button id="export-pdf" type="button" class="btn btn-sm">Exportar a PDF</button>
|
||||||
|
|||||||
@@ -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 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 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 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)
|
// 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).
|
// añadir/quitar/reordenar secciones o elementos soltados (MutationObserver).
|
||||||
// Al estar en #panel-top-sections y usar subtree:true, esto ya cubre
|
// 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".
|
// 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);
|
sectionsContainer.addEventListener('change', renderOutput);
|
||||||
new MutationObserver(renderOutput).observe(sectionsContainer, { childList: true, subtree: true });
|
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)
|
// 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
|
// 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,
|
// 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
|
// data-placeholder). `translations`, si se pasa, es el mapa { idioma:
|
||||||
// restaurar un patrón guardado). `container` es dónde se añade la
|
// texto } completo (se usa al restaurar un patrón guardado o al
|
||||||
// sección: #panel-top-sections por defecto, o el de un grupo.
|
// duplicar una sección); se muestra el que corresponda al idioma actual.
|
||||||
function addTitleSection(data, text = '', container = sectionsContainer) {
|
// `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 { section, body } = createSectionShell(data.typeLabel);
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'text';
|
input.type = 'text';
|
||||||
input.className = 'title-input input input-bordered w-full';
|
input.className = 'title-input input input-bordered w-full';
|
||||||
input.placeholder = data.placeholder;
|
input.placeholder = data.placeholder;
|
||||||
input.value = text;
|
input.dataset.translations = JSON.stringify(translations);
|
||||||
|
input.value = translations[currentLanguage] || '';
|
||||||
|
|
||||||
body.appendChild(input);
|
body.appendChild(input);
|
||||||
container.appendChild(section);
|
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>
|
// 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.
|
// (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 { section, body } = createSectionShell(data.typeLabel);
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'text';
|
input.type = 'text';
|
||||||
input.className = 'subtitle-input input input-bordered w-full';
|
input.className = 'subtitle-input input input-bordered w-full';
|
||||||
input.placeholder = data.placeholder;
|
input.placeholder = data.placeholder;
|
||||||
input.value = text;
|
input.dataset.translations = JSON.stringify(translations);
|
||||||
|
input.value = translations[currentLanguage] || '';
|
||||||
|
|
||||||
body.appendChild(input);
|
body.appendChild(input);
|
||||||
container.appendChild(section);
|
container.appendChild(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sección de texto libre.
|
// Sección de texto libre.
|
||||||
function addTextSection(data, text = '', container = sectionsContainer) {
|
function addTextSection(data, translations = {}, container = sectionsContainer) {
|
||||||
const { section, body } = createSectionShell(data.typeLabel);
|
const { section, body } = createSectionShell(data.typeLabel);
|
||||||
|
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
textarea.className = 'textarea textarea-bordered w-full';
|
textarea.className = 'textarea textarea-bordered w-full';
|
||||||
textarea.placeholder = data.placeholder;
|
textarea.placeholder = data.placeholder;
|
||||||
textarea.value = text;
|
textarea.dataset.translations = JSON.stringify(translations);
|
||||||
|
textarea.value = translations[currentLanguage] || '';
|
||||||
|
|
||||||
body.appendChild(textarea);
|
body.appendChild(textarea);
|
||||||
container.appendChild(section);
|
container.appendChild(section);
|
||||||
@@ -809,14 +868,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return { type: 'group', children: Array.from(groupContainer.children).map(serializeSection).filter(Boolean) };
|
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');
|
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');
|
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');
|
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');
|
const dndCanvas = section.querySelector('.dnd-canvas');
|
||||||
if (dndCanvas) {
|
if (dndCanvas) {
|
||||||
|
|||||||
Reference in New Issue
Block a user