refactor: Extract all hardcoded content to JSON files

- Move all bilingual text from templates to UI JSON (labels, buttons, modals)
- Move skills summary paragraph to CV JSON with HTML support
- Add new UI sections: navigation, viewControls, sections, footer, portfolio,
  pdfModal, shortcutsModal, infoModal, widgets
- Update Go structs to match expanded JSON structure
- Add template.HTML type for CV.SkillsSummary field
- Add JSON content validation test (70-json-content-validation.test.mjs)

Templates now contain only structural logic (CSS classes, HTML attributes)
while all user-visible text loads from JSON files for proper i18n support.
This commit is contained in:
juanatsap
2025-11-30 10:13:37 +00:00
parent c834919a3c
commit 9636b3659f
36 changed files with 806 additions and 168 deletions
+2 -2
View File
@@ -6,10 +6,10 @@
<p style="text-align: center; margin-bottom: 0.5rem;">
<a href="https://github.com/juanatsap/cv-site" target="_blank" rel="noopener noreferrer" class="github-repo-link" style="text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem;">
<iconify-icon icon="mdi:github" width="20" height="20"></iconify-icon>
{{if eq .Lang "es"}}Ver este proyecto en GitHub{{else}}View this project on GitHub{{end}}
{{.UI.Footer.ViewOnGithub}}
</a>
</p>
<p>© {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
{{.UI.Footer.LastUpdated}}: {{.CV.Meta.LastUpdated}}</p>
</footer>
{{end}}
+1 -7
View File
@@ -1,12 +1,6 @@
{{define "title-badges"}}
<!-- Professional Title Badges - Full Width Top Bar -->
<div class="cv-title-badges-header">
<span class="title-badge">{{if eq .Lang "es"}}CONSULTOR TÉCNICO{{else}}TECHNICAL CONSULTANT{{end}}</span>
<span class="badge-separator">|</span>
<span class="title-badge">{{if eq .Lang "es"}}INGENIERO FULL-STACK{{else}}FULL-STACK ENGINEER{{end}}</span>
<span class="badge-separator">|</span>
<span class="title-badge">{{if eq .Lang "es"}}ESPECIALISTA EN AUTENTICACIÓN{{else}}AUTHENTICATION SPECIALIST{{end}}</span>
<span class="badge-separator">|</span>
<span class="title-badge">{{if eq .Lang "es"}}ARQUITECTO DE SOLUCIONES{{else}}SOLUTION ARCHITECT{{end}}</span>
{{range $i, $badge := .CV.Personal.TitleBadges}}{{if $i}}<span class="badge-separator">|</span>{{end}}<span class="title-badge">{{$badge}}</span>{{end}}
</div>
{{end}}
+1 -1
View File
@@ -3,7 +3,7 @@
<dialog id="info-modal" class="info-modal no-print"
_="on click call closeOnBackdrop(me, event)">
<div class="info-modal-content">
<button class="info-modal-close" onclick="document.getElementById('info-modal').close()" aria-label="Close">
<button class="info-modal-close" onclick="document.getElementById('info-modal').close()" aria-label="{{.UI.PdfModal.Close}}">
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
</button>
+18 -18
View File
@@ -8,10 +8,10 @@
<div class="pdf-loading-content">
<div class="pdf-loading-spinner"></div>
<h3 class="pdf-loading-title" id="pdf-loading-title">
{{if eq .Lang "es"}}Preparando PDF...{{else}}Preparing PDF...{{end}}
{{.UI.PdfModal.PreparingPdf}}
</h3>
<p class="pdf-loading-message" id="pdf-loading-message">
{{if eq .Lang "es"}}Por favor espera mientras generamos tu CV{{else}}Please wait while we generate your CV{{end}}
{{.UI.PdfModal.PleaseWait}}
</p>
<p class="pdf-loading-estimate" id="pdf-loading-estimate"></p>
</div>
@@ -20,16 +20,16 @@
<!-- Close Button -->
<button class="info-modal-close"
onclick="document.getElementById('pdf-modal').close()"
aria-label="{{if eq .Lang "es"}}Cerrar{{else}}Close{{end}}">
aria-label="{{.UI.PdfModal.Close}}">
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
</button>
<!-- Header -->
<div class="info-modal-header">
<iconify-icon icon="catppuccin:pdf" width="40" height="40" style="margin-bottom: 0.5rem;"></iconify-icon>
<h2>{{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}</h2>
<h2>{{.UI.PdfModal.Title}}</h2>
<p class="pdf-modal-subtitle">
{{if eq .Lang "es"}}Elige tu formato preferido{{else}}Choose your preferred format{{end}}
{{.UI.PdfModal.Subtitle}}
</p>
</div>
@@ -41,7 +41,7 @@
data-cv-format="short"
role="radio"
aria-checked="false"
aria-label="{{if eq .Lang "es"}}CV Corto - 4 páginas, información esencial{{else}}Short CV - 4 pages, essential information{{end}}"
aria-label="{{.UI.PdfModal.ShortCv.AriaLabel}}"
tabindex="0"
_="on click call selectPdfCard(me)
on keydown call handlePdfCardKey(me, event)">
@@ -57,13 +57,13 @@
<!-- Page count badge -->
<div class="thumbnail-badge">
{{if eq .Lang "es"}}4 Páginas{{else}}4 Pages{{end}}
{{.UI.PdfModal.ShortCv.Pages}}
</div>
</div>
<div class="pdf-option-info">
<h3>{{if eq .Lang "es"}}CV Corto (4 páginas){{else}}Short CV (4 pages){{end}}</h3>
<p>{{if eq .Lang "es"}}Información esencial{{else}}Essential info{{end}}</p>
<h3>{{.UI.PdfModal.ShortCv.Title}}</h3>
<p>{{.UI.PdfModal.ShortCv.Description}}</p>
</div>
<div class="pdf-option-badge">
@@ -76,7 +76,7 @@
data-cv-format="default"
role="radio"
aria-checked="true"
aria-label="{{if eq .Lang "es"}}CV Por Defecto - 5 páginas con habilidades (Recomendado){{else}}Default CV - 5 pages with skills (Recommended){{end}}"
aria-label="{{.UI.PdfModal.DefaultCv.AriaLabel}}"
tabindex="0"
_="on click call selectPdfCard(me)
on keydown call handlePdfCardKey(me, event)">
@@ -97,16 +97,16 @@
<!-- Page count badge with star -->
<div class="thumbnail-badge" style="font-weight: 600;">
⭐ {{if eq .Lang "es"}}5 Páginas{{else}}5 Pages{{end}}
⭐ {{.UI.PdfModal.DefaultCv.Pages}}
</div>
</div>
<div class="pdf-option-info">
<h3>
{{if eq .Lang "es"}}CV Por Defecto (5 páginas){{else}}Default CV (5 pages){{end}}
{{.UI.PdfModal.DefaultCv.Title}}
<span style="color: #667eea; font-size: 0.9em;"></span>
</h3>
<p style="font-weight: 500;">{{if eq .Lang "es"}}Corto con habilidades - Recomendado{{else}}Short with skills - Recommended{{end}}</p>
<p style="font-weight: 500;">{{.UI.PdfModal.DefaultCv.Description}}</p>
</div>
<div class="pdf-option-badge">
@@ -119,7 +119,7 @@
data-cv-format="long"
role="radio"
aria-checked="false"
aria-label="{{if eq .Lang "es"}}CV Extendido - 9 páginas, versión completa{{else}}Extended CV - 9 pages, full version{{end}}"
aria-label="{{.UI.PdfModal.ExtendedCv.AriaLabel}}"
tabindex="0"
_="on click call selectPdfCard(me)
on keydown call handlePdfCardKey(me, event)">
@@ -137,13 +137,13 @@
<!-- Page count badge -->
<div class="thumbnail-badge">
{{if eq .Lang "es"}}9 Páginas{{else}}9 Pages{{end}}
{{.UI.PdfModal.ExtendedCv.Pages}}
</div>
</div>
<div class="pdf-option-info">
<h3>{{if eq .Lang "es"}}CV Extendido (9 páginas){{else}}Extended CV (9 pages){{end}}</h3>
<p>{{if eq .Lang "es"}}Todos los detalles{{else}}All details{{end}}</p>
<h3>{{.UI.PdfModal.ExtendedCv.Title}}</h3>
<p>{{.UI.PdfModal.ExtendedCv.Description}}</p>
</div>
<div class="pdf-option-badge">
@@ -158,7 +158,7 @@
id="pdf-download-btn"
onclick="downloadPDF()">
<iconify-icon icon="mdi:download" width="20" height="20"></iconify-icon>
{{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}
{{.UI.PdfModal.DownloadButton}}
</button>
</div>
@@ -3,7 +3,7 @@
<dialog id="shortcuts-modal" class="info-modal no-print"
_="on click call closeOnBackdrop(me, event)">
<div class="info-modal-content">
<button class="info-modal-close" onclick="document.getElementById('shortcuts-modal').close()" aria-label="{{if eq .Lang "es"}}Cerrar{{else}}Close{{end}}">
<button class="info-modal-close" onclick="document.getElementById('shortcuts-modal').close()" aria-label="{{.UI.ShortcutsModal.Close}}">
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
</button>
@@ -13,7 +13,7 @@
<span class="keyboard-icon-wrapper">
<iconify-icon icon="mdi:keyboard-outline" width="32" height="32"></iconify-icon>
</span>
{{if eq .Lang "es"}}Aprende los Atajos{{else}}Learn the Shortcuts{{end}}
{{.UI.ShortcutsModal.Subtitle}}
</div>
</div>
@@ -5,22 +5,22 @@
id="action-bar-pdf-btn"
class="action-btn pdf-btn has-tooltip"
onclick="document.getElementById('pdf-modal').showModal()"
aria-label="{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}"
data-tooltip="{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}"
aria-label="{{.UI.Widgets.ActionButtons.DownloadPdf}}"
data-tooltip="{{.UI.Widgets.ActionButtons.DownloadPdf}}"
_="on mouseenter call syncPdfHover(true)
on mouseleave call syncPdfHover(false)">
<iconify-icon icon="catppuccin:pdf" width="24" height="24"></iconify-icon>
{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}
{{.UI.Widgets.ActionButtons.DownloadPdf}}
</button>
<button
class="action-btn print-btn action-bar-print-btn has-tooltip"
aria-label="{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}"
data-tooltip="{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}"
aria-label="{{.UI.Widgets.ActionButtons.PrintFriendly}}"
data-tooltip="{{.UI.Widgets.ActionButtons.PrintFriendly}}"
_="on click call printFriendly()
on mouseenter call syncPrintHover(true)
on mouseleave call syncPrintHover(false)">
<iconify-icon icon="mdi:leaf" width="24" height="24"></iconify-icon>
{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}
{{.UI.Widgets.ActionButtons.PrintFriendly}}
</button>
</div>
{{end}}
@@ -6,54 +6,54 @@
<div class="menu-item-submenu">
<a href="#" class="menu-item has-submenu">
<iconify-icon icon="mdi:menu" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Secciones CV{{else}}CV Sections{{end}}</span>
<span>{{.UI.Navigation.CvSections}}</span>
<iconify-icon icon="mdi:chevron-right" width="16" height="16" class="submenu-arrow"></iconify-icon>
</a>
<div class="submenu-content">
<a href="#education" class="menu-item"
_="on click call scrollToSection(event, 'education')">
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Formación{{else}}Training{{end}}</span>
<span>{{.UI.Navigation.Training}}</span>
</a>
<a href="#skills" class="menu-item"
_="on click call scrollToSection(event, 'skills')">
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</span>
<span>{{.UI.Navigation.Skills}}</span>
</a>
<a href="#experience" class="menu-item"
_="on click call scrollToSection(event, 'experience')">
<iconify-icon icon="mdi:office-building" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}</span>
<span>{{.UI.Navigation.Experience}}</span>
</a>
<a href="#awards" class="menu-item"
_="on click call scrollToSection(event, 'awards')">
<iconify-icon icon="mdi:trophy" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}</span>
<span>{{.UI.Navigation.Awards}}</span>
</a>
<a href="#projects" class="menu-item"
_="on click call scrollToSection(event, 'projects')">
<iconify-icon icon="mdi:web" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Proyectos Personales / Freelance{{else}}Personal / Freelance Projects{{end}}</span>
<span>{{.UI.Navigation.Projects}}</span>
</a>
<a href="#courses" class="menu-item"
_="on click call scrollToSection(event, 'courses')">
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}</span>
<span>{{.UI.Navigation.Courses}}</span>
</a>
<a href="#languages" class="menu-item"
_="on click call scrollToSection(event, 'languages')">
<iconify-icon icon="mdi:translate" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</span>
<span>{{.UI.Navigation.Languages}}</span>
</a>
<a href="#references" class="menu-item"
_="on click call scrollToSection(event, 'references')">
<iconify-icon icon="mdi:link-variant" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Referencias{{else}}References{{end}}</span>
<span>{{.UI.Navigation.References}}</span>
</a>
<a href="#other" class="menu-item"
_="on click call scrollToSection(event, 'other')">
<iconify-icon icon="mdi:information" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Otros{{else}}Other{{end}}</span>
<span>{{.UI.Navigation.Other}}</span>
</a>
</div>
</div>
@@ -62,21 +62,21 @@
<div class="menu-section-wrapper">
<div class="menu-item menu-item-header">
<iconify-icon icon="mdi:cog-outline" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Acciones Rápidas{{else}}Quick Actions{{end}}</span>
<span>{{.UI.Navigation.QuickActions}}</span>
</div>
<a href="#" class="menu-item menu-item-action" _="on click call collapseAllSections(event)">
<iconify-icon icon="mdi:arrow-collapse-all" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Colapsar Todo{{else}}Collapse All{{end}}</span>
<span>{{.UI.Navigation.CollapseAll}}</span>
</a>
<a href="#" class="menu-item menu-item-action" _="on click call expandAllSections(event)">
<iconify-icon icon="mdi:arrow-expand-all" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Expandir Todo{{else}}Expand All{{end}}</span>
<span>{{.UI.Navigation.ExpandAll}}</span>
</a>
<a href="#" id="show-zoom-menu-btn" class="menu-item menu-item-action zoom-hidden"
_="on click call showZoomControl()">
<iconify-icon icon="mdi:magnify" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Zoom{{else}}Zoom{{end}}</span>
<span>{{.UI.Navigation.Zoom}}</span>
</a>
</div>
@@ -84,14 +84,14 @@
<div class="menu-controls-section">
<div class="menu-item menu-item-header">
<iconify-icon icon="mdi:tune-variant" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Controles de Vista{{else}}View Controls{{end}}</span>
<span>{{.UI.Navigation.ViewControls}}</span>
</div>
<!-- CV Length toggle -->
<div class="menu-control-item" id="mobile-length-toggle">
<label class="menu-control-label">
<iconify-icon icon="mdi:file-document-outline" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Longitud{{else}}Length{{end}}</span>
<span>{{.UI.ViewControls.Length}}</span>
</label>
<div style="display: flex; align-items: center; gap: 8px;">
<label class="icon-toggle">
@@ -118,7 +118,7 @@
<div class="menu-control-item" id="mobile-icon-toggle">
<label class="menu-control-label">
<iconify-icon icon="mdi:image-multiple-outline" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Iconos{{else}}Icons{{end}}</span>
<span>{{.UI.ViewControls.Icons}}</span>
</label>
<div style="display: flex; align-items: center; gap: 8px;">
<label class="icon-toggle">
@@ -145,7 +145,7 @@
<div class="menu-control-item" id="mobile-theme-toggle">
<label class="menu-control-label">
<iconify-icon icon="mdi:page-layout-sidebar-left" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Vista{{else}}View{{end}}</span>
<span>{{.UI.ViewControls.View}}</span>
</label>
<div style="display: flex; align-items: center; gap: 8px;">
<label class="icon-toggle">
@@ -173,7 +173,7 @@
<div class="menu-actions-section">
<div class="menu-item menu-item-header">
<iconify-icon icon="mdi:lightning-bolt" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Acciones{{else}}Actions{{end}}</span>
<span>{{.UI.Navigation.Actions}}</span>
</div>
<button class="menu-action-btn menu-pdf-btn"
@@ -181,7 +181,7 @@
_="on mouseenter call syncPdfHover(true)
on mouseleave call syncPdfHover(false)">
<iconify-icon icon="catppuccin:pdf" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}</span>
<span>{{.UI.Widgets.ActionButtons.DownloadPdf}}</span>
</button>
<button class="menu-action-btn menu-print-btn"
@@ -189,7 +189,7 @@
on mouseenter call syncPrintHover(true)
on mouseleave call syncPrintHover(false)">
<iconify-icon icon="mdi:leaf" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}</span>
<span>{{.UI.Widgets.ActionButtons.PrintFriendly}}</span>
</button>
</div>
</div>
@@ -3,7 +3,7 @@
<div class="view-controls-center">
<!-- CV Length toggle -->
<div class="selector-group" id="desktop-length-toggle">
<label class="selector-label">{{if eq .Lang "es"}}Longitud{{else}}Length{{end}}:</label>
<label class="selector-label">{{.UI.ViewControls.Length}}:</label>
<label class="icon-toggle">
<input type="checkbox"
id="lengthToggle"
@@ -25,7 +25,7 @@
<!-- Icon toggle -->
<div class="selector-group" id="desktop-icon-toggle">
<label class="selector-label">{{if eq .Lang "es"}}Iconos{{else}}Icons{{end}}:</label>
<label class="selector-label">{{.UI.ViewControls.Icons}}:</label>
<label class="icon-toggle">
<input type="checkbox"
id="iconToggle"
@@ -47,7 +47,7 @@
<!-- Theme toggle -->
<div class="selector-group" id="desktop-theme-toggle">
<label class="selector-label">{{if eq .Lang "es"}}Vista{{else}}View{{end}}:</label>
<label class="selector-label">{{.UI.ViewControls.View}}:</label>
<label class="icon-toggle">
<input type="checkbox"
id="themeToggle"
+1 -1
View File
@@ -8,7 +8,7 @@
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:trophy" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}
{{.UI.Navigation.Awards}}
</h3>
</summary>
{{range .CV.Awards}}
+1 -1
View File
@@ -8,7 +8,7 @@
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:school" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}
{{.UI.Navigation.Courses}}
</h3>
</summary>
{{range .CV.Courses}}
+2 -2
View File
@@ -7,12 +7,12 @@
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:school" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Formación{{else}}Training{{end}}
{{.UI.Navigation.Training}}
</h3>
</summary>
{{range .CV.Education}}
<div class="education-item">
<strong>{{.Degree}}</strong> ({{.StartDate}}-{{.EndDate}}) {{if eq $.Lang "es"}}obtenido de{{else}}obtained from the{{end}} <strong>{{.Institution}}</strong> ({{.Location}})
<strong>{{.Degree}}</strong> ({{.StartDate}}-{{.EndDate}}) {{$.UI.Sections.ObtainedFrom}} <strong>{{.Institution}}</strong> ({{.Location}})
</div>
{{end}}
</details>
+4 -4
View File
@@ -7,7 +7,7 @@
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:office-building" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}
{{.UI.Navigation.Experience}}
</h3>
</summary>
@@ -22,10 +22,10 @@
</div>
<div class="experience-content">
<strong>{{.Position}}{{if .Company}} - {{if .CompanyURL}}<a href="{{.CompanyURL}}" target="_blank" rel="noopener noreferrer">{{.Company}}</a>{{else}}{{.Company}}{{end}}{{if .Duration}} - <span class="duration-text">{{.Duration}}</span>{{end}}{{end}}</strong>
{{if .Current}}<span class="current-badge">{{if eq $.Lang "es"}}ACTUAL{{else}}CURRENT{{end}}</span>{{end}}
{{if .Expired}}<span class="expired-badge">{{if eq $.Lang "es"}}EXPIRADO{{else}}EXPIRED{{end}}</span>{{end}}
{{if .Current}}<span class="current-badge">{{$.UI.Sections.CurrentBadge}}</span>{{end}}
{{if .Expired}}<span class="expired-badge">{{$.UI.Sections.ExpiredBadge}}</span>{{end}}
<br>
<small>{{.StartDate}} / {{if .Current}}{{if eq $.Lang "es"}}presente{{else}}now{{end}}{{else}}{{.EndDate}}{{end}} - ({{.Location}})</small>
<small>{{.StartDate}} / {{if .Current}}{{$.UI.Sections.Present}}{{else}}{{.EndDate}}{{end}} - ({{.Location}})</small>
{{if .ShortDescription}}
<p class="experience-desc short-desc">{{.ShortDescription | safeHTML}}</p>
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="cv-header-content">
<div class="cv-header-left">
<h1 class="cv-name">Moreno Rubio, Juan Andrés</h1>
<p class="years-experience">{{.YearsOfExperience}} {{if eq .Lang "es"}}años de experiencia{{else}}years of experience{{end}}</p>
<p class="years-experience">{{.YearsOfExperience}} {{.UI.Sections.YearsOfExperience}}</p>
<!-- Photo positioned for mobile (centered between name and intro) -->
<div class="cv-photo">
+1 -1
View File
@@ -7,7 +7,7 @@
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:translate" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}
{{.UI.Navigation.Languages}}
</h3>
</summary>
{{range .CV.Languages}}
+2 -2
View File
@@ -8,11 +8,11 @@
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:information" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Otros{{else}}Other{{end}}
{{.UI.Navigation.Other}}
</h3>
</summary>
<div class="other-content">
{{if eq .Lang "es"}}Carnet de conducir tipo <strong>{{.CV.Other.DriverLicense}}</strong>{{else}}Driving License type <strong>{{.CV.Other.DriverLicense}}</strong>{{end}}
{{.UI.Sections.DrivingLicense}} <strong>{{.CV.Other.DriverLicense}}</strong>
</div>
</details>
</div>
+6 -6
View File
@@ -8,7 +8,7 @@
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:web" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Proyectos Personales / Freelance{{else}}Personal / Freelance Projects{{end}}
{{.UI.Navigation.Projects}}
</h3>
</summary>
{{range .CV.Projects}}
@@ -31,9 +31,9 @@
{{end}}
</strong>
{{if .Current}}<span class="live-badge"><iconify-icon icon="mdi:wifi" width="14" height="14"></iconify-icon>LIVE</span>{{end}}
{{if .MaintainedBy}}<span class="maintained-badge">{{if eq $.Lang "es"}}MANTENIDO POR{{else}}MAINTAINED BY{{end}} {{.MaintainedBy}}</span>{{end}}
{{if .MaintainedBy}}<span class="maintained-badge">{{$.UI.Sections.MaintainedBy}} {{.MaintainedBy}}</span>{{end}}
<br>
<small>{{if .StartDate}}{{.StartDate}}{{if .Current}}{{if .DynamicDate}} / {{.DynamicDate}}{{else}} / {{if eq $.Lang "es"}}presente{{else}}ahora{{end}}{{end}}{{end}}{{end}} - ({{.Location}})</small>
<small>{{if .StartDate}}{{.StartDate}}{{if .Current}}{{if .DynamicDate}} / {{.DynamicDate}}{{else}} / {{$.UI.Sections.Present}}{{end}}{{end}}{{end}} - ({{.Location}})</small>
{{if .ShortDescription}}
<p class="project-desc short-desc">{{.ShortDescription | safeHTML}}</p>
@@ -49,7 +49,7 @@
{{if .Technologies}}
<div class="project-technologies long-only">
<strong>{{if eq $.Lang "es"}}Tecnologías:{{else}}Technologies:{{end}}</strong>
<strong>{{$.UI.Sections.Technologies}}</strong>
{{range $index, $tech := .Technologies}}{{if $index}}, {{end}}{{$tech}}{{end}}
</div>
{{end}}
@@ -59,8 +59,8 @@
<!-- Link to full portfolio -->
<div class="projects-footer">
<p>{{if eq .Lang "es"}}Ver todos los proyectos en mi{{else}}See all projects on my{{end}}
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer"><strong>{{if eq .Lang "es"}}portfolio de Domestika{{else}}Domestika portfolio{{end}}</strong></a></p>
<p>{{.UI.Portfolio.SeeAllProjects}}
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer"><strong>{{.UI.Portfolio.DomestikaPortfolio}}</strong></a></p>
</div>
</details>
</div>
+1 -1
View File
@@ -8,7 +8,7 @@
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:link-variant" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Referencias{{else}}References{{end}}
{{.UI.Navigation.References}}
</h3>
</summary>
{{range .CV.References}}
@@ -7,16 +7,10 @@
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:brain" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}
{{.UI.Navigation.Skills}}
</h3>
</summary>
<p class="summary-text">
{{if eq .Lang "es"}}
Desarrollador <strong>full-stack</strong> con experiencia en <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong> y <strong>HTMX</strong> para <strong>aplicaciones modernas</strong>, además de conocimientos en Java y PHP para proyectos legacy. He trabajado en <strong>unos 20 sitios web</strong> y realizado <strong>consultoría para 35-40 clientes internacionales</strong>, desde e-commerce y plataformas empresariales hasta <strong>sistemas de autenticación</strong> que gestionan <strong>millones de usuarios</strong>. Familiarizado con flujos de trabajo asistidos por <strong>IA</strong> y gestión de infraestructura (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). Me adapto bien tanto al trabajo independiente como colaborativo en equipos internacionales.
{{else}}
<strong>Full-stack</strong> developer with experience in <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong>, and <strong>HTMX</strong> for <strong>modern applications</strong>, plus Java and PHP knowledge for legacy projects. I've worked on <strong>around 20 websites</strong> and provided <strong>consulting for 35-40 international clients</strong>, from e-commerce and enterprise platforms to <strong>authentication systems</strong> managing <strong>millions of users</strong>. Familiar with <strong>AI-assisted development</strong> workflows and infrastructure management (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). I adapt well to both independent work and collaborative teams across different countries.
{{end}}
</p>
<p class="summary-text">{{.CV.SkillsSummary}}</p>
</details>
</div>
+2 -2
View File
@@ -2,8 +2,8 @@
<!-- Back to Top Link - Hyperscript smooth scroll without URL pollution -->
<button id="back-to-top"
class="back-to-top no-print has-tooltip tooltip-left"
aria-label="{{if eq .Lang "es"}}Volver arriba{{else}}Back to top{{end}}"
data-tooltip="{{if eq .Lang "es"}}Volver arriba{{else}}Back to top{{end}}"
aria-label="{{.UI.Widgets.BackToTop.AriaLabel}}"
data-tooltip="{{.UI.Widgets.BackToTop.Tooltip}}"
style="display: none;"
_="on click call scrollToTop(event)">
<iconify-icon icon="mdi:arrow-up" width="24" height="24"></iconify-icon>
@@ -3,8 +3,8 @@
<button
id="download-button"
class="fixed-btn download-btn no-print has-tooltip"
aria-label="{{if eq .Lang "es"}}Descargar PDF{{else}}Download as PDF{{end}}"
data-tooltip="{{if eq .Lang "es"}}Descargar PDF{{else}}Download as PDF{{end}}"
aria-label="{{.UI.Widgets.Download.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Download.Tooltip}}"
onclick="openPdfModal()"
_="on mouseenter call syncPdfHover(true)
on mouseleave call syncPdfHover(false)">
+2 -2
View File
@@ -1,8 +1,8 @@
{{define "info-button"}}
<!-- Info Button (Bottom Left) -->
<button id="info-button" class="info-button no-print has-tooltip"
aria-label="{{if eq .Lang "es"}}Información{{else}}Information{{end}}"
data-tooltip="{{if eq .Lang "es"}}Información{{else}}Information{{end}}"
aria-label="{{.UI.Widgets.Info.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Info.Tooltip}}"
onclick="document.getElementById('info-modal').showModal()">
<iconify-icon icon="mdi:information-outline" width="24" height="24"></iconify-icon>
</button>
+2 -2
View File
@@ -3,10 +3,10 @@
<div id="pdf-toast" class="success-toast no-print" role="status" aria-live="polite" aria-atomic="true">
<span class="toast-icon" id="pdf-toast-icon">📥</span>
<div class="toast-content">
<p class="toast-title" id="pdf-toast-title">{{if eq .Lang "es"}}Preparando PDF{{else}}Preparing PDF{{end}}</p>
<p class="toast-title" id="pdf-toast-title">{{.UI.Widgets.PdfToast.Title}}</p>
<p class="toast-message" id="pdf-toast-message"></p>
</div>
<button aria-label="{{if eq .Lang "es"}}Cerrar notificación{{else}}Close notification{{end}}" class="toast-close" onclick="document.getElementById('pdf-toast').classList.remove('show')">×</button>
<button aria-label="{{.UI.Widgets.PdfToast.CloseLabel}}" class="toast-close" onclick="document.getElementById('pdf-toast').classList.remove('show')">×</button>
<div class="toast-progress">
<div class="toast-progress-bar" id="pdf-toast-progress"></div>
</div>
@@ -3,8 +3,8 @@
<button
id="print-friendly-button"
class="fixed-btn print-friendly-btn no-print has-tooltip"
aria-label="{{if eq .Lang "es"}}Imprimir CV{{else}}Print Friendly{{end}}"
data-tooltip="{{if eq .Lang "es"}}Imprimir CV{{else}}Print Friendly{{end}}"
aria-label="{{.UI.Widgets.Print.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Print.Tooltip}}"
onclick="window.print()"
_="on mouseenter call syncPrintHover(true)
on mouseleave call syncPrintHover(false)">
@@ -4,8 +4,8 @@
id="shortcuts-button"
class="fixed-btn shortcuts-btn no-print has-tooltip"
onclick="document.getElementById('shortcuts-modal').showModal()"
aria-label="{{if eq .Lang "es"}}Atajos de teclado{{else}}Keyboard shortcuts{{end}}"
data-tooltip="{{if eq .Lang "es"}}Atajos de teclado (?){{else}}Keyboard shortcuts (?){{end}}">
aria-label="{{.UI.Widgets.Shortcuts.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Shortcuts.Tooltip}}">
<iconify-icon icon="mdi:keyboard-outline" width="28" height="28"></iconify-icon>
</button>
{{end}}
+6 -6
View File
@@ -1,6 +1,6 @@
{{define "zoom-control"}}
<!-- Zoom Control (Fixed Bottom Center, Draggable) - Hyperscript Enhanced -->
<div id="zoom-control" class="zoom-control no-print zoom-hidden" role="group" aria-label="{{if eq .Lang "es"}}Control de zoom{{else}}Zoom control{{end}}"
<div id="zoom-control" class="zoom-control no-print zoom-hidden" role="group" aria-label="{{.UI.Widgets.ZoomControl.GroupLabel}}"
_="on load call initZoomControl(me)
on mousedown(clientX, clientY) if isZoomDragTarget(event.target) call startZoomDrag(me, clientX, clientY) then halt the event end
on mousemove(clientX, clientY) from document if moveZoomDrag(me, clientX, clientY) halt the event end
@@ -9,8 +9,8 @@
<button
id="zoom-close"
class="zoom-close-btn"
aria-label="{{if eq .Lang "es"}}Cerrar control de zoom{{else}}Close zoom control{{end}}"
title="{{if eq .Lang "es"}}Cerrar{{else}}Close{{end}}"
aria-label="{{.UI.Widgets.ZoomControl.CloseLabel}}"
title="{{.UI.Widgets.ZoomControl.CloseTitle}}"
_="on click call hideZoomControl()">
<iconify-icon icon="mdi:close" width="16" height="16" style="pointer-events: none;"></iconify-icon>
</button>
@@ -25,7 +25,7 @@
max="300"
step="1"
value="100"
aria-label="{{if eq .Lang "es"}}Ajustar nivel de zoom del CV{{else}}Adjust CV zoom level{{end}}"
aria-label="{{.UI.Widgets.ZoomControl.SliderLabel}}"
aria-valuemin="25"
aria-valuemax="300"
aria-valuenow="100"
@@ -37,8 +37,8 @@
<button
id="zoom-reset"
class="zoom-reset-btn"
aria-label="{{if eq .Lang "es"}}Restablecer zoom al 100%{{else}}Reset zoom to 100%{{end}}"
title="{{if eq .Lang "es"}}Restablecer{{else}}Reset{{end}}"
aria-label="{{.UI.Widgets.ZoomControl.ResetLabel}}"
title="{{.UI.Widgets.ZoomControl.ResetTitle}}"
aria-live="polite"
_="on click call handleZoomReset()">
<span id="zoom-value-current">100</span>
@@ -3,8 +3,8 @@
<button
id="zoom-toggle-button"
class="fixed-btn zoom-toggle-btn no-print has-tooltip"
aria-label="{{if eq .Lang "es"}}Alternar control de zoom{{else}}Toggle zoom control{{end}}"
data-tooltip="{{if eq .Lang "es"}}Control de zoom{{else}}Zoom control{{end}}"
aria-label="{{.UI.Widgets.ZoomToggle.AriaLabel}}"
data-tooltip="{{.UI.Widgets.ZoomToggle.Tooltip}}"
_="on click call toggleZoomControl()
on mouseenter call highlightZoomControl(true)
on mouseleave call highlightZoomControl(false)">