feat: first take at calendar
This commit is contained in:
@@ -15,7 +15,7 @@ import { getPicoStylesheetHref, sanitizePicoTheme, type PicoThemeName } from '..
|
||||
import type { MenuDocument } from './MenuEngine';
|
||||
import type { ProjectMetadata } from './MetaEngine';
|
||||
import { loadPublishedGenerationSets } from './GenerationPostSnapshotService';
|
||||
import { buildSitemapAndFeeds, collectSitemapArchiveMetadata } from './GenerationSitemapFeedService';
|
||||
import { buildCalendarArchiveData, buildSitemapAndFeeds, collectSitemapArchiveMetadata } from './GenerationSitemapFeedService';
|
||||
import { buildTargetedValidationPlan, planMissingValidationPaths } from './ValidationApplyPlannerService';
|
||||
import { compareSitemapToHtml } from './SiteValidationDiffService';
|
||||
import {
|
||||
@@ -228,6 +228,7 @@ export class BlogGenerationEngine {
|
||||
let rssXml = '';
|
||||
let atomXml = '';
|
||||
let feedPosts: PostData[] = [];
|
||||
let calendarJson = '';
|
||||
|
||||
if (includeCore) {
|
||||
onProgress(5, 'Building sitemap XML...');
|
||||
@@ -252,6 +253,7 @@ export class BlogGenerationEngine {
|
||||
rssXml = sitemapAndFeedResult.rssXml;
|
||||
atomXml = sitemapAndFeedResult.atomXml;
|
||||
feedPosts = sitemapAndFeedResult.feedPosts;
|
||||
calendarJson = `${JSON.stringify(buildCalendarArchiveData(publishedListPosts), null, 2)}\n`;
|
||||
|
||||
onProgress(8, 'Building RSS and Atom feeds...');
|
||||
} else if (includeCategory || includeTag || includeDate) {
|
||||
@@ -275,6 +277,7 @@ export class BlogGenerationEngine {
|
||||
const sitemapPath = path.join(htmlDir, 'sitemap.xml');
|
||||
const rssPath = path.join(htmlDir, 'rss.xml');
|
||||
const atomPath = path.join(htmlDir, 'atom.xml');
|
||||
const calendarPath = path.join(htmlDir, 'calendar.json');
|
||||
|
||||
const estimatedUnitsBySection = estimateGenerationUnitsBySection({
|
||||
posts: publishedListPosts,
|
||||
@@ -333,6 +336,13 @@ export class BlogGenerationEngine {
|
||||
content: atomXml,
|
||||
});
|
||||
reportUnitProgress('Atom feed written');
|
||||
await writeFileIfHashChanged({
|
||||
projectId: options.projectId,
|
||||
filePath: calendarPath,
|
||||
relativePath: 'calendar.json',
|
||||
content: calendarJson,
|
||||
});
|
||||
reportUnitProgress('Calendar data written');
|
||||
|
||||
onProgress(15, 'Copying assets...');
|
||||
await copyPreviewAssets(htmlDir, {
|
||||
|
||||
@@ -120,7 +120,7 @@ export function estimateGenerationUnitsBySection(params: {
|
||||
}
|
||||
|
||||
return {
|
||||
core: 4 + rootPages + pageRoutes,
|
||||
core: 5 + rootPages + pageRoutes,
|
||||
single: posts.length,
|
||||
category: categoryPages,
|
||||
tag: tagPages,
|
||||
|
||||
@@ -44,6 +44,12 @@ export interface SitemapArchiveMetadata {
|
||||
latestPostUpdatedAt: string;
|
||||
}
|
||||
|
||||
export interface CalendarArchiveData {
|
||||
years: Record<string, number>;
|
||||
months: Record<string, number>;
|
||||
days: Record<string, number>;
|
||||
}
|
||||
|
||||
function resolvePostCreatedAt(post: { createdAt: Date | string }): Date {
|
||||
if (post.createdAt instanceof Date) {
|
||||
return post.createdAt;
|
||||
@@ -154,6 +160,42 @@ function escapeCdata(value: string): string {
|
||||
return value.replace(/]]>/g, ']]]]><![CDATA[>');
|
||||
}
|
||||
|
||||
export function buildCalendarArchiveData(posts: PostData[]): CalendarArchiveData {
|
||||
const yearCounts = new Map<string, number>();
|
||||
const monthCounts = new Map<string, number>();
|
||||
const dayCounts = new Map<string, number>();
|
||||
|
||||
for (const post of posts) {
|
||||
const createdAt = resolvePostCreatedAt(post);
|
||||
const year = String(createdAt.getFullYear());
|
||||
const month = String(createdAt.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(createdAt.getDate()).padStart(2, '0');
|
||||
|
||||
const monthKey = `${year}-${month}`;
|
||||
const dayKey = `${monthKey}-${day}`;
|
||||
|
||||
yearCounts.set(year, (yearCounts.get(year) ?? 0) + 1);
|
||||
monthCounts.set(monthKey, (monthCounts.get(monthKey) ?? 0) + 1);
|
||||
dayCounts.set(dayKey, (dayCounts.get(dayKey) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const toObject = (entries: Map<string, number>): Record<string, number> => {
|
||||
const result: Record<string, number> = {};
|
||||
|
||||
for (const key of Array.from(entries.keys()).sort()) {
|
||||
result[key] = entries.get(key) ?? 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
years: toObject(yearCounts),
|
||||
months: toObject(monthCounts),
|
||||
days: toObject(dayCounts),
|
||||
};
|
||||
}
|
||||
|
||||
export function collectSitemapArchiveMetadata(params: {
|
||||
baseUrl: string;
|
||||
maxPostsPerPage: number;
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { PostData } from './PostEngine';
|
||||
import type { MenuDocument, MenuItemData } from './MenuEngine';
|
||||
import { PICO_THEME_NAMES } from '../shared/picoThemes';
|
||||
import { CODE_ENHANCEMENTS_RUNTIME_JS } from './assets/codeEnhancementsRuntime';
|
||||
import { CALENDAR_RUNTIME_JS } from './assets/calendarRuntime';
|
||||
import { TAG_CLOUD_RUNTIME_JS } from './assets/tagCloudRuntime';
|
||||
import { resolveRenderLanguageFromProjectPreferences, translateRender } from '../shared/i18n';
|
||||
|
||||
@@ -222,6 +223,18 @@ export const PREVIEW_ASSETS: Record<string, PreviewAssetDefinition> = {
|
||||
contentType: 'application/javascript; charset=utf-8',
|
||||
sourceText: TAG_CLOUD_RUNTIME_JS,
|
||||
},
|
||||
'vanilla-calendar.min.css': {
|
||||
modulePath: 'vanilla-calendar-pro/styles/index.css',
|
||||
contentType: 'text/css; charset=utf-8',
|
||||
},
|
||||
'vanilla-calendar.min.js': {
|
||||
modulePath: 'vanilla-calendar-pro',
|
||||
contentType: 'application/javascript; charset=utf-8',
|
||||
},
|
||||
'calendar-runtime.js': {
|
||||
contentType: 'application/javascript; charset=utf-8',
|
||||
sourceText: CALENDAR_RUNTIME_JS,
|
||||
},
|
||||
};
|
||||
|
||||
export const PREVIEW_IMAGE_ASSETS = {
|
||||
|
||||
190
src/main/engine/assets/calendarRuntime.ts
Normal file
190
src/main/engine/assets/calendarRuntime.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
export const CALENDAR_RUNTIME_JS = String.raw`(() => {
|
||||
const button = document.querySelector('[data-blog-calendar-toggle]');
|
||||
const panel = document.querySelector('[data-blog-calendar-panel]');
|
||||
const closeButton = document.querySelector('[data-blog-calendar-close]');
|
||||
const calendarRoot = document.querySelector('[data-blog-calendar-root]');
|
||||
const status = document.querySelector('[data-blog-calendar-status]');
|
||||
|
||||
if (!button || !panel || !calendarRoot || !status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = {
|
||||
loading: panel.getAttribute('data-i18n-loading') || 'Loading calendar…',
|
||||
error: panel.getAttribute('data-i18n-error') || 'Calendar data could not be loaded.',
|
||||
};
|
||||
|
||||
let isInitialized = false;
|
||||
let years = {};
|
||||
let months = {};
|
||||
let days = {};
|
||||
|
||||
function pad2(value) {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
function normalizeCountMap(value) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const map = {};
|
||||
for (const [key, rawCount] of Object.entries(value)) {
|
||||
const count = Number(rawCount);
|
||||
if (!Number.isFinite(count) || count <= 0) {
|
||||
continue;
|
||||
}
|
||||
map[key] = Math.floor(count);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function navigateTo(pathname) {
|
||||
if (!pathname) {
|
||||
return;
|
||||
}
|
||||
window.location.assign(pathname);
|
||||
}
|
||||
|
||||
async function loadCalendarData() {
|
||||
const response = await fetch('/calendar.json', { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error('calendar.json request failed');
|
||||
}
|
||||
|
||||
const parsed = await response.json();
|
||||
years = normalizeCountMap(parsed?.years);
|
||||
months = normalizeCountMap(parsed?.months);
|
||||
days = normalizeCountMap(parsed?.days);
|
||||
}
|
||||
|
||||
function getDateFromClickEvent(event) {
|
||||
if (!(event?.target instanceof Element)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dateEl = event.target.closest('[data-vc-date]');
|
||||
if (!(dateEl instanceof HTMLElement)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return dateEl.dataset.vcDate || '';
|
||||
}
|
||||
|
||||
async function initializeCalendar() {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
status.textContent = labels.loading;
|
||||
|
||||
try {
|
||||
await loadCalendarData();
|
||||
|
||||
const Calendar = window.VanillaCalendarPro?.Calendar;
|
||||
if (typeof Calendar !== 'function') {
|
||||
throw new Error('Vanilla Calendar Pro is unavailable');
|
||||
}
|
||||
|
||||
const calendar = new Calendar('[data-blog-calendar-root]', {
|
||||
onCreateDateEls(_self, dateEl) {
|
||||
const dateKey = dateEl.dataset.vcDate || '';
|
||||
const count = Number(days[dateKey] || 0);
|
||||
const buttonEl = dateEl.querySelector('[data-vc-date-btn]');
|
||||
|
||||
if (!(buttonEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = buttonEl.querySelector('.blog-calendar-post-count');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
dateEl.removeAttribute('data-blog-calendar-has-posts');
|
||||
return;
|
||||
}
|
||||
|
||||
dateEl.setAttribute('data-blog-calendar-has-posts', 'true');
|
||||
const marker = document.createElement('span');
|
||||
marker.className = 'blog-calendar-post-count';
|
||||
marker.textContent = String(count);
|
||||
buttonEl.appendChild(marker);
|
||||
},
|
||||
onClickDate(_self, event) {
|
||||
const dateKey = getDateFromClickEvent(event);
|
||||
if (!dateKey || !days[dateKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [year, month, day] = dateKey.split('-');
|
||||
if (!year || !month || !day) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo('/' + year + '/' + month + '/' + day + '/');
|
||||
},
|
||||
onClickMonth(self) {
|
||||
const selectedYear = Number(self?.context?.selectedYear);
|
||||
const selectedMonth = Number(self?.context?.selectedMonth);
|
||||
|
||||
if (!Number.isInteger(selectedYear) || !Number.isInteger(selectedMonth)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monthKey = String(selectedYear) + '-' + pad2(selectedMonth + 1);
|
||||
if (!months[monthKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo('/' + String(selectedYear) + '/' + pad2(selectedMonth + 1) + '/');
|
||||
},
|
||||
onClickYear(self) {
|
||||
const selectedYear = Number(self?.context?.selectedYear);
|
||||
|
||||
if (!Number.isInteger(selectedYear)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yearKey = String(selectedYear);
|
||||
if (!years[yearKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo('/' + String(selectedYear) + '/');
|
||||
},
|
||||
});
|
||||
|
||||
calendar.init();
|
||||
status.textContent = '';
|
||||
status.setAttribute('hidden', 'hidden');
|
||||
isInitialized = true;
|
||||
} catch {
|
||||
status.textContent = labels.error;
|
||||
status.removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function setPanelOpen(isOpen) {
|
||||
if (isOpen) {
|
||||
panel.removeAttribute('hidden');
|
||||
void initializeCalendar();
|
||||
return;
|
||||
}
|
||||
|
||||
panel.setAttribute('hidden', 'hidden');
|
||||
}
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
const isHidden = panel.hasAttribute('hidden');
|
||||
setPanelOpen(isHidden);
|
||||
});
|
||||
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', () => {
|
||||
setPanelOpen(false);
|
||||
});
|
||||
}
|
||||
})();`;
|
||||
@@ -6,6 +6,7 @@
|
||||
<link rel="stylesheet" href="{{ resolved_pico_stylesheet_href }}" />
|
||||
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
||||
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
||||
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
|
||||
<link rel="alternate" type="application/atom+xml" title="Atom" href="/atom.xml" />
|
||||
{% render 'partials/styles' %}
|
||||
@@ -14,4 +15,6 @@
|
||||
<script defer src="/assets/d3.layout.cloud.js"></script>
|
||||
<script defer src="/assets/tag-cloud.js"></script>
|
||||
<script defer src="/assets/lightbox.min.js"></script>
|
||||
<script defer src="/assets/vanilla-calendar.min.js"></script>
|
||||
<script defer src="/assets/calendar-runtime.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
{% if menu_items and menu_items.size > 0 %}
|
||||
<nav class="blog-menu">
|
||||
<nav class="blog-menu">
|
||||
{% if menu_items and menu_items.size > 0 %}
|
||||
{% render 'partials/menu-items', items: menu_items %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="blog-menu-calendar">
|
||||
<button
|
||||
type="button"
|
||||
class="blog-menu-calendar-button"
|
||||
data-blog-calendar-toggle
|
||||
aria-label="{{ 'render.calendar.open' | i18n: language }}"
|
||||
title="{{ 'render.calendar.open' | i18n: language }}"
|
||||
>
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
||||
<rect x="3" y="5" width="18" height="16" rx="2" ry="2"></rect>
|
||||
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||
<line x1="8" y1="3" x2="8" y2="7"></line>
|
||||
<line x1="16" y1="3" x2="16" y2="7"></line>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<section
|
||||
id="blog-calendar"
|
||||
class="blog-calendar-panel"
|
||||
data-blog-calendar-panel
|
||||
data-i18n-loading="{{ 'render.calendar.loading' | i18n: language }}"
|
||||
data-i18n-error="{{ 'render.calendar.error' | i18n: language }}"
|
||||
hidden
|
||||
>
|
||||
<header class="blog-calendar-header">
|
||||
<strong>{{ 'render.calendar.title' | i18n: language }}</strong>
|
||||
<button
|
||||
type="button"
|
||||
class="blog-calendar-close"
|
||||
data-blog-calendar-close
|
||||
aria-label="{{ 'render.calendar.close' | i18n: language }}"
|
||||
title="{{ 'render.calendar.close' | i18n: language }}"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<div class="blog-calendar-content">
|
||||
<div data-blog-calendar-root></div>
|
||||
<p class="blog-calendar-status" data-blog-calendar-status>{{ 'render.calendar.loading' | i18n: language }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[data-theme='dark'] { --pico-background-color: #13171f; }
|
||||
body { max-width: 960px; margin: 0 auto; padding: 2rem 1rem 4rem; background: var(--pico-background-color, var(--background-color)); color: var(--pico-color, var(--color)); }
|
||||
main { display: grid; gap: 1rem; }
|
||||
.blog-menu { border-top: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); border-bottom: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); padding: .4rem 0; margin: -.15rem 0 .2rem; }
|
||||
.blog-menu { position: relative; display: flex; align-items: center; justify-content: space-between; gap: .75rem; border-top: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); border-bottom: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); padding: .4rem 0; margin: -.15rem 0 .2rem; }
|
||||
.blog-menu-list { list-style: none; display: flex; flex-wrap: wrap; gap: .25rem .75rem; margin: 0; padding: 0; }
|
||||
.blog-menu-item { position: relative; }
|
||||
.blog-menu-link { display: inline-flex; align-items: center; color: var(--pico-muted-color, var(--muted-color)); text-decoration: none; font-size: .94rem; line-height: 1.4; padding: .2rem .1rem; }
|
||||
@@ -17,6 +17,19 @@
|
||||
.blog-menu-submenu .blog-menu-list { flex-direction: column; flex-wrap: nowrap; gap: .2rem; }
|
||||
.blog-menu-item-with-children:hover > .blog-menu-submenu,
|
||||
.blog-menu-item-with-children:focus-within > .blog-menu-submenu { display: block; }
|
||||
.blog-menu-calendar { position: relative; display: inline-flex; align-items: center; justify-content: center; margin-left: auto; }
|
||||
.blog-menu-calendar-button { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; padding: 0; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: var(--pico-card-background-color, var(--card-background-color)); color: var(--pico-muted-color, var(--muted-color)); border-radius: .25rem; cursor: pointer; }
|
||||
.blog-menu-calendar-button svg { display: block; width: 1rem; height: 1rem; fill: none; stroke: currentColor; }
|
||||
.blog-menu-calendar-button:hover,
|
||||
.blog-menu-calendar-button:focus-visible { color: var(--pico-color, var(--color)); border-color: var(--pico-color, var(--color)); }
|
||||
.blog-calendar-panel { position: absolute; top: calc(100% + .3rem); right: 0; width: min(21.5rem, 92vw); border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: var(--pico-card-background-color, var(--card-background-color)); padding: .55rem; z-index: 30; }
|
||||
.blog-calendar-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: .35rem; }
|
||||
.blog-calendar-close { border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); background: transparent; color: var(--pico-muted-color, var(--muted-color)); width: 1.8rem; height: 1.8rem; border-radius: .2rem; padding: 0; cursor: pointer; }
|
||||
.blog-calendar-close:hover,
|
||||
.blog-calendar-close:focus-visible { color: var(--pico-color, var(--color)); border-color: var(--pico-color, var(--color)); }
|
||||
.blog-calendar-status { margin: .45rem 0 0; color: var(--pico-muted-color, var(--muted-color)); font-size: .85rem; }
|
||||
[data-blog-calendar-has-posts='true'] [data-vc-date-btn] { border-color: var(--pico-primary, var(--pico-color, var(--color))); }
|
||||
.blog-calendar-post-count { display: inline-flex; align-items: center; justify-content: center; min-width: 1.15rem; height: 1.15rem; margin-left: .22rem; border-radius: 999px; font-size: .62rem; line-height: 1; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); color: var(--pico-color, var(--color)); }
|
||||
.post { border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); padding: 1rem; background: var(--pico-card-background-color, var(--card-background-color)); min-width: 0; }
|
||||
.post pre { position: relative; overflow-x: auto; max-width: 100%; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); border-radius: .3rem; margin: .9rem 0; padding: .85rem .9rem; background: var(--pico-code-background-color, rgba(33, 38, 45, .82)); box-sizing: border-box; }
|
||||
.post pre code { display: block; font-size: .88rem; line-height: 1.5; white-space: pre; }
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
"render.gallery.empty": "Keine verknüpften Bilder gefunden.",
|
||||
"render.tagCloud.empty": "Keine Tags gefunden.",
|
||||
"render.tagCloud.ariaLabel": "Tag-Wolke",
|
||||
"render.calendar.open": "Kalender öffnen",
|
||||
"render.calendar.close": "Kalender schließen",
|
||||
"render.calendar.title": "Archivkalender",
|
||||
"render.calendar.loading": "Kalender wird geladen …",
|
||||
"render.calendar.error": "Kalenderdaten konnten nicht geladen werden.",
|
||||
"render.taxonomy.ariaLabel": "Taxonomie",
|
||||
"render.video.youtubeTitle": "YouTube-Video",
|
||||
"render.video.vimeoTitle": "Vimeo-Video",
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
"render.gallery.empty": "No linked images found.",
|
||||
"render.tagCloud.empty": "No tags found.",
|
||||
"render.tagCloud.ariaLabel": "Tag cloud",
|
||||
"render.calendar.open": "Open calendar",
|
||||
"render.calendar.close": "Close calendar",
|
||||
"render.calendar.title": "Archive calendar",
|
||||
"render.calendar.loading": "Loading calendar…",
|
||||
"render.calendar.error": "Calendar data could not be loaded.",
|
||||
"render.taxonomy.ariaLabel": "Taxonomy",
|
||||
"render.video.youtubeTitle": "YouTube video",
|
||||
"render.video.vimeoTitle": "Vimeo video",
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
"render.gallery.empty": "No se encontraron imágenes vinculadas.",
|
||||
"render.tagCloud.empty": "No se encontraron etiquetas.",
|
||||
"render.tagCloud.ariaLabel": "Nube de etiquetas",
|
||||
"render.calendar.open": "Abrir calendario",
|
||||
"render.calendar.close": "Cerrar calendario",
|
||||
"render.calendar.title": "Calendario de archivo",
|
||||
"render.calendar.loading": "Cargando calendario…",
|
||||
"render.calendar.error": "No se pudieron cargar los datos del calendario.",
|
||||
"render.taxonomy.ariaLabel": "Taxonomía",
|
||||
"render.video.youtubeTitle": "Vídeo de YouTube",
|
||||
"render.video.vimeoTitle": "Vídeo de Vimeo",
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
"render.gallery.empty": "Aucune image liée trouvée.",
|
||||
"render.tagCloud.empty": "Aucun tag trouvé.",
|
||||
"render.tagCloud.ariaLabel": "Nuage de tags",
|
||||
"render.calendar.open": "Ouvrir le calendrier",
|
||||
"render.calendar.close": "Fermer le calendrier",
|
||||
"render.calendar.title": "Calendrier des archives",
|
||||
"render.calendar.loading": "Chargement du calendrier…",
|
||||
"render.calendar.error": "Impossible de charger les données du calendrier.",
|
||||
"render.taxonomy.ariaLabel": "Taxonomie",
|
||||
"render.video.youtubeTitle": "Vidéo YouTube",
|
||||
"render.video.vimeoTitle": "Vidéo Vimeo",
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
"render.gallery.empty": "Nessuna immagine collegata trovata.",
|
||||
"render.tagCloud.empty": "Nessun tag trovato.",
|
||||
"render.tagCloud.ariaLabel": "Nuvola di tag",
|
||||
"render.calendar.open": "Apri calendario",
|
||||
"render.calendar.close": "Chiudi calendario",
|
||||
"render.calendar.title": "Calendario archivio",
|
||||
"render.calendar.loading": "Caricamento calendario…",
|
||||
"render.calendar.error": "Impossibile caricare i dati del calendario.",
|
||||
"render.taxonomy.ariaLabel": "Tassonomia",
|
||||
"render.video.youtubeTitle": "Video YouTube",
|
||||
"render.video.vimeoTitle": "Video Vimeo",
|
||||
|
||||
Reference in New Issue
Block a user