Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to CV Manager will be documented in this file.

Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).

## [1.49.2] - 2026-05-04

### Fixed
- **Missing translations on admin item-row controls and custom-section UI ([#161](https://github.com/vincentmakes/cv-manager/issues/161)).** Several user-visible strings in `public/shared/admin.js` bypassed the i18n system and stayed in English regardless of the user's selected locale — and leaked into browser-printed PDFs. The admin-side renderer had drifted from the public-side renderer in `public/shared/scripts.js`, which already routed the same strings through `t(...)`. All call sites now go through `t(...)`:
- The "View →" link on grid- and list-layout custom-section items (reused existing `view_link` key).
- The "Learn More →" link on cards-layout custom-section items (reused existing `learn_more` key).
- The "Manage Items" button at the bottom of every custom section.
- The `title` tooltips on the per-row action controls — Toggle Visibility (reused existing `action.toggle_visibility` key), Edit, Delete, and the drag handle — across Experience, Certifications, Education, Skills, Projects, and custom-section item rows.

Added four new keys (`action.manage_items`, `action.edit`, `action.delete`, `action.drag_to_reorder`) to all 8 locale files (en, de, fr, nl, es, it, pt, zh) so key parity is preserved (enforced by `tests/frontend.test.js`).

## [1.49.1] - 2026-05-02

### Changed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cv-manager",
"version": "1.49.1",
"version": "1.49.2",
"description": "Professional CV Management System",
"main": "src/server.js",
"scripts": {
Expand Down
52 changes: 26 additions & 26 deletions public/shared/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ function renderCustomSection(section) {
</div>
<button class="add-btn no-print" onclick="manageCustomSectionItems(${section.id})">
<span class="material-symbols-outlined">add</span>
Manage Items
${t('action.manage_items')}
</button>
</section>
`;
Expand Down Expand Up @@ -867,7 +867,7 @@ function renderGridLayout(items, cols) {
${item.title && !hideTitle ? `<h3 class="custom-item-title">${escapeHtml(item.title)}</h3>` : ''}
${item.subtitle ? `<div class="custom-item-subtitle">${escapeHtml(item.subtitle)}</div>` : ''}
${item.description ? `<div class="custom-item-description">${renderMarkdown(item.description, { mode: 'block' })}</div>` : ''}
${item.link ? `<a href="${escapeHtml(item.link)}" class="custom-item-link" target="_blank" rel="noopener">View →</a>` : ''}
${item.link ? `<a href="${escapeHtml(item.link)}" class="custom-item-link" target="_blank" rel="noopener">${t('view_link')}</a>` : ''}
</div>
`;
}).join('')}</div>`;
Expand All @@ -887,7 +887,7 @@ function renderListLayout(items) {
${item.subtitle ? `<div class="custom-item-subtitle">${escapeHtml(item.subtitle)}</div>` : ''}
${item.description ? `<div class="custom-item-description">${renderMarkdown(item.description, { mode: 'block' })}</div>` : ''}
</div>
${item.link ? `<a href="${escapeHtml(item.link)}" class="custom-item-link" target="_blank" rel="noopener">View →</a>` : ''}
${item.link ? `<a href="${escapeHtml(item.link)}" class="custom-item-link" target="_blank" rel="noopener">${t('view_link')}</a>` : ''}
</div>
`;
}).join('')}</div>`;
Expand All @@ -905,7 +905,7 @@ function renderCardsLayout(items) {
${item.title && !hideTitle ? `<h3 class="custom-card-title">${escapeHtml(item.title)}</h3>` : ''}
${item.subtitle ? `<div class="custom-card-subtitle">${escapeHtml(item.subtitle)}</div>` : ''}
${item.description ? `<div class="custom-card-description">${renderMarkdown(item.description, { mode: 'block' })}</div>` : ''}
${item.link ? `<a href="${escapeHtml(item.link)}" class="custom-card-link" target="_blank" rel="noopener">Learn More →</a>` : ''}
${item.link ? `<a href="${escapeHtml(item.link)}" class="custom-card-link" target="_blank" rel="noopener">${t('learn_more')}</a>` : ''}
</div>
`;
}).join('')}</div>`;
Expand Down Expand Up @@ -1050,13 +1050,13 @@ async function loadExperiences() {
<button class="item-btn move-btn" onclick="moveExperience(${exp.id}, 'down')" title="${t('action.move_down')}"${index === total - 1 ? ' disabled' : ''}>
${moveDownIcon()}
</button>
<button class="item-btn" onclick="toggleVisibility('experiences', ${exp.id}, ${!exp.visible})" title="Toggle Visibility">
<button class="item-btn" onclick="toggleVisibility('experiences', ${exp.id}, ${!exp.visible})" title="${t('action.toggle_visibility')}">
${visibilityIcon(exp.visible)}
</button>
<button class="item-btn" onclick="openModal('experience', ${exp.id})" title="Edit">
<button class="item-btn" onclick="openModal('experience', ${exp.id})" title="${t('action.edit')}">
${editIcon()}
</button>
<button class="item-btn delete" onclick="confirmDelete('experiences', ${exp.id})" title="Delete">
<button class="item-btn delete" onclick="confirmDelete('experiences', ${exp.id})" title="${t('action.delete')}">
${deleteIcon()}
</button>
</div>`;
Expand Down Expand Up @@ -1089,15 +1089,15 @@ async function loadCertifications() {
const hasLink = isValidUrl(cert.credential_id);
return `
<article class="cert-card ${cert.visible ? '' : 'hidden-print'}${hasLogo ? ' has-logo' : ''}" data-id="${cert.id}" draggable="true" itemscope itemtype="https://schema.org/EducationalOccupationalCredential">
<div class="drag-handle" title="Drag to reorder">${dragHandleIcon()}</div>
<div class="drag-handle" title="${t('action.drag_to_reorder')}">${dragHandleIcon()}</div>
<div class="item-actions">
<button class="item-btn" onclick="toggleVisibility('certifications', ${cert.id}, ${!cert.visible})" title="Toggle Visibility">
<button class="item-btn" onclick="toggleVisibility('certifications', ${cert.id}, ${!cert.visible})" title="${t('action.toggle_visibility')}">
${visibilityIcon(cert.visible)}
</button>
<button class="item-btn" onclick="openModal('certification', ${cert.id})" title="Edit">
<button class="item-btn" onclick="openModal('certification', ${cert.id})" title="${t('action.edit')}">
${editIcon()}
</button>
<button class="item-btn delete" onclick="confirmDelete('certifications', ${cert.id})" title="Delete">
<button class="item-btn delete" onclick="confirmDelete('certifications', ${cert.id})" title="${t('action.delete')}">
${deleteIcon()}
</button>
</div>
Expand All @@ -1124,15 +1124,15 @@ async function loadEducation() {

container.innerHTML = education.map(edu => `
<article class="item-card ${edu.visible ? '' : 'hidden-print'}${edu.logo_filename ? ' has-logo' : ''}" data-id="${edu.id}" draggable="true" itemscope itemtype="https://schema.org/EducationalOccupationalCredential">
<div class="drag-handle" title="Drag to reorder">${dragHandleIcon()}</div>
<div class="drag-handle" title="${t('action.drag_to_reorder')}">${dragHandleIcon()}</div>
<div class="item-actions">
<button class="item-btn" onclick="toggleVisibility('education', ${edu.id}, ${!edu.visible})" title="Toggle Visibility">
<button class="item-btn" onclick="toggleVisibility('education', ${edu.id}, ${!edu.visible})" title="${t('action.toggle_visibility')}">
${visibilityIcon(edu.visible)}
</button>
<button class="item-btn" onclick="openModal('education', ${edu.id})" title="Edit">
<button class="item-btn" onclick="openModal('education', ${edu.id})" title="${t('action.edit')}">
${editIcon()}
</button>
<button class="item-btn delete" onclick="confirmDelete('education', ${edu.id})" title="Delete">
<button class="item-btn delete" onclick="confirmDelete('education', ${edu.id})" title="${t('action.delete')}">
${deleteIcon()}
</button>
</div>
Expand Down Expand Up @@ -1164,15 +1164,15 @@ async function loadSkills() {

container.innerHTML = skills.map(cat => `
<div class="skill-category ${cat.visible ? '' : 'hidden-print'}" data-id="${cat.id}" draggable="true">
<div class="drag-handle" title="Drag to reorder">${dragHandleIcon()}</div>
<div class="drag-handle" title="${t('action.drag_to_reorder')}">${dragHandleIcon()}</div>
<div class="item-actions">
<button class="item-btn" onclick="toggleVisibility('skills', ${cat.id}, ${!cat.visible})" title="Toggle Visibility">
<button class="item-btn" onclick="toggleVisibility('skills', ${cat.id}, ${!cat.visible})" title="${t('action.toggle_visibility')}">
${visibilityIcon(cat.visible)}
</button>
<button class="item-btn" onclick="openModal('skill', ${cat.id})" title="Edit">
<button class="item-btn" onclick="openModal('skill', ${cat.id})" title="${t('action.edit')}">
${editIcon()}
</button>
<button class="item-btn delete" onclick="confirmDelete('skills', ${cat.id})" title="Delete">
<button class="item-btn delete" onclick="confirmDelete('skills', ${cat.id})" title="${t('action.delete')}">
${deleteIcon()}
</button>
</div>
Expand All @@ -1197,15 +1197,15 @@ async function loadProjects() {

container.innerHTML = projects.map(proj => `
<article class="project-card ${proj.visible ? '' : 'hidden-print'}" data-id="${proj.id}" draggable="true" itemscope itemtype="https://schema.org/CreativeWork">
<div class="drag-handle" title="Drag to reorder">${dragHandleIcon()}</div>
<div class="drag-handle" title="${t('action.drag_to_reorder')}">${dragHandleIcon()}</div>
<div class="item-actions">
<button class="item-btn" onclick="toggleVisibility('projects', ${proj.id}, ${!proj.visible})" title="Toggle Visibility">
<button class="item-btn" onclick="toggleVisibility('projects', ${proj.id}, ${!proj.visible})" title="${t('action.toggle_visibility')}">
${visibilityIcon(proj.visible)}
</button>
<button class="item-btn" onclick="openModal('project', ${proj.id})" title="Edit">
<button class="item-btn" onclick="openModal('project', ${proj.id})" title="${t('action.edit')}">
${editIcon()}
</button>
<button class="item-btn delete" onclick="confirmDelete('projects', ${proj.id})" title="Delete">
<button class="item-btn delete" onclick="confirmDelete('projects', ${proj.id})" title="${t('action.delete')}">
${deleteIcon()}
</button>
</div>
Expand Down Expand Up @@ -5834,17 +5834,17 @@ async function manageCustomSectionItems(sectionId) {
<div class="custom-items-list" data-section-id="${sectionId}">
${items.length === 0 ? '<p style="color: var(--gray-500); text-align: center; padding: 20px;">No items yet.</p>' : items.map(item => `
<div class="custom-item-row" data-id="${item.id}" draggable="true">
<div class="drag-handle" title="Drag to reorder">${dragHandleIcon()}</div>
<div class="drag-handle" title="${t('action.drag_to_reorder')}">${dragHandleIcon()}</div>
${(section.layout_type === 'picture-grid' || section.layout_type === 'timeline') && item.image ? `<img src="/uploads/${escapeHtml(item.image)}?${Date.now()}" alt="" class="custom-item-thumb">` : ''}
<div class="custom-item-info">
<div class="custom-item-title">${escapeHtml(item.title || (section.layout_type === 'picture-grid' ? t('custom_item.picture') : 'Untitled'))}</div>
${section.layout_type === 'timeline' ? `<div class="custom-item-subtitle">${escapeHtml(item.subtitle || '')}${item.metadata?.start_date ? ` | ${escapeHtml(item.metadata.start_date)} - ${item.metadata.end_date ? escapeHtml(item.metadata.end_date) : t('present')}` : ''}</div>` : (item.subtitle ? `<div class="custom-item-subtitle">${escapeHtml(item.subtitle)}</div>` : '')}
</div>
<div class="custom-item-actions">
<button class="item-btn" onclick="openCustomItemModal(${sectionId}, ${item.id})" title="Edit">
<button class="item-btn" onclick="openCustomItemModal(${sectionId}, ${item.id})" title="${t('action.edit')}">
${editIcon()}
</button>
<button class="item-btn delete" onclick="confirmDeleteCustomItem(${sectionId}, ${item.id})" title="Delete">
<button class="item-btn delete" onclick="confirmDeleteCustomItem(${sectionId}, ${item.id})" title="${t('action.delete')}">
${deleteIcon()}
</button>
</div>
Expand Down
4 changes: 4 additions & 0 deletions public/shared/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"action.move_down": "Nach unten",
"action.reorder_sections": "Abschnitte neu anordnen",
"action.copy_to_dataset": "In einen anderen Lebenslauf kopieren",
"action.manage_items": "Einträge verwalten",
"action.edit": "Bearbeiten",
"action.delete": "Löschen",
"action.drag_to_reorder": "Zum Neuordnen ziehen",
"copy_section.title": "Abschnitt in einen anderen Lebenslauf kopieren",
"copy_section.subtitle": "Wähle einen Ziel-Lebenslauf, der den Abschnitt '{{section}}' erhalten soll.",
"copy_section.from": "Von:",
Expand Down
4 changes: 4 additions & 0 deletions public/shared/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
"action.move_down": "Move Down",
"action.reorder_sections": "Reorder sections",
"action.copy_to_dataset": "Copy to another CV",
"action.manage_items": "Manage Items",
"action.edit": "Edit",
"action.delete": "Delete",
"action.drag_to_reorder": "Drag to reorder",
"copy_section.title": "Copy section to another CV",
"copy_section.subtitle": "Select a target CV to receive the '{{section}}' section.",
"copy_section.from": "From:",
Expand Down
4 changes: 4 additions & 0 deletions public/shared/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"action.move_down": "Mover abajo",
"action.reorder_sections": "Reordenar secciones",
"action.copy_to_dataset": "Copiar a otro CV",
"action.manage_items": "Gestionar elementos",
"action.edit": "Editar",
"action.delete": "Eliminar",
"action.drag_to_reorder": "Arrastrar para reordenar",
"copy_section.title": "Copiar sección a otro CV",
"copy_section.subtitle": "Selecciona un CV de destino para recibir la sección '{{section}}'.",
"copy_section.from": "Desde:",
Expand Down
4 changes: 4 additions & 0 deletions public/shared/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"action.move_down": "Déplacer vers le bas",
"action.reorder_sections": "Réorganiser les sections",
"action.copy_to_dataset": "Copier vers un autre CV",
"action.manage_items": "Gérer les éléments",
"action.edit": "Modifier",
"action.delete": "Supprimer",
"action.drag_to_reorder": "Glisser pour réorganiser",
"copy_section.title": "Copier la section vers un autre CV",
"copy_section.subtitle": "Sélectionnez un CV cible pour recevoir la section '{{section}}'.",
"copy_section.from": "Depuis :",
Expand Down
4 changes: 4 additions & 0 deletions public/shared/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"action.move_down": "Sposta giù",
"action.reorder_sections": "Riordina sezioni",
"action.copy_to_dataset": "Copia in un altro CV",
"action.manage_items": "Gestisci elementi",
"action.edit": "Modifica",
"action.delete": "Elimina",
"action.drag_to_reorder": "Trascina per riordinare",
"copy_section.title": "Copia sezione in un altro CV",
"copy_section.subtitle": "Seleziona un CV di destinazione per ricevere la sezione '{{section}}'.",
"copy_section.from": "Da:",
Expand Down
4 changes: 4 additions & 0 deletions public/shared/i18n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"action.move_down": "Omlaag",
"action.reorder_sections": "Secties herschikken",
"action.copy_to_dataset": "Kopiëren naar een ander cv",
"action.manage_items": "Items beheren",
"action.edit": "Bewerken",
"action.delete": "Verwijderen",
"action.drag_to_reorder": "Sleep om te herschikken",
"copy_section.title": "Sectie naar een ander cv kopiëren",
"copy_section.subtitle": "Kies een doel-cv om de sectie '{{section}}' te ontvangen.",
"copy_section.from": "Van:",
Expand Down
4 changes: 4 additions & 0 deletions public/shared/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"action.move_down": "Mover para baixo",
"action.reorder_sections": "Reordenar secções",
"action.copy_to_dataset": "Copiar para outro CV",
"action.manage_items": "Gerir itens",
"action.edit": "Editar",
"action.delete": "Eliminar",
"action.drag_to_reorder": "Arrastar para reordenar",
"copy_section.title": "Copiar secção para outro CV",
"copy_section.subtitle": "Selecione um CV de destino para receber a secção '{{section}}'.",
"copy_section.from": "De:",
Expand Down
4 changes: 4 additions & 0 deletions public/shared/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"action.move_down": "下移",
"action.reorder_sections": "重新排序板块",
"action.copy_to_dataset": "复制到另一份简历",
"action.manage_items": "管理项目",
"action.edit": "编辑",
"action.delete": "删除",
"action.drag_to_reorder": "拖动以重新排序",
"copy_section.title": "将板块复制到另一份简历",
"copy_section.subtitle": "选择接收 '{{section}}' 板块的目标简历。",
"copy_section.from": "来源:",
Expand Down
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"version": "1.49.1",
"version": "1.49.2",
"changelog": "https://github.com/vincentmakes/cv-manager/blob/main/CHANGELOG.md"
}
Loading