diff --git a/app/pages/package-timeline/[[org]]/[packageName].vue b/app/pages/package-timeline/[[org]]/[packageName].vue index a86c3f2af3..701428e161 100644 --- a/app/pages/package-timeline/[[org]]/[packageName].vue +++ b/app/pages/package-timeline/[[org]]/[packageName].vue @@ -174,6 +174,19 @@ if (import.meta.client) { const bytesFormatter = useBytesFormatter() +// Format a duration (in days) into a localized human-readable string +function formatDuration(days: number): string { + if (days < 30) return $t('package.timeline.time_gap_days', { count: days }, days) + if (days < 365) + return $t( + 'package.timeline.time_gap_months', + { count: Math.floor(days / 30) }, + Math.floor(days / 30), + ) + const years = Math.floor(days / 365) + return $t('package.timeline.time_gap_years', { count: years }, years) +} + // Detect notable changes between consecutive versions (size, license, ESM, types) // Versions are compared against their semver predecessor, not chronological neighbor, // so interleaved legacy releases don't produce misleading cross-line diffs. @@ -189,11 +202,32 @@ const versionSubEvents = computed(() => { } for (const current of entries) { - const previous = prevBySemver.get(current.version) - if (!previous) continue - const events: SubEvent[] = [] + // Time gap from the previous chronological release + // entries are sorted newest-first by publish time, so index+1 is the predecessor + const chronologicalIndex = entries.indexOf(current) + const prevRelease = entries[chronologicalIndex + 1] + if (prevRelease) { + const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000 + const gapMs = Date.parse(current.time) - Date.parse(prevRelease.time) + if (gapMs > SEVEN_DAYS_MS) { + const gapDays = Math.floor(gapMs / (1000 * 60 * 60 * 24)) + events.push({ + key: 'timeGap', + positive: false, + icon: 'i-lucide:clock', + text: $t('package.timeline.time_gap', { duration: formatDuration(gapDays) }), + }) + } + } + + const previous = prevBySemver.get(current.version) + if (!previous) { + if (events.length) result.set(current.version, events) + continue + } + // Size changes const currentSize = sizeCache.get(sizeKey(current.version)) const previousSize = sizeCache.get(sizeKey(previous.version)) diff --git a/i18n/locales/en.json b/i18n/locales/en.json index a09e22b59f..f99d2286e8 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -646,6 +646,10 @@ "trusted_publisher_removed": "Trusted publishing removed", "provenance_added": "Provenance enabled", "provenance_removed": "Provenance removed", + "time_gap": "{duration} after previous release", + "time_gap_days": "{count} day | {count} days", + "time_gap_months": "{count} month | {count} months", + "time_gap_years": "{count} year | {count} years", "chart": { "tab_aria_label": "Metric selection", "dependency_size": "Dependency Size", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index 99d9019d6a..16b44a37ac 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -601,7 +601,11 @@ "version_events": "版本 {version}:{events}", "general_description": "折线图展示了 {package} 包从版本 {first} 到 {last} 的 {metric} 变化情况。版本 {first} 的 {metric} 为 {first_value},版本 {last} 为 {last_value}(整体变化 {overall_progress_percentage}%)。{key_changes} {watermark}。" } - } + }, + "time_gap": "距上一版本 {duration}", + "time_gap_days": "{count} 天", + "time_gap_months": "{count} 个月", + "time_gap_years": "{count} 年" }, "dependencies": { "title": "依赖({count} 个)", diff --git a/i18n/locales/zh-TW.json b/i18n/locales/zh-TW.json index e2b15eefb7..a5670617a3 100644 --- a/i18n/locales/zh-TW.json +++ b/i18n/locales/zh-TW.json @@ -555,7 +555,11 @@ "timeline": { "chart": { "copy_alt": {} - } + }, + "time_gap": "距上一版本 {duration}", + "time_gap_days": "{count} 天", + "time_gap_months": "{count} 個月", + "time_gap_years": "{count} 年" }, "dependencies": { "title": "相依({count} 個)", diff --git a/i18n/schema.json b/i18n/schema.json index b7793e498c..d5b0bb6623 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -1942,6 +1942,18 @@ "provenance_removed": { "type": "string" }, + "time_gap": { + "type": "string" + }, + "time_gap_days": { + "type": "string" + }, + "time_gap_months": { + "type": "string" + }, + "time_gap_years": { + "type": "string" + }, "chart": { "type": "object", "properties": {