- {{
- $t('package.trends.no_data', {
- facet: $t('package.trends.items.downloads'),
- })
- }}
+
+ {{ $t('package.trends.no_data') }}
+
{
/* Override default placement of the refresh button to have it to the minimap's side */
@media screen and (min-width: 767px) {
- #download-analytics .vue-data-ui-refresh-button {
+ #trends-chart .vue-data-ui-refresh-button {
top: -0.6rem !important;
left: calc(100% + 2rem) !important;
}
diff --git a/app/components/Package/WeeklyDownloadStats.vue b/app/components/Package/WeeklyDownloadStats.vue
index 5cf37a7e8..8b522ee73 100644
--- a/app/components/Package/WeeklyDownloadStats.vue
+++ b/app/components/Package/WeeklyDownloadStats.vue
@@ -86,7 +86,7 @@ const pulseColor = computed(() => {
return isDarkMode.value ? accent.value : lightenOklch(accent.value, 0.5)
})
-const weeklyDownloads = shallowRef
([])
+const weeklyDownloads = shallowRef([])
const isLoadingWeeklyDownloads = shallowRef(true)
const hasWeeklyDownloads = computed(() => weeklyDownloads.value.length > 0)
@@ -111,7 +111,7 @@ async function loadWeeklyDownloads() {
() => props.createdIso,
() => ({ granularity: 'week' as const, weeks: 52 }),
)
- weeklyDownloads.value = (result as WeeklyDownloadPoint[]) ?? []
+ weeklyDownloads.value = (result as WeeklyDataPoint[]) ?? []
} catch {
weeklyDownloads.value = []
} finally {
@@ -130,7 +130,7 @@ watch(
const dataset = computed(() =>
weeklyDownloads.value.map(d => ({
- value: d?.downloads ?? 0,
+ value: d?.value ?? 0,
period: $t('package.trends.date_range', {
start: d.weekStart ?? '-',
end: d.weekEnd ?? '-',
@@ -277,16 +277,17 @@ const config = computed(() => {
-
-
+
}
-export type DailyDownloadPoint = { downloads: number; day: string; timestamp: number }
-export type WeeklyDownloadPoint = {
- downloads: number
+export type DailyDataPoint = { value: number; day: string; timestamp: number }
+export type WeeklyDataPoint = {
+ value: number
weekKey: string
weekStart: string
weekEnd: string
timestampStart: number
timestampEnd: number
}
-export type MonthlyDownloadPoint = { downloads: number; month: string; timestamp: number }
-export type YearlyDownloadPoint = { downloads: number; year: string; timestamp: number }
+export type MonthlyDataPoint = { value: number; month: string; timestamp: number }
+export type YearlyDataPoint = { value: number; year: string; timestamp: number }
-type PackageDownloadEvolutionOptionsBase = {
+type EvolutionOptionsBase = {
startDate?: string
endDate?: string
}
-export type PackageDownloadEvolutionOptionsDay = PackageDownloadEvolutionOptionsBase & {
+export type EvolutionOptionsDay = EvolutionOptionsBase & {
granularity: 'day'
}
-export type PackageDownloadEvolutionOptionsWeek = PackageDownloadEvolutionOptionsBase & {
+export type EvolutionOptionsWeek = EvolutionOptionsBase & {
granularity: 'week'
weeks?: number
}
-export type PackageDownloadEvolutionOptionsMonth = PackageDownloadEvolutionOptionsBase & {
+export type EvolutionOptionsMonth = EvolutionOptionsBase & {
granularity: 'month'
months?: number
}
-export type PackageDownloadEvolutionOptionsYear = PackageDownloadEvolutionOptionsBase & {
+export type EvolutionOptionsYear = EvolutionOptionsBase & {
granularity: 'year'
}
-export type PackageDownloadEvolutionOptions =
- | PackageDownloadEvolutionOptionsDay
- | PackageDownloadEvolutionOptionsWeek
- | PackageDownloadEvolutionOptionsMonth
- | PackageDownloadEvolutionOptionsYear
+export type EvolutionOptions =
+ | EvolutionOptionsDay
+ | EvolutionOptionsWeek
+ | EvolutionOptionsMonth
+ | EvolutionOptionsYear
-type DailyDownloadsResponse = { downloads: Array<{ day: string; downloads: number }> }
+type DailyRawPoint = { day: string; value: number }
function toIsoDateString(date: Date): string {
return date.toISOString().slice(0, 10)
@@ -105,23 +105,19 @@ function splitIsoRangeIntoChunksInclusive(
return chunks
}
-function mergeDailyPoints(
- points: Array<{ day: string; downloads: number }>,
-): Array<{ day: string; downloads: number }> {
- const downloadsByDay = new Map
()
+function mergeDailyPoints(points: DailyRawPoint[]): DailyRawPoint[] {
+ const valuesByDay = new Map()
for (const point of points) {
- downloadsByDay.set(point.day, (downloadsByDay.get(point.day) ?? 0) + point.downloads)
+ valuesByDay.set(point.day, (valuesByDay.get(point.day) ?? 0) + point.value)
}
- return Array.from(downloadsByDay.entries())
+ return Array.from(valuesByDay.entries())
.sort(([a], [b]) => a.localeCompare(b))
- .map(([day, downloads]) => ({ day, downloads }))
+ .map(([day, value]) => ({ day, value }))
}
-function buildDailyEvolutionFromDaily(
- daily: Array<{ day: string; downloads: number }>,
-): Array<{ day: string; downloads: number; timestamp: number }> {
+export function buildDailyEvolutionFromDaily(daily: DailyRawPoint[]): DailyDataPoint[] {
return daily
.slice()
.sort((a, b) => a.day.localeCompare(b.day))
@@ -129,15 +125,15 @@ function buildDailyEvolutionFromDaily(
const dayDate = parseIsoDateOnly(item.day)
const timestamp = dayDate.getTime()
- return { day: item.day, downloads: item.downloads, timestamp }
+ return { day: item.day, value: item.value, timestamp }
})
}
-function buildRollingWeeklyEvolutionFromDaily(
- daily: Array<{ day: string; downloads: number }>,
+export function buildRollingWeeklyEvolutionFromDaily(
+ daily: DailyRawPoint[],
rangeStartIso: string,
rangeEndIso: string,
-): WeeklyDownloadPoint[] {
+): WeeklyDataPoint[] {
const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day))
const rangeStartDate = parseIsoDateOnly(rangeStartIso)
const rangeEndDate = parseIsoDateOnly(rangeEndIso)
@@ -150,12 +146,12 @@ function buildRollingWeeklyEvolutionFromDaily(
if (dayOffset < 0) continue
const weekIndex = Math.floor(dayOffset / 7)
- groupedByIndex.set(weekIndex, (groupedByIndex.get(weekIndex) ?? 0) + item.downloads)
+ groupedByIndex.set(weekIndex, (groupedByIndex.get(weekIndex) ?? 0) + item.value)
}
return Array.from(groupedByIndex.entries())
.sort(([a], [b]) => a - b)
- .map(([weekIndex, downloads]) => {
+ .map(([weekIndex, value]) => {
const weekStartDate = addDays(rangeStartDate, weekIndex * 7)
const weekEndDate = addDays(weekStartDate, 6)
@@ -170,7 +166,7 @@ function buildRollingWeeklyEvolutionFromDaily(
const timestampEnd = clampedWeekEndDate.getTime()
return {
- downloads,
+ value,
weekKey: `${weekStartIso}_${weekEndIso}`,
weekStart: weekStartIso,
weekEnd: weekEndIso,
@@ -180,66 +176,59 @@ function buildRollingWeeklyEvolutionFromDaily(
})
}
-function buildMonthlyEvolutionFromDaily(
- daily: Array<{ day: string; downloads: number }>,
-): Array<{ month: string; downloads: number; timestamp: number }> {
+export function buildMonthlyEvolutionFromDaily(daily: DailyRawPoint[]): MonthlyDataPoint[] {
const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day))
- const downloadsByMonth = new Map()
+ const valuesByMonth = new Map()
for (const item of sorted) {
const month = item.day.slice(0, 7)
- downloadsByMonth.set(month, (downloadsByMonth.get(month) ?? 0) + item.downloads)
+ valuesByMonth.set(month, (valuesByMonth.get(month) ?? 0) + item.value)
}
- return Array.from(downloadsByMonth.entries())
+ return Array.from(valuesByMonth.entries())
.sort(([a], [b]) => a.localeCompare(b))
- .map(([month, downloads]) => {
+ .map(([month, value]) => {
const monthStartDate = parseIsoDateOnly(`${month}-01`)
const timestamp = monthStartDate.getTime()
- return { month, downloads, timestamp }
+ return { month, value, timestamp }
})
}
-function buildYearlyEvolutionFromDaily(
- daily: Array<{ day: string; downloads: number }>,
-): Array<{ year: string; downloads: number; timestamp: number }> {
+export function buildYearlyEvolutionFromDaily(daily: DailyRawPoint[]): YearlyDataPoint[] {
const sorted = daily.slice().sort((a, b) => a.day.localeCompare(b.day))
- const downloadsByYear = new Map()
+ const valuesByYear = new Map()
for (const item of sorted) {
const year = item.day.slice(0, 4)
- downloadsByYear.set(year, (downloadsByYear.get(year) ?? 0) + item.downloads)
+ valuesByYear.set(year, (valuesByYear.get(year) ?? 0) + item.value)
}
- return Array.from(downloadsByYear.entries())
+ return Array.from(valuesByYear.entries())
.sort(([a], [b]) => a.localeCompare(b))
- .map(([year, downloads]) => {
+ .map(([year, value]) => {
const yearStartDate = parseIsoDateOnly(`${year}-01-01`)
const timestamp = yearStartDate.getTime()
- return { year, downloads, timestamp }
+ return { year, value, timestamp }
})
}
-function getClientDailyRangePromiseCache() {
- if (!import.meta.client) return null
+const npmDailyRangeCache = import.meta.client ? new Map>() : null
+const likesEvolutionCache = import.meta.client ? new Map>() : null
- const globalScope = globalThis as unknown as {
- __npmDailyRangePromiseCache?: Map>>
- }
-
- if (!globalScope.__npmDailyRangePromiseCache) {
- globalScope.__npmDailyRangePromiseCache = new Map()
- }
-
- return globalScope.__npmDailyRangePromiseCache
+/** Clears client-side promise caches. Exported for use in tests. */
+export function clearClientCaches() {
+ npmDailyRangeCache?.clear()
+ likesEvolutionCache?.clear()
}
async function fetchDailyRangeCached(packageName: string, startIso: string, endIso: string) {
- const cache = getClientDailyRangePromiseCache()
+ const cache = npmDailyRangeCache
if (!cache) {
const response = await fetchNpmDownloadsRange(packageName, startIso, endIso)
- return [...response.downloads].sort((a, b) => a.day.localeCompare(b.day))
+ return [...response.downloads]
+ .sort((a, b) => a.day.localeCompare(b.day))
+ .map(d => ({ day: d.day, value: d.downloads }))
}
const cacheKey = `${packageName}:${startIso}:${endIso}`
@@ -247,8 +236,10 @@ async function fetchDailyRangeCached(packageName: string, startIso: string, endI
if (cachedPromise) return cachedPromise
const promise = fetchNpmDownloadsRange(packageName, startIso, endIso)
- .then((response: DailyDownloadsResponse) =>
- [...response.downloads].sort((a, b) => a.day.localeCompare(b.day)),
+ .then(response =>
+ [...response.downloads]
+ .sort((a, b) => a.day.localeCompare(b.day))
+ .map(d => ({ day: d.day, value: d.downloads })),
)
.catch(error => {
cache.delete(cacheKey)
@@ -272,7 +263,7 @@ async function fetchDailyRangeChunked(packageName: string, startIso: string, end
return fetchDailyRangeCached(packageName, startIso, endIso)
}
- const all: Array<{ day: string; downloads: number }> = []
+ const all: DailyRawPoint[] = []
for (const range of ranges) {
const part = await fetchDailyRangeCached(packageName, range.startIso, range.endIso)
@@ -288,7 +279,7 @@ function toDateOnly(value?: string): string | null {
return /^\d{4}-\d{2}-\d{2}$/.test(dateOnly) ? dateOnly : null
}
-function getNpmPackageCreationDate(packument: PackumentLikeForTime): string | null {
+export function getNpmPackageCreationDate(packument: PackumentLikeForTime): string | null {
const time = packument.time
if (!time) return null
if (time.created) return time.created
@@ -303,7 +294,7 @@ function getNpmPackageCreationDate(packument: PackumentLikeForTime): string | nu
export function useCharts() {
function resolveDateRange(
- downloadEvolutionOptions: PackageDownloadEvolutionOptions,
+ evolutionOptions: EvolutionOptions,
packageCreatedIso: string | null,
): { start: Date; end: Date } {
const today = new Date()
@@ -311,10 +302,10 @@ export function useCharts() {
Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() - 1),
)
- const endDateOnly = toDateOnly(downloadEvolutionOptions.endDate)
+ const endDateOnly = toDateOnly(evolutionOptions.endDate)
const end = endDateOnly ? parseIsoDateOnly(endDateOnly) : yesterday
- const startDateOnly = toDateOnly(downloadEvolutionOptions.startDate)
+ const startDateOnly = toDateOnly(evolutionOptions.startDate)
if (startDateOnly) {
const start = parseIsoDateOnly(startDateOnly)
return { start, end }
@@ -322,14 +313,14 @@ export function useCharts() {
let start: Date
- if (downloadEvolutionOptions.granularity === 'year') {
+ if (evolutionOptions.granularity === 'year') {
if (packageCreatedIso) {
start = startOfUtcYear(new Date(packageCreatedIso))
} else {
start = addDays(end, -(5 * 365) + 1)
}
- } else if (downloadEvolutionOptions.granularity === 'month') {
- const monthCount = downloadEvolutionOptions.months ?? 12
+ } else if (evolutionOptions.granularity === 'month') {
+ const monthCount = evolutionOptions.months ?? 12
const firstOfThisMonth = startOfUtcMonth(end)
start = new Date(
Date.UTC(
@@ -338,8 +329,8 @@ export function useCharts() {
1,
),
)
- } else if (downloadEvolutionOptions.granularity === 'week') {
- const weekCount = downloadEvolutionOptions.weeks ?? 52
+ } else if (evolutionOptions.granularity === 'week') {
+ const weekCount = evolutionOptions.weeks ?? 52
// Full rolling weeks ending on `end` (yesterday by default)
// Range length is exactly weekCount * 7 days (inclusive)
@@ -354,13 +345,11 @@ export function useCharts() {
async function fetchPackageDownloadEvolution(
packageName: MaybeRefOrGetter,
createdIso: MaybeRefOrGetter,
- downloadEvolutionOptions: MaybeRefOrGetter,
- ): Promise<
- DailyDownloadPoint[] | WeeklyDownloadPoint[] | MonthlyDownloadPoint[] | YearlyDownloadPoint[]
- > {
+ evolutionOptions: MaybeRefOrGetter,
+ ): Promise {
const resolvedPackageName = toValue(packageName)
const resolvedCreatedIso = toValue(createdIso) ?? null
- const resolvedOptions = toValue(downloadEvolutionOptions)
+ const resolvedOptions = toValue(evolutionOptions)
const { start, end } = resolveDateRange(resolvedOptions, resolvedCreatedIso)
@@ -376,8 +365,53 @@ export function useCharts() {
return buildYearlyEvolutionFromDaily(sortedDaily)
}
+ async function fetchPackageLikesEvolution(
+ packageName: MaybeRefOrGetter,
+ evolutionOptions: MaybeRefOrGetter,
+ ): Promise {
+ const resolvedPackageName = toValue(packageName)
+ const resolvedOptions = toValue(evolutionOptions)
+
+ // Fetch daily likes data (with client-side promise caching)
+ const cache = likesEvolutionCache
+ const cacheKey = resolvedPackageName
+
+ let dailyLikesPromise: Promise
+
+ if (cache?.has(cacheKey)) {
+ dailyLikesPromise = cache.get(cacheKey)!
+ } else {
+ dailyLikesPromise = $fetch>(
+ `/api/social/likes-evolution/${resolvedPackageName}`,
+ )
+ .then(data => (data ?? []).map(d => ({ day: d.day, value: d.likes })))
+ .catch(error => {
+ cache?.delete(cacheKey)
+ throw error
+ })
+
+ cache?.set(cacheKey, dailyLikesPromise)
+ }
+
+ const sortedDaily = await dailyLikesPromise
+
+ const { start, end } = resolveDateRange(resolvedOptions, null)
+ const startIso = toIsoDateString(start)
+ const endIso = toIsoDateString(end)
+
+ const filteredDaily = sortedDaily.filter(d => d.day >= startIso && d.day <= endIso)
+
+ if (resolvedOptions.granularity === 'day') return buildDailyEvolutionFromDaily(filteredDaily)
+ if (resolvedOptions.granularity === 'week')
+ return buildRollingWeeklyEvolutionFromDaily(filteredDaily, startIso, endIso)
+ if (resolvedOptions.granularity === 'month')
+ return buildMonthlyEvolutionFromDaily(filteredDaily)
+ return buildYearlyEvolutionFromDaily(filteredDaily)
+ }
+
return {
fetchPackageDownloadEvolution,
+ fetchPackageLikesEvolution,
getNpmPackageCreationDate,
}
}
diff --git a/i18n/locales/ar.json b/i18n/locales/ar.json
index 2ab1882b8..5f54bd812 100644
--- a/i18n/locales/ar.json
+++ b/i18n/locales/ar.json
@@ -306,8 +306,7 @@
"downloads": {
"title": "التنزيلات الأسبوعية",
"analyze": "تحليل التنزيلات",
- "community_distribution": "عرض توزيع تبنّي المجتمع",
- "modal_title": "التنزيلات"
+ "community_distribution": "عرض توزيع تبنّي المجتمع"
},
"install_scripts": {
"title": "سكربتات التثبيت",
diff --git a/i18n/locales/az-AZ.json b/i18n/locales/az-AZ.json
index c353c16d7..efc17be05 100644
--- a/i18n/locales/az-AZ.json
+++ b/i18n/locales/az-AZ.json
@@ -243,8 +243,7 @@
},
"downloads": {
"title": "Həftəlik Endirmələr",
- "analyze": "Endirmələri təhlil et",
- "modal_title": "Endirmələr"
+ "analyze": "Endirmələri təhlil et"
},
"install_scripts": {
"title": "Quraşdırma Skriptləri",
diff --git a/i18n/locales/bn-IN.json b/i18n/locales/bn-IN.json
index 892699100..deae04901 100644
--- a/i18n/locales/bn-IN.json
+++ b/i18n/locales/bn-IN.json
@@ -272,8 +272,7 @@
},
"downloads": {
"title": "সাপ্তাহিক ডাউনলোড",
- "analyze": "ডাউনলোড বিশ্লেষণ করুন",
- "modal_title": "ডাউনলোড"
+ "analyze": "ডাউনলোড বিশ্লেষণ করুন"
},
"install_scripts": {
"title": "ইনস্টল স্ক্রিপ্ট",
diff --git a/i18n/locales/cs-CZ.json b/i18n/locales/cs-CZ.json
index ca06a3784..2272a361f 100644
--- a/i18n/locales/cs-CZ.json
+++ b/i18n/locales/cs-CZ.json
@@ -278,8 +278,7 @@
},
"downloads": {
"title": "Týdenní stažení",
- "analyze": "Analyzovat stažení",
- "modal_title": "Stažení"
+ "analyze": "Analyzovat stažení"
},
"install_scripts": {
"title": "Instalační skripty",
diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json
index 7b91cdac7..1031b602a 100644
--- a/i18n/locales/de-DE.json
+++ b/i18n/locales/de-DE.json
@@ -329,8 +329,7 @@
"downloads": {
"title": "Wöchentliche Downloads",
"analyze": "Downloads analysieren",
- "community_distribution": "Community-Adoptionsverteilung ansehen",
- "modal_title": "Downloads"
+ "community_distribution": "Community-Adoptionsverteilung ansehen"
},
"install_scripts": {
"title": "Installationsskripte",
@@ -960,9 +959,6 @@
"types_none": "Keine",
"vulnerabilities_summary": "{count} ({critical}C/{high}H)",
"up_to_you": "Deine Entscheidung!"
- },
- "trends": {
- "title": "Wöchentliche Downloads"
}
}
},
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index b87818fb1..46607f6a6 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -339,15 +339,17 @@
"legend_estimation": "Estimation",
"no_data": "No data available",
"y_axis_label": "{granularity} {facet}",
+ "facet": "Facet",
+ "title": "Trends",
"items": {
- "downloads": "Downloads"
+ "downloads": "Downloads",
+ "likes": "Likes"
}
},
"downloads": {
"title": "Weekly Downloads",
"analyze": "Analyze downloads",
- "community_distribution": "View community adoption distribution",
- "modal_title": "Downloads"
+ "community_distribution": "View community adoption distribution"
},
"install_scripts": {
"title": "Install Scripts",
@@ -980,7 +982,7 @@
"up_to_you": "Up to you!"
},
"trends": {
- "title": "Weekly Downloads"
+ "title": "Compare Trends"
}
}
},
diff --git a/i18n/locales/es.json b/i18n/locales/es.json
index 58212ca0a..13483b8d5 100644
--- a/i18n/locales/es.json
+++ b/i18n/locales/es.json
@@ -307,8 +307,7 @@
"downloads": {
"title": "Descargas Semanales",
"analyze": "Analizar descargas",
- "community_distribution": "Ver distribución de adopción comunitaria",
- "modal_title": "Descargas"
+ "community_distribution": "Ver distribución de adopción comunitaria"
},
"install_scripts": {
"title": "Scripts de Instalación",
diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json
index d48837801..dc3fc9f86 100644
--- a/i18n/locales/fr-FR.json
+++ b/i18n/locales/fr-FR.json
@@ -314,31 +314,10 @@
"show_more": "(afficher {count} de plus)",
"show_less": "(afficher moins)"
},
- "trends": {
- "granularity": "Granularité",
- "granularity_daily": "Quotidien",
- "granularity_weekly": "Hebdomadaire",
- "granularity_monthly": "Mensuel",
- "granularity_yearly": "Annuel",
- "start_date": "Début",
- "end_date": "Fin",
- "loading": "Chargement...",
- "date_range": "{start} au {end}",
- "date_range_multiline": "{start}\nau {end}",
- "download_file": "Télécharger {fileType}",
- "toggle_annotator": "Afficher/Masquer l'annotateur",
- "legend_estimation": "Estimation",
- "no_data": "Données non disponibles",
- "y_axis_label": "{facet} {granularity}",
- "items": {
- "downloads": "Téléchargements"
- }
- },
"downloads": {
"title": "Téléchargements hebdomadaires",
"analyze": "Analyser les téléchargements",
- "community_distribution": "Voir la distribution des versions téléchargées par la communauté",
- "modal_title": "Téléchargements"
+ "community_distribution": "Voir la distribution des versions téléchargées par la communauté"
},
"install_scripts": {
"title": "Scripts d'installation",
@@ -422,6 +401,29 @@
"name_asc": "Nom (A-Z)",
"name_desc": "Nom (Z-A)"
},
+ "trends": {
+ "granularity": "Granularité",
+ "granularity_daily": "Quotidien",
+ "granularity_weekly": "Hebdomadaire",
+ "granularity_monthly": "Mensuel",
+ "granularity_yearly": "Annuel",
+ "start_date": "Début",
+ "end_date": "Fin",
+ "loading": "Chargement...",
+ "date_range": "{start} au {end}",
+ "date_range_multiline": "{start}\nau {end}",
+ "download_file": "Télécharger {fileType}",
+ "toggle_annotator": "Afficher/Masquer l'annotateur",
+ "legend_estimation": "Estimation",
+ "no_data": "Données non disponibles",
+ "y_axis_label": "{facet} {granularity}",
+ "facet": "Facette",
+ "title": "Tendances",
+ "items": {
+ "downloads": "Téléchargements",
+ "likes": "J'aime"
+ }
+ },
"size": {
"b": "{size} o",
"kb": "{size} ko",
@@ -970,7 +972,7 @@
"up_to_you": "À vous de décider !"
},
"trends": {
- "title": "Téléchargements hebdomadaires"
+ "title": "Comparer les tendances"
}
}
},
diff --git a/i18n/locales/hi-IN.json b/i18n/locales/hi-IN.json
index 1c1a8424b..c05efad54 100644
--- a/i18n/locales/hi-IN.json
+++ b/i18n/locales/hi-IN.json
@@ -273,8 +273,7 @@
},
"downloads": {
"title": "साप्ताहिक डाउनलोड्स",
- "analyze": "डाउनलोड्स का विश्लेषण करें",
- "modal_title": "डाउनलोड्स"
+ "analyze": "डाउनलोड्स का विश्लेषण करें"
},
"install_scripts": {
"title": "इंस्टॉल स्क्रिप्ट्स",
diff --git a/i18n/locales/hu-HU.json b/i18n/locales/hu-HU.json
index ecd47f1f3..80328ff5c 100644
--- a/i18n/locales/hu-HU.json
+++ b/i18n/locales/hu-HU.json
@@ -242,8 +242,7 @@
},
"downloads": {
"title": "Heti letöltések",
- "analyze": "Letöltések elemzése",
- "modal_title": "Letöltések"
+ "analyze": "Letöltések elemzése"
},
"install_scripts": {
"title": "Telepítő scriptek",
diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json
index 3d7d4a12c..fcd298fff 100644
--- a/i18n/locales/id-ID.json
+++ b/i18n/locales/id-ID.json
@@ -259,8 +259,7 @@
},
"downloads": {
"title": "Unduhan Mingguan",
- "analyze": "Analisis unduhan",
- "modal_title": "Unduhan"
+ "analyze": "Analisis unduhan"
},
"install_scripts": {
"title": "Skrip Instalasi",
diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json
index 97b7dd883..84787109f 100644
--- a/i18n/locales/it-IT.json
+++ b/i18n/locales/it-IT.json
@@ -313,8 +313,7 @@
"downloads": {
"title": "Download settimanali",
"analyze": "Analizza download",
- "community_distribution": "Visualizza distribuzione adozione della comunità",
- "modal_title": "Download"
+ "community_distribution": "Visualizza distribuzione adozione della comunità"
},
"install_scripts": {
"title": "Script di installazione",
diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json
index 80d0efe69..00e0744a7 100644
--- a/i18n/locales/ja-JP.json
+++ b/i18n/locales/ja-JP.json
@@ -307,8 +307,7 @@
"downloads": {
"title": "週間ダウンロード数",
"analyze": "ダウンロード数を分析",
- "community_distribution": "コミュニティの採用分布を表示",
- "modal_title": "ダウンロード数"
+ "community_distribution": "コミュニティの採用分布を表示"
},
"install_scripts": {
"title": "インストールスクリプト",
diff --git a/i18n/locales/ne-NP.json b/i18n/locales/ne-NP.json
index 9bc008675..140aa8388 100644
--- a/i18n/locales/ne-NP.json
+++ b/i18n/locales/ne-NP.json
@@ -259,8 +259,7 @@
},
"downloads": {
"title": "साप्ताहिक डाउनलोड",
- "analyze": "डाउनलोड विश्लेषण",
- "modal_title": "डाउनलोडहरू"
+ "analyze": "डाउनलोड विश्लेषण"
},
"install_scripts": {
"title": "इन्स्टल स्क्रिप्टहरू",
diff --git a/i18n/locales/no-NO.json b/i18n/locales/no-NO.json
index 3ab1abe74..96b325ba8 100644
--- a/i18n/locales/no-NO.json
+++ b/i18n/locales/no-NO.json
@@ -285,8 +285,7 @@
"downloads": {
"title": "Ukentlige nedlastinger",
"analyze": "Analyser nedlastinger",
- "community_distribution": "Vis distribusjon av bruk i fellesskapet",
- "modal_title": "Nedlastinger"
+ "community_distribution": "Vis distribusjon av bruk i fellesskapet"
},
"install_scripts": {
"title": "Installasjonsskript",
diff --git a/i18n/locales/pl-PL.json b/i18n/locales/pl-PL.json
index 048fdc18b..f8ef96d81 100644
--- a/i18n/locales/pl-PL.json
+++ b/i18n/locales/pl-PL.json
@@ -286,8 +286,7 @@
},
"downloads": {
"title": "Pobrania tygodniowe",
- "analyze": "Analizuj pobrania",
- "modal_title": "Pobrania"
+ "analyze": "Analizuj pobrania"
},
"install_scripts": {
"title": "Skrypty instalacji",
diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json
index 941820894..0be3186b2 100644
--- a/i18n/locales/pt-BR.json
+++ b/i18n/locales/pt-BR.json
@@ -274,8 +274,7 @@
},
"downloads": {
"title": "Downloads Semanais",
- "analyze": "Analisar downloads",
- "modal_title": "Downloads"
+ "analyze": "Analisar downloads"
},
"install_scripts": {
"title": "Scripts de Instalação",
diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json
index 2eccddaa3..d1ffc6cf3 100644
--- a/i18n/locales/ru-RU.json
+++ b/i18n/locales/ru-RU.json
@@ -240,8 +240,7 @@
},
"downloads": {
"title": "Загрузки за неделю",
- "analyze": "Анализировать загрузки",
- "modal_title": "Загрузки"
+ "analyze": "Анализировать загрузки"
},
"install_scripts": {
"title": "Скрипты установки",
diff --git a/i18n/locales/te-IN.json b/i18n/locales/te-IN.json
index 417ce86a9..deb15585a 100644
--- a/i18n/locales/te-IN.json
+++ b/i18n/locales/te-IN.json
@@ -273,8 +273,7 @@
},
"downloads": {
"title": "వారపు డౌన్లోడ్లు",
- "analyze": "డౌన్లోడ్లను విశ్లేషించండి",
- "modal_title": "డౌన్లోడ్లు"
+ "analyze": "డౌన్లోడ్లను విశ్లేషించండి"
},
"install_scripts": {
"title": "ఇన్స్టాల్ స్క్రిప్ట్లు",
diff --git a/i18n/locales/uk-UA.json b/i18n/locales/uk-UA.json
index e32042515..44c8572f4 100644
--- a/i18n/locales/uk-UA.json
+++ b/i18n/locales/uk-UA.json
@@ -243,8 +243,7 @@
},
"downloads": {
"title": "Завантажень на тиждень",
- "analyze": "Проаналізувати завантаження",
- "modal_title": "Завантаження"
+ "analyze": "Проаналізувати завантаження"
},
"install_scripts": {
"title": "Скрипти встановлення",
diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json
index 3b621c9ac..917d9ed6d 100644
--- a/i18n/locales/zh-CN.json
+++ b/i18n/locales/zh-CN.json
@@ -337,8 +337,7 @@
"downloads": {
"title": "每周下载量",
"analyze": "分析下载量",
- "community_distribution": "查看社区采用分布",
- "modal_title": "下载量"
+ "community_distribution": "查看社区采用分布"
},
"install_scripts": {
"title": "安装脚本",
@@ -968,9 +967,6 @@
"types_none": "无",
"vulnerabilities_summary": "{count}({critical} 严重/{high} 高)",
"up_to_you": "由你决定!"
- },
- "trends": {
- "title": "每周下载量"
}
}
},
diff --git a/i18n/locales/zh-TW.json b/i18n/locales/zh-TW.json
index 2f2430038..dc6c18374 100644
--- a/i18n/locales/zh-TW.json
+++ b/i18n/locales/zh-TW.json
@@ -304,8 +304,7 @@
"downloads": {
"title": "每週下載量",
"analyze": "分析下載量",
- "community_distribution": "檢視社群採用分布",
- "modal_title": "下載量"
+ "community_distribution": "檢視社群採用分布"
},
"install_scripts": {
"title": "安裝腳本",
diff --git a/i18n/schema.json b/i18n/schema.json
index 1a3e98c56..541930487 100644
--- a/i18n/schema.json
+++ b/i18n/schema.json
@@ -1021,11 +1021,20 @@
"y_axis_label": {
"type": "string"
},
+ "facet": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
"items": {
"type": "object",
"properties": {
"downloads": {
"type": "string"
+ },
+ "likes": {
+ "type": "string"
}
},
"additionalProperties": false
@@ -1044,9 +1053,6 @@
},
"community_distribution": {
"type": "string"
- },
- "modal_title": {
- "type": "string"
}
},
"additionalProperties": false
diff --git a/lunaria/files/ar-EG.json b/lunaria/files/ar-EG.json
index c6e9fc62e..02a2e98fb 100644
--- a/lunaria/files/ar-EG.json
+++ b/lunaria/files/ar-EG.json
@@ -305,8 +305,7 @@
"downloads": {
"title": "التنزيلات الأسبوعية",
"analyze": "تحليل التنزيلات",
- "community_distribution": "عرض توزيع تبنّي المجتمع",
- "modal_title": "التنزيلات"
+ "community_distribution": "عرض توزيع تبنّي المجتمع"
},
"install_scripts": {
"title": "سكربتات التثبيت",
diff --git a/lunaria/files/az-AZ.json b/lunaria/files/az-AZ.json
index 750c48e0b..5daf0093d 100644
--- a/lunaria/files/az-AZ.json
+++ b/lunaria/files/az-AZ.json
@@ -242,8 +242,7 @@
},
"downloads": {
"title": "Həftəlik Endirmələr",
- "analyze": "Endirmələri təhlil et",
- "modal_title": "Endirmələr"
+ "analyze": "Endirmələri təhlil et"
},
"install_scripts": {
"title": "Quraşdırma Skriptləri",
diff --git a/lunaria/files/bn-IN.json b/lunaria/files/bn-IN.json
index 892699100..deae04901 100644
--- a/lunaria/files/bn-IN.json
+++ b/lunaria/files/bn-IN.json
@@ -272,8 +272,7 @@
},
"downloads": {
"title": "সাপ্তাহিক ডাউনলোড",
- "analyze": "ডাউনলোড বিশ্লেষণ করুন",
- "modal_title": "ডাউনলোড"
+ "analyze": "ডাউনলোড বিশ্লেষণ করুন"
},
"install_scripts": {
"title": "ইনস্টল স্ক্রিপ্ট",
diff --git a/lunaria/files/cs-CZ.json b/lunaria/files/cs-CZ.json
index 1ccfc993c..ccdcebdcc 100644
--- a/lunaria/files/cs-CZ.json
+++ b/lunaria/files/cs-CZ.json
@@ -277,8 +277,7 @@
},
"downloads": {
"title": "Týdenní stažení",
- "analyze": "Analyzovat stažení",
- "modal_title": "Stažení"
+ "analyze": "Analyzovat stažení"
},
"install_scripts": {
"title": "Instalační skripty",
diff --git a/lunaria/files/de-DE.json b/lunaria/files/de-DE.json
index c7d4738c4..91f3db42c 100644
--- a/lunaria/files/de-DE.json
+++ b/lunaria/files/de-DE.json
@@ -328,8 +328,7 @@
"downloads": {
"title": "Wöchentliche Downloads",
"analyze": "Downloads analysieren",
- "community_distribution": "Community-Adoptionsverteilung ansehen",
- "modal_title": "Downloads"
+ "community_distribution": "Community-Adoptionsverteilung ansehen"
},
"install_scripts": {
"title": "Installationsskripte",
@@ -959,9 +958,6 @@
"types_none": "Keine",
"vulnerabilities_summary": "{count} ({critical}C/{high}H)",
"up_to_you": "Deine Entscheidung!"
- },
- "trends": {
- "title": "Wöchentliche Downloads"
}
}
},
diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json
index 42c1d35f2..f2f945fae 100644
--- a/lunaria/files/en-GB.json
+++ b/lunaria/files/en-GB.json
@@ -338,15 +338,17 @@
"legend_estimation": "Estimation",
"no_data": "No data available",
"y_axis_label": "{granularity} {facet}",
+ "facet": "Facet",
+ "title": "Trends",
"items": {
- "downloads": "Downloads"
+ "downloads": "Downloads",
+ "likes": "Likes"
}
},
"downloads": {
"title": "Weekly Downloads",
"analyze": "Analyze downloads",
- "community_distribution": "View community adoption distribution",
- "modal_title": "Downloads"
+ "community_distribution": "View community adoption distribution"
},
"install_scripts": {
"title": "Install Scripts",
@@ -979,7 +981,7 @@
"up_to_you": "Up to you!"
},
"trends": {
- "title": "Weekly Downloads"
+ "title": "Compare Trends"
}
}
},
diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json
index 16de18d12..5777d1471 100644
--- a/lunaria/files/en-US.json
+++ b/lunaria/files/en-US.json
@@ -338,15 +338,17 @@
"legend_estimation": "Estimation",
"no_data": "No data available",
"y_axis_label": "{granularity} {facet}",
+ "facet": "Facet",
+ "title": "Trends",
"items": {
- "downloads": "Downloads"
+ "downloads": "Downloads",
+ "likes": "Likes"
}
},
"downloads": {
"title": "Weekly Downloads",
"analyze": "Analyze downloads",
- "community_distribution": "View community adoption distribution",
- "modal_title": "Downloads"
+ "community_distribution": "View community adoption distribution"
},
"install_scripts": {
"title": "Install Scripts",
@@ -979,7 +981,7 @@
"up_to_you": "Up to you!"
},
"trends": {
- "title": "Weekly Downloads"
+ "title": "Compare Trends"
}
}
},
diff --git a/lunaria/files/es-419.json b/lunaria/files/es-419.json
index b151389ff..9b53cc8c2 100644
--- a/lunaria/files/es-419.json
+++ b/lunaria/files/es-419.json
@@ -306,8 +306,7 @@
"downloads": {
"title": "Descargas Semanales",
"analyze": "Analizar descargas",
- "community_distribution": "Ver distribución de adopción comunitaria",
- "modal_title": "Descargas"
+ "community_distribution": "Ver distribución de adopción comunitaria"
},
"install_scripts": {
"title": "Scripts de Instalación",
diff --git a/lunaria/files/es-ES.json b/lunaria/files/es-ES.json
index 9cb7af79c..2618496d4 100644
--- a/lunaria/files/es-ES.json
+++ b/lunaria/files/es-ES.json
@@ -306,8 +306,7 @@
"downloads": {
"title": "Descargas Semanales",
"analyze": "Analizar descargas",
- "community_distribution": "Ver distribución de adopción comunitaria",
- "modal_title": "Descargas"
+ "community_distribution": "Ver distribución de adopción comunitaria"
},
"install_scripts": {
"title": "Scripts de Instalación",
diff --git a/lunaria/files/fr-FR.json b/lunaria/files/fr-FR.json
index b9bc21fa2..56caacd0a 100644
--- a/lunaria/files/fr-FR.json
+++ b/lunaria/files/fr-FR.json
@@ -313,31 +313,10 @@
"show_more": "(afficher {count} de plus)",
"show_less": "(afficher moins)"
},
- "trends": {
- "granularity": "Granularité",
- "granularity_daily": "Quotidien",
- "granularity_weekly": "Hebdomadaire",
- "granularity_monthly": "Mensuel",
- "granularity_yearly": "Annuel",
- "start_date": "Début",
- "end_date": "Fin",
- "loading": "Chargement...",
- "date_range": "{start} au {end}",
- "date_range_multiline": "{start}\nau {end}",
- "download_file": "Télécharger {fileType}",
- "toggle_annotator": "Afficher/Masquer l'annotateur",
- "legend_estimation": "Estimation",
- "no_data": "Données non disponibles",
- "y_axis_label": "{facet} {granularity}",
- "items": {
- "downloads": "Téléchargements"
- }
- },
"downloads": {
"title": "Téléchargements hebdomadaires",
"analyze": "Analyser les téléchargements",
- "community_distribution": "Voir la distribution des versions téléchargées par la communauté",
- "modal_title": "Téléchargements"
+ "community_distribution": "Voir la distribution des versions téléchargées par la communauté"
},
"install_scripts": {
"title": "Scripts d'installation",
@@ -421,6 +400,29 @@
"name_asc": "Nom (A-Z)",
"name_desc": "Nom (Z-A)"
},
+ "trends": {
+ "granularity": "Granularité",
+ "granularity_daily": "Quotidien",
+ "granularity_weekly": "Hebdomadaire",
+ "granularity_monthly": "Mensuel",
+ "granularity_yearly": "Annuel",
+ "start_date": "Début",
+ "end_date": "Fin",
+ "loading": "Chargement...",
+ "date_range": "{start} au {end}",
+ "date_range_multiline": "{start}\nau {end}",
+ "download_file": "Télécharger {fileType}",
+ "toggle_annotator": "Afficher/Masquer l'annotateur",
+ "legend_estimation": "Estimation",
+ "no_data": "Données non disponibles",
+ "y_axis_label": "{facet} {granularity}",
+ "facet": "Facette",
+ "title": "Tendances",
+ "items": {
+ "downloads": "Téléchargements",
+ "likes": "J'aime"
+ }
+ },
"size": {
"b": "{size} o",
"kb": "{size} ko",
@@ -969,7 +971,7 @@
"up_to_you": "À vous de décider !"
},
"trends": {
- "title": "Téléchargements hebdomadaires"
+ "title": "Comparer les tendances"
}
}
},
diff --git a/lunaria/files/hi-IN.json b/lunaria/files/hi-IN.json
index 8bd1b3e35..6413c905d 100644
--- a/lunaria/files/hi-IN.json
+++ b/lunaria/files/hi-IN.json
@@ -272,8 +272,7 @@
},
"downloads": {
"title": "साप्ताहिक डाउनलोड्स",
- "analyze": "डाउनलोड्स का विश्लेषण करें",
- "modal_title": "डाउनलोड्स"
+ "analyze": "डाउनलोड्स का विश्लेषण करें"
},
"install_scripts": {
"title": "इंस्टॉल स्क्रिप्ट्स",
diff --git a/lunaria/files/hu-HU.json b/lunaria/files/hu-HU.json
index 2388a6de0..bbfa97d8a 100644
--- a/lunaria/files/hu-HU.json
+++ b/lunaria/files/hu-HU.json
@@ -241,8 +241,7 @@
},
"downloads": {
"title": "Heti letöltések",
- "analyze": "Letöltések elemzése",
- "modal_title": "Letöltések"
+ "analyze": "Letöltések elemzése"
},
"install_scripts": {
"title": "Telepítő scriptek",
diff --git a/lunaria/files/id-ID.json b/lunaria/files/id-ID.json
index 4a9e09837..be3fd948d 100644
--- a/lunaria/files/id-ID.json
+++ b/lunaria/files/id-ID.json
@@ -258,8 +258,7 @@
},
"downloads": {
"title": "Unduhan Mingguan",
- "analyze": "Analisis unduhan",
- "modal_title": "Unduhan"
+ "analyze": "Analisis unduhan"
},
"install_scripts": {
"title": "Skrip Instalasi",
diff --git a/lunaria/files/it-IT.json b/lunaria/files/it-IT.json
index a6038e3ed..f182921f7 100644
--- a/lunaria/files/it-IT.json
+++ b/lunaria/files/it-IT.json
@@ -312,8 +312,7 @@
"downloads": {
"title": "Download settimanali",
"analyze": "Analizza download",
- "community_distribution": "Visualizza distribuzione adozione della comunità",
- "modal_title": "Download"
+ "community_distribution": "Visualizza distribuzione adozione della comunità"
},
"install_scripts": {
"title": "Script di installazione",
diff --git a/lunaria/files/ja-JP.json b/lunaria/files/ja-JP.json
index ac1c0402a..6951bc342 100644
--- a/lunaria/files/ja-JP.json
+++ b/lunaria/files/ja-JP.json
@@ -306,8 +306,7 @@
"downloads": {
"title": "週間ダウンロード数",
"analyze": "ダウンロード数を分析",
- "community_distribution": "コミュニティの採用分布を表示",
- "modal_title": "ダウンロード数"
+ "community_distribution": "コミュニティの採用分布を表示"
},
"install_scripts": {
"title": "インストールスクリプト",
diff --git a/lunaria/files/ne-NP.json b/lunaria/files/ne-NP.json
index 22c420b83..516896d4d 100644
--- a/lunaria/files/ne-NP.json
+++ b/lunaria/files/ne-NP.json
@@ -258,8 +258,7 @@
},
"downloads": {
"title": "साप्ताहिक डाउनलोड",
- "analyze": "डाउनलोड विश्लेषण",
- "modal_title": "डाउनलोडहरू"
+ "analyze": "डाउनलोड विश्लेषण"
},
"install_scripts": {
"title": "इन्स्टल स्क्रिप्टहरू",
diff --git a/lunaria/files/no-NO.json b/lunaria/files/no-NO.json
index f2a54d225..a6400830b 100644
--- a/lunaria/files/no-NO.json
+++ b/lunaria/files/no-NO.json
@@ -284,8 +284,7 @@
"downloads": {
"title": "Ukentlige nedlastinger",
"analyze": "Analyser nedlastinger",
- "community_distribution": "Vis distribusjon av bruk i fellesskapet",
- "modal_title": "Nedlastinger"
+ "community_distribution": "Vis distribusjon av bruk i fellesskapet"
},
"install_scripts": {
"title": "Installasjonsskript",
diff --git a/lunaria/files/pl-PL.json b/lunaria/files/pl-PL.json
index 7eeb103cb..69027061b 100644
--- a/lunaria/files/pl-PL.json
+++ b/lunaria/files/pl-PL.json
@@ -285,8 +285,7 @@
},
"downloads": {
"title": "Pobrania tygodniowe",
- "analyze": "Analizuj pobrania",
- "modal_title": "Pobrania"
+ "analyze": "Analizuj pobrania"
},
"install_scripts": {
"title": "Skrypty instalacji",
diff --git a/lunaria/files/pt-BR.json b/lunaria/files/pt-BR.json
index 4aef14000..9c5f6dc94 100644
--- a/lunaria/files/pt-BR.json
+++ b/lunaria/files/pt-BR.json
@@ -273,8 +273,7 @@
},
"downloads": {
"title": "Downloads Semanais",
- "analyze": "Analisar downloads",
- "modal_title": "Downloads"
+ "analyze": "Analisar downloads"
},
"install_scripts": {
"title": "Scripts de Instalação",
diff --git a/lunaria/files/ru-RU.json b/lunaria/files/ru-RU.json
index 7ea489fc4..199a41e8d 100644
--- a/lunaria/files/ru-RU.json
+++ b/lunaria/files/ru-RU.json
@@ -239,8 +239,7 @@
},
"downloads": {
"title": "Загрузки за неделю",
- "analyze": "Анализировать загрузки",
- "modal_title": "Загрузки"
+ "analyze": "Анализировать загрузки"
},
"install_scripts": {
"title": "Скрипты установки",
diff --git a/lunaria/files/te-IN.json b/lunaria/files/te-IN.json
index 9f3221bf6..df8f67fae 100644
--- a/lunaria/files/te-IN.json
+++ b/lunaria/files/te-IN.json
@@ -272,8 +272,7 @@
},
"downloads": {
"title": "వారపు డౌన్లోడ్లు",
- "analyze": "డౌన్లోడ్లను విశ్లేషించండి",
- "modal_title": "డౌన్లోడ్లు"
+ "analyze": "డౌన్లోడ్లను విశ్లేషించండి"
},
"install_scripts": {
"title": "ఇన్స్టాల్ స్క్రిప్ట్లు",
diff --git a/lunaria/files/uk-UA.json b/lunaria/files/uk-UA.json
index 8e1c920d7..b01199a75 100644
--- a/lunaria/files/uk-UA.json
+++ b/lunaria/files/uk-UA.json
@@ -242,8 +242,7 @@
},
"downloads": {
"title": "Завантажень на тиждень",
- "analyze": "Проаналізувати завантаження",
- "modal_title": "Завантаження"
+ "analyze": "Проаналізувати завантаження"
},
"install_scripts": {
"title": "Скрипти встановлення",
diff --git a/lunaria/files/zh-CN.json b/lunaria/files/zh-CN.json
index 75ed80e3e..670d24974 100644
--- a/lunaria/files/zh-CN.json
+++ b/lunaria/files/zh-CN.json
@@ -336,8 +336,7 @@
"downloads": {
"title": "每周下载量",
"analyze": "分析下载量",
- "community_distribution": "查看社区采用分布",
- "modal_title": "下载量"
+ "community_distribution": "查看社区采用分布"
},
"install_scripts": {
"title": "安装脚本",
@@ -967,9 +966,6 @@
"types_none": "无",
"vulnerabilities_summary": "{count}({critical} 严重/{high} 高)",
"up_to_you": "由你决定!"
- },
- "trends": {
- "title": "每周下载量"
}
}
},
diff --git a/lunaria/files/zh-TW.json b/lunaria/files/zh-TW.json
index e7b3c4bbf..4a9191d25 100644
--- a/lunaria/files/zh-TW.json
+++ b/lunaria/files/zh-TW.json
@@ -303,8 +303,7 @@
"downloads": {
"title": "每週下載量",
"analyze": "分析下載量",
- "community_distribution": "檢視社群採用分布",
- "modal_title": "下載量"
+ "community_distribution": "檢視社群採用分布"
},
"install_scripts": {
"title": "安裝腳本",
diff --git a/server/api/social/likes-evolution/[...pkg].get.ts b/server/api/social/likes-evolution/[...pkg].get.ts
new file mode 100644
index 000000000..37d2bbae3
--- /dev/null
+++ b/server/api/social/likes-evolution/[...pkg].get.ts
@@ -0,0 +1,12 @@
+export default defineEventHandler(async event => {
+ const packageName = getRouterParam(event, 'pkg')
+ if (!packageName) {
+ throw createError({
+ status: 400,
+ message: 'package name not provided',
+ })
+ }
+
+ const likesUtil = new PackageLikesUtils()
+ return await likesUtil.getLikesEvolution(packageName)
+})
diff --git a/server/utils/atproto/utils/likes.ts b/server/utils/atproto/utils/likes.ts
index fe71c2592..ac042d707 100644
--- a/server/utils/atproto/utils/likes.ts
+++ b/server/utils/atproto/utils/likes.ts
@@ -1,5 +1,6 @@
import { $nsid as likeNsid } from '#shared/types/lexicons/dev/npmx/feed/like.defs'
import type { Backlink } from '#shared/utils/constellation'
+import { TID } from '@atproto/common'
//Cache keys and helpers
const CACHE_PREFIX = 'atproto-likes:'
@@ -8,29 +9,59 @@ const CACHE_USER_LIKES_KEY = (packageName: string, did: string) =>
`${CACHE_PREFIX}${packageName}:users:${did}:liked`
const CACHE_USERS_BACK_LINK = (packageName: string, did: string) =>
`${CACHE_PREFIX}${packageName}:users:${did}:backlink`
+const CACHE_EVOLUTION_KEY = (packageName: string) => `${CACHE_PREFIX}${packageName}:evolution`
const CACHE_MAX_AGE = CACHE_MAX_AGE_ONE_MINUTE * 5
+/**
+ * Decodes TID timestamps from backlink rkeys and groups by day.
+ * Pure function — no I/O, no side effects.
+ */
+export function aggregateBacklinksByDay(
+ backlinks: Backlink[],
+): Array<{ day: string; likes: number }> {
+ const countsByDay = new Map()
+ for (const backlink of backlinks) {
+ try {
+ const tid = TID.fromStr(backlink.rkey)
+ const timestampMs = tid.timestamp() / 1000
+ const date = new Date(timestampMs)
+ const day = date.toISOString().slice(0, 10)
+ countsByDay.set(day, (countsByDay.get(day) ?? 0) + 1)
+ } catch {
+ console.warn(`Skipping non-TID rkey: ${backlink.rkey}`)
+ }
+ }
+ return Array.from(countsByDay.entries())
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([day, likes]) => ({ day, likes }))
+}
+
+/** The subset of Constellation that PackageLikesUtils actually needs. */
+export type ConstellationLike = Pick
+
/**
* Logic to handle liking, unliking, and seeing if a user has liked a package on npmx
*/
export class PackageLikesUtils {
- private readonly constellation: Constellation
+ private readonly constellation: ConstellationLike
private readonly cache: CacheAdapter
- constructor() {
- this.constellation = new Constellation(
- // Passes in a fetch wrapped as cachedfetch since are already doing some heavy caching here
- async (
- url: string,
- options: Parameters[1] = {},
- _ttl?: number,
- ): Promise> => {
- const data = (await $fetch(url, options)) as T
- return { data, isStale: false, cachedAt: null }
- },
- )
- this.cache = getCacheAdapter('generic')
+ constructor(deps?: { constellation?: ConstellationLike; cache?: CacheAdapter }) {
+ this.constellation =
+ deps?.constellation ??
+ new Constellation(
+ // Passes in a fetch wrapped as cachedfetch since are already doing some heavy caching here
+ async (
+ url: string,
+ options: Parameters[1] = {},
+ _ttl?: number,
+ ): Promise> => {
+ const data = (await $fetch(url, options)) as T
+ return { data, isStale: false, cachedAt: null }
+ },
+ )
+ this.cache = deps?.cache ?? getCacheAdapter('generic')
}
/**
@@ -248,4 +279,41 @@ export class PackageLikesUtils {
userHasLiked: false,
}
}
+
+ /**
+ * Gets the likes evolution for a package as daily {day, likes} points.
+ * Fetches ALL backlinks via paginated constellation calls, decodes TID
+ * timestamps from each rkey, and groups by day.
+ * Results are cached for 5 minutes.
+ */
+ async getLikesEvolution(packageName: string): Promise> {
+ const cacheKey = CACHE_EVOLUTION_KEY(packageName)
+ const cached = await this.cache.get>(cacheKey)
+ if (cached) return cached
+
+ const subjectRef = PACKAGE_SUBJECT_REF(packageName)
+ const allBacklinks: Backlink[] = []
+ let cursor: string | undefined
+
+ // Paginate through all backlinks
+ do {
+ const { data } = await this.constellation.getBackLinks(
+ subjectRef,
+ likeNsid,
+ 'subjectRef',
+ 100,
+ cursor,
+ false,
+ [],
+ 0,
+ )
+ allBacklinks.push(...data.records)
+ cursor = data.cursor
+ } while (cursor)
+
+ const result = aggregateBacklinksByDay(allBacklinks)
+
+ await this.cache.set(cacheKey, result, CACHE_MAX_AGE)
+ return result
+ }
}
diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts
index af4f3dd2d..2a9f1cbd6 100644
--- a/test/nuxt/a11y.spec.ts
+++ b/test/nuxt/a11y.spec.ts
@@ -197,8 +197,8 @@ import {
// The #components import automatically provides the client variant
import HeaderAccountMenuServer from '~/components/Header/AccountMenu.server.vue'
import ToggleServer from '~/components/Settings/Toggle.server.vue'
-import PackageDownloadAnalytics from '~/components/Package/DownloadAnalytics.vue'
import SearchProviderToggleServer from '~/components/SearchProviderToggle.server.vue'
+import PackageTrendsChart from '~/components/Package/TrendsChart.vue'
describe('component accessibility audits', () => {
describe('DateTime', () => {
@@ -607,10 +607,10 @@ describe('component accessibility audits', () => {
// inherently provided by the native